diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index ea06d6d43..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "[Bug Report] Short Form Subject (50 Chars or less)" -labels: bug report -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem please ensure that your screenshots are SFW or at least appropriately censored. - -**Stash Version: (from Settings -> About):** - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..0dc6d10a8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,64 @@ +name: Bug Report +description: Create a report to help us fix the bug +labels: ["bug report"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: textarea + id: description + attributes: + label: Describe the bug + description: Provide a clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: Detail the steps that would replicate this issue. + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behaviour + description: Provide clear and concise description of what you expected to happen. + validations: + required: true + - type: textarea + id: context + attributes: + label: Screenshots or additional context + description: Provide any additional context and SFW screenshots here to help us solve this issue. + validations: + required: false + - type: input + id: stashversion + attributes: + label: Stash version + description: This can be found in Settings > About. + placeholder: (e.g. v0.28.1) + validations: + required: true + - type: input + id: devicedetails + attributes: + label: Device details + description: | + If this is an issue that occurs when using the Stash interface, please provide details of the device/browser used which presents the reported issue. + placeholder: (e.g. Firefox 97 (64-bit) on Windows 11) + validations: + required: false + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output from Settings > Logs. This will be automatically formatted into code, so no need for backticks. + render: shell \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..028fdf8ac --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Community forum + url: https://discourse.stashapp.cc + about: Start a discussion on the community forum. + - name: Community Discord + url: https://discord.gg/Y8MNsvQBvZ + about: Chat with the community on Discord. + - name: Documentation + url: https://docs.stashapp.cc + about: Check out documentation for help and information. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/discussion---request-for-commentary--rfc-.md b/.github/ISSUE_TEMPLATE/discussion---request-for-commentary--rfc-.md deleted file mode 100644 index b79564f83..000000000 --- a/.github/ISSUE_TEMPLATE/discussion---request-for-commentary--rfc-.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: Discussion / Request for Commentary [RFC] -about: This is for issues that will be discussed and won't necessarily result directly - in commits or pull requests. -title: "[RFC] Short Form Title" -labels: help wanted -assignees: '' - ---- - - - -## Long Form - - -## Examples - - -## Reference Reading - diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index db5df9d8b..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: "[Feature] Short Form Title (50 chars or less.)" -labels: feature request -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..f139433c5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,44 @@ +name: Feature Request +description: Request a new feature or idea to be added to Stash +labels: ["feature request"] +body: + - type: textarea + id: description + attributes: + label: Describe the feature you'd like + description: Provide a clear description of the feature you'd like implemented + validations: + required: true + - type: textarea + id: benefits + attributes: + label: Describe the benefits this would bring to existing users + description: | + Explain the measurable benefits this feature would achieve for existing users. + The benefits should be described in terms of outcomes for users, not specific implementations. + validations: + required: true + - type: textarea + id: already_possible + attributes: + label: Is there an existing way to achieve this goal? + description: | + Yes/No. If Yes, describe how your proposed feature differs from or improves upon the current method + validations: + required: true + - type: checkboxes + id: confirm-search + attributes: + label: Have you searched for an existing open/closed issue? + description: | + To help us keep these issues under control, please ensure you have first [searched our issue list](https://github.com/stashapp/stash/issues?q=is%3Aissue) for any existing issues that cover the core request or benefit of your proposal. + options: + - label: I have searched for existing issues and none cover the core request of my proposal + required: true + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + validations: + required: false \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 92c98effc..1e46ecd69 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,10 @@ name: Build on: push: - branches: [ develop, master ] + branches: + - develop + - master + - 'releases/**' pull_request: release: types: [ published ] @@ -12,7 +15,7 @@ concurrency: cancel-in-progress: true env: - COMPILER_IMAGE: stashapp/compiler:11 + COMPILER_IMAGE: stashapp/compiler:12 jobs: build: @@ -37,7 +40,7 @@ jobs: cache-name: cache-node_modules with: path: ui/v2.5/node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('ui/v2.5/yarn.lock') }} + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('ui/v2.5/pnpm-lock.yaml') }} - name: Cache UI build uses: actions/cache@v3 @@ -46,7 +49,7 @@ jobs: cache-name: cache-ui with: path: ui/v2.5/build - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('ui/v2.5/yarn.lock', 'ui/v2.5/public/**', 'ui/v2.5/src/**', 'graphql/**/*.graphql') }} + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('ui/v2.5/pnpm-lock.yaml', 'ui/v2.5/public/**', 'ui/v2.5/src/**', 'graphql/**/*.graphql') }} - name: Cache go build uses: actions/cache@v3 @@ -65,7 +68,7 @@ jobs: docker run -d --name build --mount type=bind,source="$(pwd)",target=/stash,consistency=delegated --mount type=bind,source="$(pwd)/.go-cache",target=/root/.cache/go-build,consistency=delegated --env OFFICIAL_BUILD=${{ env.official-build }} -w /stash $COMPILER_IMAGE tail -f /dev/null - name: Pre-install - run: docker exec -t build /bin/bash -c "make pre-ui" + run: docker exec -t build /bin/bash -c "make CI=1 pre-ui" - name: Generate run: docker exec -t build /bin/bash -c "make generate" diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 1b7838b62..71c743ced 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -6,10 +6,11 @@ on: branches: - master - develop + - 'releases/**' pull_request: env: - COMPILER_IMAGE: stashapp/compiler:11 + COMPILER_IMAGE: stashapp/compiler:12 jobs: golangci: diff --git a/.idea/go.iml b/.idea/go.iml index eddfcc6c3..86461b085 100644 --- a/.idea/go.iml +++ b/.idea/go.iml @@ -1,5 +1,6 @@ + @@ -10,4 +11,4 @@ - + \ No newline at end of file diff --git a/Makefile b/Makefile index b6d0a9e28..7e19063a3 100644 --- a/Makefile +++ b/Makefile @@ -275,7 +275,7 @@ generate: generate-backend generate-ui .PHONY: generate-ui generate-ui: - cd ui/v2.5 && yarn run gqlgen + cd ui/v2.5 && npm run gqlgen .PHONY: generate-backend generate-backend: touch-ui @@ -338,9 +338,19 @@ server-clean: # installs UI dependencies. Run when first cloning repository, or if UI # dependencies have changed +# If CI is set, configures pnpm to use a local store to avoid +# putting .pnpm-store in /stash +# NOTE: to run in the docker build container, using the existing +# node_modules folder, rename the .modules.yaml to .modules.yaml.bak +# and a new one will be generated. This will need to be reversed after +# building. .PHONY: pre-ui pre-ui: - cd ui/v2.5 && yarn install --frozen-lockfile +ifdef CI + cd ui/v2.5 && pnpm config set store-dir ~/.pnpm-store && pnpm install --frozen-lockfile +else + cd ui/v2.5 && pnpm install --frozen-lockfile +endif .PHONY: ui-env ui-env: build-info @@ -359,7 +369,7 @@ ui: ui-only generate-login-locale .PHONY: ui-only ui-only: ui-env - cd ui/v2.5 && yarn build + cd ui/v2.5 && npm run build .PHONY: zip-ui zip-ui: @@ -368,20 +378,24 @@ zip-ui: .PHONY: ui-start ui-start: ui-env - cd ui/v2.5 && yarn start --host + cd ui/v2.5 && npm run start -- --host .PHONY: fmt-ui fmt-ui: - cd ui/v2.5 && yarn format + cd ui/v2.5 && npm run format # runs all of the frontend PR-acceptance steps .PHONY: validate-ui validate-ui: - cd ui/v2.5 && yarn run validate + cd ui/v2.5 && npm run validate # these targets run the same steps as fmt-ui and validate-ui, but only on files that have changed fmt-ui-quick: - cd ui/v2.5 && yarn run prettier --write $$(git diff --name-only --relative --diff-filter d . ../../graphql) + cd ui/v2.5 && \ + files=$$(git diff --name-only --relative --diff-filter d . ../../graphql); \ + if [ -n "$$files" ]; then \ + npm run prettier -- --write $$files; \ + fi # does not run tsc checks, as they are slow validate-ui-quick: @@ -389,9 +403,9 @@ validate-ui-quick: tsfiles=$$(git diff --name-only --relative --diff-filter d src | grep -e "\.tsx\?\$$"); \ scssfiles=$$(git diff --name-only --relative --diff-filter d src | grep "\.scss"); \ prettyfiles=$$(git diff --name-only --relative --diff-filter d . ../../graphql); \ - if [ -n "$$tsfiles" ]; then yarn run eslint $$tsfiles; fi && \ - if [ -n "$$scssfiles" ]; then yarn run stylelint $$scssfiles; fi && \ - if [ -n "$$prettyfiles" ]; then yarn run prettier --check $$prettyfiles; fi + if [ -n "$$tsfiles" ]; then npm run eslint -- $$tsfiles; fi && \ + if [ -n "$$scssfiles" ]; then npm run stylelint -- $$scssfiles; fi && \ + if [ -n "$$prettyfiles" ]; then npm run prettier -- --check $$prettyfiles; fi # runs all of the backend PR-acceptance steps .PHONY: validate-backend diff --git a/README.md b/README.md index c54d94528..5ccefe4bc 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ [![GitHub release (latest by date)](https://img.shields.io/github/v/release/stashapp/stash?logo=github)](https://github.com/stashapp/stash/releases/latest) [![GitHub issues by-label](https://img.shields.io/github/issues-raw/stashapp/stash/bounty)](https://github.com/stashapp/stash/labels/bounty) -### **Stash is a self-hosted webapp written in Go which organizes and serves your porn.** -![demo image](docs/readme_assets/demo_image.png) +### **Stash is a self-hosted webapp written in Go which organizes and serves your diverse content collection, catering to both your SFW and NSFW needs.** + +![Screenshot of Stash web application interface](docs/readme_assets/demo_image.png) * Stash gathers information about videos in your collection from the internet, and is extensible through the use of community-built plugins for a large number of content producers and sites. * Stash supports a wide variety of both video and image formats. @@ -19,80 +20,88 @@ You can [watch a SFW demo video](https://vimeo.com/545323354) to see it in action. -For further information you can consult the [documentation](https://docs.stashapp.cc) or [read the in-app manual](ui/v2.5/src/docs/en). +For further information you can consult the [documentation](https://docs.stashapp.cc) or access the in-app manual from within the application (also available at [docs.stashapp.cc/in-app-manual](https://docs.stashapp.cc/in-app-manual)). # Installing Stash +Step-by-step instructions are available at [docs.stashapp.cc/installation](https://docs.stashapp.cc/installation/). + #### Windows Users: -As of version 0.27.0, Stash doesn't support anymore _Windows 7, 8, Server 2008 and Server 2012._ -Windows 10 or Server 2016 are at least required. +As of version 0.27.0, Stash no longer supports _Windows 7, 8, Server 2008 and Server 2012._ +At least Windows 10 or Server 2016 is required. #### Mac Users: -As of version 0.29.0, Stash requires at least _macOS 11 Big Sur._ -Stash can still be ran through docker on older versions of macOS +As of version 0.29.0, Stash requires _macOS 11 Big Sur_ or later. +Stash can still be run through docker on older versions of macOS. Windows | macOS | Linux | Docker :---:|:---:|:---:|:---: [Latest Release](https://github.com/stashapp/stash/releases/latest/download/stash-win.exe)
[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/stash-win.exe) | [Latest Release](https://github.com/stashapp/stash/releases/latest/download/Stash.app.zip)
[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/Stash.app.zip) | [Latest Release (amd64)](https://github.com/stashapp/stash/releases/latest/download/stash-linux)
[Development Preview (amd64)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-linux)
[More Architectures...](https://github.com/stashapp/stash/releases/latest) | [Instructions](docker/production/README.md)
[Sample docker-compose.yml](docker/production/docker-compose.yml) -Download links for other platforms and architectures are available on the [Releases page](https://github.com/stashapp/stash/releases). +Download links for other platforms and architectures are available on the [Releases](https://github.com/stashapp/stash/releases) page. ## First Run #### Windows/macOS Users: Security Prompt -On Windows or macOS, running the app might present a security prompt since the binary isn't yet signed. +On Windows or macOS, running the app might present a security prompt since the application binary isn't yet signed. -On Windows, bypass this by clicking "more info" and then the "run anyway" button. On macOS, Control+Click the app, click "Open", and then "Open" again. +- On Windows, bypass this by clicking "more info" and then the "run anyway" button. +- On macOS, Control+Click the app, click "Open", and then "Open" again. -#### FFmpeg -Stash requires FFmpeg. If you don't have it installed, Stash will download a copy for you. It is recommended that Linux users install `ffmpeg` from their distro's package manager. +#### ffmpeg + +Stash requires FFmpeg. If you don't have it installed, Stash will prompt you to download a copy during setup. It is recommended that Linux users install `ffmpeg` from their distro's package manager. # Usage ## Quickstart Guide -Stash is a web-based application. Once the application is running, the interface is available (by default) from http://localhost:9999. + +Stash is a web-based application. Once the application is running, the interface is available (by default) from `http://localhost:9999`. On first run, Stash will prompt you for some configuration options and media directories to index, called "Scanning" in Stash. After scanning, your media will be available for browsing, curating, editing, and tagging. Stash can pull metadata (performers, tags, descriptions, studios, and more) directly from many sites through the use of [scrapers](https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/docs/en/Manual/Scraping.md), which integrate directly into Stash. Identifying an entire collection will typically require a mix of multiple sources: -- The project maintains [StashDB](https://stashdb.org/), a crowd-sourced repository of scene, studio, and performer information. Connecting it to Stash will allow you to automatically identify much of a typical media collection. It runs on our stash-box software and is primarily focused on mainstream digital scenes and studios. Instructions, invite codes, and more can be found in this guide to [Accessing StashDB](https://guidelines.stashdb.org/docs/faq_getting-started/stashdb/accessing-stashdb/). +- The stashapp team maintains [StashDB](https://stashdb.org/), a crowd-sourced repository of scene, studio, and performer information. Connecting it to Stash will allow you to automatically identify much of a typical media collection. It runs on our stash-box software and is primarily focused on mainstream digital scenes and studios. Instructions, invite codes, and more can be found in this guide to [Accessing StashDB](https://guidelines.stashdb.org/docs/faq_getting-started/stashdb/accessing-stashdb/). - Several community-managed stash-box databases can also be connected to Stash in a similar manner. Each one serves a slightly different niche and follows their own methodology. A rundown of each stash-box, their differences, and the information you need to sign up can be found in this guide to [Accessing Stash-Boxes](https://guidelines.stashdb.org/docs/faq_getting-started/stashdb/accessing-stash-boxes/). -- Many community-maintained scrapers can also be downloaded, installed, and updated from within Stash, allowing you to pull data from a wide range of other websites and databases. They can be found by navigating to Settings -> Metadata Providers -> Available Scrapers -> Community (stable). These can be trickier to use than a stash-box because every scraper works a little differently. For more information, please visit the [CommunityScrapers repository](https://github.com/stashapp/CommunityScrapers). +- Many community-maintained scrapers can also be downloaded, installed, and updated from within Stash, allowing you to pull data from a wide range of other websites and databases. They can be found by navigating to `Settings → Metadata Providers → Available Scrapers → Community (stable)`. These can be trickier to use than a stash-box because every scraper works a little differently. For more information, please visit the [CommunityScrapers repository](https://github.com/stashapp/CommunityScrapers). - All of the above methods of scraping data into Stash are also covered in more detail in our [Guide to Scraping](https://docs.stashapp.cc/beginner-guides/guide-to-scraping/). [StashDB](http://stashdb.org) is the canonical instance of our open source metadata API, [stash-box](https://github.com/stashapp/stash-box). # Translation + [![Translate](https://translate.codeberg.org/widget/stash/stash/svg-badge.svg)](https://translate.codeberg.org/engage/stash/) -Stash is available in 32 languages (so far!) and it could be in your language too. We use Weblate to coordinate community translations. If you want to help us translate Stash into your language, you can make an account at [Codeberg's Weblate](https://translate.codeberg.org/projects/stash/stash/) to get started contributing new languages or improving existing ones. Thanks! +Stash is available in 32 languages (so far!) and it could be in your language too. We use Weblate to coordinate community translations. If you want to help us translate Stash, you can make an account at [Codeberg's Weblate](https://translate.codeberg.org/projects/stash/stash/) to contribute to new or existing languages. Thanks! + +The badge below shows the current translation status of Stash across all supported languages: [![Translation status](https://translate.codeberg.org/widget/stash/stash/multi-auto.svg)](https://translate.codeberg.org/engage/stash/) -## Join Our Community +# Support & Resources -We are excited to announce that we have a new home for support, feature requests, and discussions related to Stash and its associated projects. Join our community on the [Discourse forum](https://discourse.stashapp.cc) to connect with other users, share your ideas, and get help from fellow enthusiasts. +Need help or want to get involved? Start with the documentation, then reach out to the community if you need further assistance. -# Support (FAQ) +- Documentation + - Official docs: https://docs.stashapp.cc - official guides guides and troubleshooting. + - In-app manual: press Shift + ? in the app or view the manual online: https://docs.stashapp.cc/in-app-manual. + - FAQ: https://discourse.stashapp.cc/c/support/faq/28 - common questions and answers. + - Community wiki: https://discourse.stashapp.cc/tags/c/community-wiki/22/stash - guides, how-to’s and tips. + +- Community & discussion + - Community forum: https://discourse.stashapp.cc - community support, feature requests and discussions. + - Discord: https://discord.gg/2TsNFKt - real-time chat and community support. + - GitHub discussions: https://github.com/stashapp/stash/discussions - community support and feature discussions. + - Lemmy community: https://discuss.online/c/stashapp - Reddit-style community space. -Check out our documentation on [Stash-Docs](https://docs.stashapp.cc) for information about the software, questions, guides, add-ons and more. - -For more help you can: -* Check the in-app documentation, in the top right corner of the app (it's also mirrored on [Stash-Docs](https://docs.stashapp.cc/in-app-manual)) -* Join our [community forum](https://discourse.stashapp.cc) -* Join the [Discord server](https://discord.gg/2TsNFKt) -* Start a [discussion on GitHub](https://github.com/stashapp/stash/discussions) - -# Customization - -## Themes and CSS Customization - -There is a [directory of community-created themes](https://docs.stashapp.cc/themes/list) on Stash-Docs. - -You can also change the Stash interface to fit your desired style with various snippets from [Custom CSS snippets](https://docs.stashapp.cc/themes/custom-css-snippets). +- Community scrapers & plugins + - Metadata sources: https://docs.stashapp.cc/metadata-sources/ + - Plugins: https://docs.stashapp.cc/plugins/ + - Themes: https://docs.stashapp.cc/themes/ + - Other projects: https://docs.stashapp.cc/other-projects/ # For Developers diff --git a/cmd/phasher/main.go b/cmd/phasher/main.go index 864195631..e0801d5d7 100644 --- a/cmd/phasher/main.go +++ b/cmd/phasher/main.go @@ -5,20 +5,39 @@ import ( "fmt" "os" "os/exec" + "path/filepath" flag "github.com/spf13/pflag" "github.com/stashapp/stash/pkg/ffmpeg" + "github.com/stashapp/stash/pkg/hash/imagephash" "github.com/stashapp/stash/pkg/hash/videophash" "github.com/stashapp/stash/pkg/models" ) func customUsage() { fmt.Fprintf(os.Stderr, "Usage:\n") - fmt.Fprintf(os.Stderr, "%s [OPTIONS] VIDEOFILE...\n\nOptions:\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "%s [OPTIONS] FILE...\n\nOptions:\n", os.Args[0]) flag.PrintDefaults() } func printPhash(ff *ffmpeg.FFMpeg, ffp *ffmpeg.FFProbe, inputfile string, quiet *bool) error { + // Determine if this is a video or image file based on extension + ext := filepath.Ext(inputfile) + ext = ext[1:] // remove the leading dot + + // Common image extensions + imageExts := map[string]bool{ + "jpg": true, "jpeg": true, "png": true, "gif": true, "webp": true, "bmp": true, + } + + if imageExts[ext] { + return printImagePhash(inputfile, quiet) + } + + return printVideoPhash(ff, ffp, inputfile, quiet) +} + +func printVideoPhash(ff *ffmpeg.FFMpeg, ffp *ffmpeg.FFProbe, inputfile string, quiet *bool) error { ffvideoFile, err := ffp.NewVideoFile(inputfile) if err != nil { return err @@ -46,6 +65,24 @@ func printPhash(ff *ffmpeg.FFMpeg, ffp *ffmpeg.FFProbe, inputfile string, quiet return nil } +func printImagePhash(inputfile string, quiet *bool) error { + imgFile := &models.ImageFile{ + BaseFile: &models.BaseFile{Path: inputfile}, + } + + phash, err := imagephash.Generate(imgFile) + if err != nil { + return err + } + + if *quiet { + fmt.Printf("%x\n", *phash) + } else { + fmt.Printf("%x %v\n", *phash, imgFile.Path) + } + return nil +} + func getPaths() (string, string) { ffmpegPath, _ := exec.LookPath("ffmpeg") ffprobePath, _ := exec.LookPath("ffprobe") @@ -67,7 +104,7 @@ func main() { args := flag.Args() if len(args) < 1 { - fmt.Fprintf(os.Stderr, "Missing VIDEOFILE argument.\n") + fmt.Fprintf(os.Stderr, "Missing FILE argument.\n") flag.Usage() os.Exit(2) } @@ -87,4 +124,5 @@ func main() { fmt.Fprintln(os.Stderr, err) } } + } diff --git a/cmd/stash/main.go b/cmd/stash/main.go index 86edd6276..57fedd0e2 100644 --- a/cmd/stash/main.go +++ b/cmd/stash/main.go @@ -76,6 +76,10 @@ func main() { defer pprof.StopCPUProfile() } + // initialise desktop.IsDesktop here so that it doesn't get affected by + // ffmpeg hardware checks later on + desktop.InitIsDesktop() + mgr, err := manager.Initialize(cfg, l) if err != nil { exitError(fmt.Errorf("manager initialization error: %w", err)) @@ -110,7 +114,7 @@ func main() { // Logs only error level message to stderr. func initLogTemp() *log.Logger { l := log.NewLogger() - l.Init("", true, "Error") + l.Init("", true, "Error", 0) logger.Logger = l return l @@ -118,7 +122,7 @@ func initLogTemp() *log.Logger { func initLog(cfg *config.Config) *log.Logger { l := log.NewLogger() - l.Init(cfg.GetLogFile(), cfg.GetLogOut(), cfg.GetLogLevel()) + l.Init(cfg.GetLogFile(), cfg.GetLogOut(), cfg.GetLogLevel(), cfg.GetLogFileMaxSize()) logger.Logger = l return l diff --git a/docker/build/x86_64/Dockerfile b/docker/build/x86_64/Dockerfile index 4d153e8bc..163bd64b2 100644 --- a/docker/build/x86_64/Dockerfile +++ b/docker/build/x86_64/Dockerfile @@ -1,14 +1,16 @@ # This dockerfile should be built with `make docker-build` from the stash root. # Build Frontend -FROM node:20-alpine AS frontend +FROM node:24-alpine AS frontend RUN apk add --no-cache make git ## cache node_modules separately -COPY ./ui/v2.5/package.json ./ui/v2.5/yarn.lock /stash/ui/v2.5/ +COPY ./ui/v2.5/package.json ./ui/v2.5/pnpm-lock.yaml /stash/ui/v2.5/ WORKDIR /stash COPY Makefile /stash/ COPY ./graphql /stash/graphql/ COPY ./ui /stash/ui/ +# pnpm install with npm +RUN npm install -g pnpm RUN make pre-ui RUN make generate-ui ARG GITHASH diff --git a/docker/build/x86_64/Dockerfile-CUDA b/docker/build/x86_64/Dockerfile-CUDA index 4cab3f6c1..8a0b02e10 100644 --- a/docker/build/x86_64/Dockerfile-CUDA +++ b/docker/build/x86_64/Dockerfile-CUDA @@ -5,11 +5,13 @@ ARG CUDA_VERSION=12.8.0 FROM node:20-alpine AS frontend RUN apk add --no-cache make git ## cache node_modules separately -COPY ./ui/v2.5/package.json ./ui/v2.5/yarn.lock /stash/ui/v2.5/ +COPY ./ui/v2.5/package.json ./ui/v2.5/pnpm-lock.yaml /stash/ui/v2.5/ WORKDIR /stash COPY Makefile /stash/ COPY ./graphql /stash/graphql/ COPY ./ui /stash/ui/ +# pnpm install with npm +RUN npm install -g pnpm RUN make pre-ui RUN make generate-ui ARG GITHASH diff --git a/docker/ci/x86_64/Dockerfile b/docker/ci/x86_64/Dockerfile index 957da347c..6a9c6b76d 100644 --- a/docker/ci/x86_64/Dockerfile +++ b/docker/ci/x86_64/Dockerfile @@ -13,7 +13,7 @@ FROM --platform=$TARGETPLATFORM alpine:latest AS app COPY --from=binary /stash /usr/bin/ RUN apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg tzdata vips vips-tools \ - && pip install --user --break-system-packages mechanicalsoup cloudscraper stashapp-tools + && pip install --break-system-packages mechanicalsoup cloudscraper stashapp-tools ENV STASH_CONFIG_FILE=/root/.stash/config.yml # Basic build-time metadata as defined at https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys diff --git a/docker/compiler/Dockerfile b/docker/compiler/Dockerfile index 40b92c180..0154d7e61 100644 --- a/docker/compiler/Dockerfile +++ b/docker/compiler/Dockerfile @@ -8,15 +8,11 @@ RUN mkdir -p /etc/apt/keyrings ADD https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key nodesource.gpg.key RUN cat nodesource.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && rm nodesource.gpg.key -RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list - -ADD https://dl.yarnpkg.com/debian/pubkey.gpg yarn.gpg -RUN cat yarn.gpg | gpg --dearmor -o /etc/apt/keyrings/yarn.gpg && rm yarn.gpg -RUN echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list +RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list RUN apt-get update && \ apt-get install -y --no-install-recommends \ - git make tar bash nodejs yarn zip \ + git make tar bash nodejs zip \ clang llvm-dev cmake patch libxml2-dev uuid-dev libssl-dev xz-utils \ bzip2 gzip sed cpio libbz2-dev zlib1g-dev \ gcc-mingw-w64 \ @@ -24,6 +20,9 @@ RUN apt-get update && \ gcc-aarch64-linux-gnu libc-dev-arm64-cross && \ rm -rf /var/lib/apt/lists/*; +# pnpm install with npm +RUN npm install -g pnpm + # FreeBSD cross-compilation setup # https://github.com/smartmontools/docker-build/blob/6b8c92560d17d325310ba02d9f5a4b250cb0764a/Dockerfile#L66 ENV FREEBSD_VERSION 13.4 diff --git a/docker/compiler/Makefile b/docker/compiler/Makefile index 275466640..ed6a9a285 100644 --- a/docker/compiler/Makefile +++ b/docker/compiler/Makefile @@ -1,6 +1,6 @@ user=stashapp repo=compiler -version=11 +version=12 latest: docker build -t ${user}/${repo}:latest . diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 4a1cf30df..85c2f6f23 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -5,7 +5,8 @@ * [Go](https://golang.org/dl/) * [GolangCI](https://golangci-lint.run/) - A meta-linter which runs several linters in parallel * To install, follow the [local installation instructions](https://golangci-lint.run/welcome/install/#local-installation) -* [Yarn](https://yarnpkg.com/en/docs/install) - Yarn package manager +* [nodejs](https://nodejs.org/en/download) - nodejs runtime + * corepack/[pnpm](https://pnpm.io/installation) - nodejs package manager (included with nodejs) ## Environment @@ -22,32 +23,22 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MinGW. For examp ### macOS 1. If you don't have it already, install the [Homebrew package manager](https://brew.sh). -2. Install dependencies: `brew install go git yarn gcc make node ffmpeg` +2. Install dependencies: `brew install go git gcc make node ffmpeg` ### Linux #### Arch Linux -1. Install dependencies: `sudo pacman -S go git yarn gcc make nodejs ffmpeg --needed` +1. Install dependencies: `sudo pacman -S go git gcc make nodejs ffmpeg --needed` #### Ubuntu -1. Install dependencies: `sudo apt-get install golang git yarnpkg gcc nodejs ffmpeg -y` +1. Install dependencies: `sudo apt-get install golang git gcc nodejs ffmpeg -y` ### OpenBSD -1. Install dependencies `doas pkg_add gmake go git yarn node cmake` -2. Compile a custom ffmpeg from ports. The default ffmpeg in OpenBSD's packages is not compiled with WebP support, which is required by Stash. - - If you've already installed ffmpeg, uninstall it: `doas pkg_delete ffmpeg` - - If you haven't already, [fetch the ports tree and verify](https://www.openbsd.org/faq/ports/ports.html#PortsFetch). - - Find the ffmpeg port in `/usr/ports/graphics/ffmpeg`, and patch the Makefile to include libwebp - - Add `webp` to `WANTLIB` - - Add `graphics/libwebp` to the list in `LIB_DEPENDS` - - Add `-lwebp -lwebpdecoder -lwebpdemux -lwebpmux` to `LIBavcodec_EXTRALIBS` - - Add `--enable-libweb` to the list in `CONFIGURE_ARGS` - - If you've already built ffmpeg from ports before, you may need to also increment `REVISION` - - Run `doas make install` - - Follow the instructions below to build a release, but replace the final step `make build-release` with `gmake flags-release stash`, to [avoid the PIE buildmode](https://github.com/golang/go/issues/59866). +1. Install dependencies `doas pkg_add gmake go git node cmake ffmpeg` +2. Follow the instructions below to build a release, but replace the final step `make build-release` with `gmake flags-release stash`, to [avoid the PIE buildmode](https://github.com/golang/go/issues/59866). NOTE: The `make` command in OpenBSD will be `gmake`. For example, `make pre-ui` will be `gmake pre-ui`. diff --git a/go.mod b/go.mod index 268276841..db0d6fe34 100644 --- a/go.mod +++ b/go.mod @@ -7,15 +7,15 @@ require ( github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552 github.com/Yamashou/gqlgenc v0.32.1 github.com/anacrolix/dms v1.2.2 - github.com/antchfx/htmlquery v1.3.0 + github.com/antchfx/htmlquery v1.3.5 github.com/asticode/go-astisub v0.25.1 - github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617 - github.com/chromedp/chromedp v0.9.2 + github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d + github.com/chromedp/chromedp v0.14.2 github.com/corona10/goimagehash v1.1.0 github.com/disintegration/imaging v1.6.2 github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d github.com/doug-martin/goqu/v9 v9.18.0 - github.com/go-chi/chi/v5 v5.0.12 + github.com/go-chi/chi/v5 v5.2.2 github.com/go-chi/cors v1.2.1 github.com/go-chi/httplog v0.3.1 github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 @@ -32,7 +32,11 @@ require ( github.com/json-iterator/go v1.1.12 github.com/kermieisinthehouse/gosx-notifier v0.1.2 github.com/kermieisinthehouse/systray v1.2.4 - github.com/knadh/koanf v1.5.0 + github.com/knadh/koanf/parsers/yaml v1.1.0 + github.com/knadh/koanf/providers/env v1.1.0 + github.com/knadh/koanf/providers/file v1.2.0 + github.com/knadh/koanf/providers/posflag v1.0.1 + github.com/knadh/koanf/v2 v2.2.1 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mattn/go-sqlite3 v1.14.22 github.com/mitchellh/mapstructure v1.5.0 @@ -42,7 +46,7 @@ require ( github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd github.com/sirupsen/logrus v1.9.3 github.com/spf13/cast v1.6.0 - github.com/spf13/pflag v1.0.5 + github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 github.com/tidwall/gjson v1.16.0 github.com/vearutop/statigz v1.4.0 @@ -51,33 +55,35 @@ require ( github.com/vektra/mockery/v2 v2.10.0 github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e github.com/zencoder/go-dash/v3 v3.0.2 - golang.org/x/crypto v0.38.0 + golang.org/x/crypto v0.45.0 golang.org/x/image v0.18.0 - golang.org/x/net v0.40.0 - golang.org/x/sys v0.33.0 - golang.org/x/term v0.32.0 - golang.org/x/text v0.25.0 + golang.org/x/net v0.47.0 + golang.org/x/sys v0.38.0 + golang.org/x/term v0.37.0 + golang.org/x/text v0.31.0 golang.org/x/time v0.10.0 gopkg.in/guregu/null.v4 v4.0.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/agnivade/levenshtein v1.2.1 // indirect - github.com/antchfx/xpath v1.2.3 // indirect + github.com/antchfx/xpath v1.3.5 // indirect github.com/asticode/go-astikit v0.20.0 // indirect github.com/asticode/go-astits v1.8.0 // indirect - github.com/chromedp/sysutil v1.0.0 // indirect + github.com/chromedp/sysutil v1.1.0 // indirect github.com/coder/websocket v1.8.12 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.7.0 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect - github.com/gobwas/ws v1.3.0 // indirect + github.com/gobwas/ws v1.4.0 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect @@ -85,9 +91,8 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/josharian/intern v1.0.0 // indirect + github.com/knadh/koanf/maps v0.1.2 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -114,9 +119,10 @@ require ( github.com/urfave/cli/v2 v2.27.6 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/sync v0.14.0 // indirect - golang.org/x/tools v0.33.0 // indirect + go.yaml.in/yaml/v3 v3.0.3 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/tools v0.38.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index bbb38befb..dbe82cf99 100644 --- a/go.sum +++ b/go.sum @@ -72,7 +72,6 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/anacrolix/dms v1.2.2 h1:0mk2/DXNqa5KDDbaLgFPf3oMV6VCGdFNh3d/gt4oafM= github.com/anacrolix/dms v1.2.2/go.mod h1:msPKAoppoNRfrYplJqx63FZ+VipDZ4Xsj3KzIQxyU7k= github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= @@ -86,10 +85,10 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/ github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= -github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E= -github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8= -github.com/antchfx/xpath v1.2.3 h1:CCZWOzv5bAqjVv0offZ2LVgVYFbeldKQVuLNbViZdes= -github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= +github.com/antchfx/htmlquery v1.3.5 h1:aYthDDClnG2a2xePf6tys/UyyM/kRcsFRm+ifhFKoU0= +github.com/antchfx/htmlquery v1.3.5/go.mod h1:5oyIPIa3ovYGtLqMPNjBF2Uf25NPCKsMjCnQ8lvjaoA= +github.com/antchfx/xpath v1.3.5 h1:PqbXLC3TkfeZyakF5eeh3NTWEbYl4VHNVeufANzDbKQ= +github.com/antchfx/xpath v1.3.5/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= @@ -104,16 +103,6 @@ github.com/asticode/go-astisub v0.25.1 h1:RZMGfZPp7CXOkI6g+zCU7DRLuciGPGup921uKZ github.com/asticode/go-astisub v0.25.1/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8= github.com/asticode/go-astits v1.8.0 h1:rf6aiiGn/QhlFjNON1n5plqF3Fs025XLUwiQ0NB6oZg= github.com/asticode/go-astits v1.8.0/go.mod h1:DkOWmBNQpnr9mv24KfZjq4JawCFX1FCqjLVGvO0DygQ= -github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= -github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= -github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= -github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= -github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= -github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -127,13 +116,12 @@ github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= -github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617 h1:/5dwcyi5WOawM1Iz6MjrYqB90TRIdZv3O0fVHEJb86w= -github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= -github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw= -github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= -github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= -github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU= +github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= +github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM= +github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo= +github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= +github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -185,7 +173,6 @@ github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8 github.com/doug-martin/goqu/v9 v9.18.0 h1:/6bcuEtAe6nsSMVK/M+fOiXUNfyFF3yYtE07DBPFMYY= github.com/doug-martin/goqu/v9 v9.18.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ= github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -200,19 +187,17 @@ github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= -github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/httplog v0.3.1 h1:uC3IUWCZagtbCinb3ypFh36SEcgd6StWw2Bu0XSXRtg= @@ -220,31 +205,28 @@ github.com/go-chi/httplog v0.3.1/go.mod h1:UoiQQ/MTZH5V6JbNB2FzF0DynTh5okpXxlhsy github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs= +github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= -github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= -github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -288,7 +270,6 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -305,7 +286,7 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -346,11 +327,9 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= -github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= @@ -358,8 +337,6 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= -github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -369,17 +346,12 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= @@ -394,14 +366,8 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= -github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= -github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hasura/go-graphql-client v0.13.1 h1:kKbjhxhpwz58usVl+Xvgah/TDha5K2akNTRQdsEHN6U= github.com/hasura/go-graphql-client v0.13.1/go.mod h1:k7FF7h53C+hSNFRG3++DdVZWIuHdCaTbI7siTJ//zGQ= -github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= -github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -412,18 +378,10 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= -github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -431,17 +389,25 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kermieisinthehouse/gosx-notifier v0.1.2 h1:KV0KBeKK2B24kIHY7iK0jgS64Q05f4oB+hUZmsPodxQ= github.com/kermieisinthehouse/gosx-notifier v0.1.2/go.mod h1:xyWT07azFtUOcHl96qMVvKhvKzsMcS7rKTHQyv8WTho= github.com/kermieisinthehouse/systray v1.2.4 h1:pdH5vnl+KKjRrVCRU4g/2W1/0HVzuuJ6WXHlPPHYY6s= github.com/kermieisinthehouse/systray v1.2.4/go.mod h1:axh6C/jNuSyC0QGtidZJURc9h+h41HNoMySoLVrhVR4= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= -github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= +github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= +github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/yaml v1.1.0 h1:3ltfm9ljprAHt4jxgeYLlFPmUaunuCgu1yILuTXRdM4= +github.com/knadh/koanf/parsers/yaml v1.1.0/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg= +github.com/knadh/koanf/providers/env v1.1.0 h1:U2VXPY0f+CsNDkvdsG8GcsnK4ah85WwWyJgef9oQMSc= +github.com/knadh/koanf/providers/env v1.1.0/go.mod h1:QhHHHZ87h9JxJAn2czdEl6pdkNnDh/JS1Vtsyt65hTY= +github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmYIDXI7OJU6+U= +github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= +github.com/knadh/koanf/providers/posflag v1.0.1 h1:EnMxHSrPkYCFnKgBUl5KBgrjed8gVFrcXDzaW4l/C6Y= +github.com/knadh/koanf/providers/posflag v1.0.1/go.mod h1:3Wn3+YG3f4ljzRyCUgIwH7G0sZ1pMjCOsNBovrbKmAk= +github.com/knadh/koanf/v2 v2.2.1 h1:jaleChtw85y3UdBnI0wCqcg1sj1gPoz6D3caGNHtrNE= +github.com/knadh/koanf/v2 v2.2.1/go.mod h1:PSFru3ufQgTsI7IF+95rf9s8XA1+aHxKuO/W+dPoHEY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -465,8 +431,6 @@ github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc8 github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -492,22 +456,17 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -519,26 +478,20 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007 h1:Ohgj9L0EYOgXxkDp+bczlMBiulwmqYzQpvQNUdtt3oc= github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007/go.mod h1:wKCOWMb6iNlvKiOToY2cNuaovSXvIiv1zDi9QDR7aGQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= -github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -555,24 +508,17 @@ github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSg github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E= github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo= -github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -590,8 +536,6 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM= @@ -600,7 +544,6 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -621,8 +564,9 @@ github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU= github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= @@ -683,11 +627,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/zencoder/go-dash/v3 v3.0.2 h1:oP1+dOh+Gp57PkvdCyMfbHtrHaxfl3w4kR3KBBbuqQE= github.com/zencoder/go-dash/v3 v3.0.2/go.mod h1:30R5bKy1aUYY45yesjtZ9l8trNc2TwNqbS17WVQmCzk= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= -go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -701,6 +642,8 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= +go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -718,8 +661,12 @@ golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -761,8 +708,12 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -811,9 +762,14 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -843,18 +799,21 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -866,12 +825,10 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -886,8 +843,6 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -895,7 +850,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -908,7 +862,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -931,21 +884,30 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -953,9 +915,14 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1020,8 +987,11 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1068,7 +1038,6 @@ google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1132,11 +1101,9 @@ google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= @@ -1177,7 +1144,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1190,14 +1156,14 @@ gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.3/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -1214,4 +1180,3 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index 7d0a761da..7fda85b24 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -165,6 +165,12 @@ type Query { input: ScrapeSingleStudioInput! ): [ScrapedStudio!]! + "Scrape for a single tag" + scrapeSingleTag( + source: ScraperSourceInput! + input: ScrapeSingleTagInput! + ): [ScrapedTag!]! + "Scrape for a single performer" scrapeSinglePerformer( source: ScraperSourceInput! @@ -328,6 +334,7 @@ type Mutation { sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker + bulkSceneMarkerUpdate(input: BulkSceneMarkerUpdateInput!): [SceneMarker!] sceneMarkerDestroy(id: ID!): Boolean! sceneMarkersDestroy(ids: [ID!]!): Boolean! @@ -366,11 +373,13 @@ type Mutation { performerDestroy(input: PerformerDestroyInput!): Boolean! performersDestroy(ids: [ID!]!): Boolean! bulkPerformerUpdate(input: BulkPerformerUpdateInput!): [Performer!] + performerMerge(input: PerformerMergeInput!): Performer! studioCreate(input: StudioCreateInput!): Studio studioUpdate(input: StudioUpdateInput!): Studio studioDestroy(input: StudioDestroyInput!): Boolean! studiosDestroy(ids: [ID!]!): Boolean! + bulkStudioUpdate(input: BulkStudioUpdateInput!): [Studio!] movieCreate(input: MovieCreateInput!): Movie @deprecated(reason: "Use groupCreate instead") @@ -413,6 +422,8 @@ type Mutation { """ moveFiles(input: MoveFilesInput!): Boolean! deleteFiles(ids: [ID!]!): Boolean! + "Deletes file entries from the database without deleting the files from the filesystem" + destroyFiles(ids: [ID!]!): Boolean! fileSetFingerprints(input: FileSetFingerprintsInput!): Boolean! diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 4d6d2080b..6990d9d95 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -2,6 +2,8 @@ input SetupInput { "Empty to indicate $HOME/.stash/config.yml default" configLocation: String! stashes: [StashConfigInput!]! + "True if SFW content mode is enabled" + sfwContentMode: Boolean "Empty to indicate default" databaseFile: String! "Empty to indicate default" @@ -67,6 +69,8 @@ input ConfigGeneralInput { databasePath: String "Path to backup directory" backupDirectoryPath: String + "Path to trash directory - if set, deleted files will be moved here instead of being permanently deleted" + deleteTrashPath: String "Path to generated files" generatedPath: String "Path to import/export files" @@ -153,6 +157,8 @@ input ConfigGeneralInput { logLevel: String "Whether to log http access" logAccess: Boolean + "Maximum log size" + logFileMaxSize: Int "True if galleries should be created from folders with images" createGalleriesFromFolders: Boolean "Regex used to identify images as gallery covers" @@ -187,6 +193,8 @@ type ConfigGeneralResult { databasePath: String! "Path to backup directory" backupDirectoryPath: String! + "Path to trash directory - if set, deleted files will be moved here instead of being permanently deleted" + deleteTrashPath: String! "Path to generated files" generatedPath: String! "Path to import/export files" @@ -277,6 +285,8 @@ type ConfigGeneralResult { logLevel: String! "Whether to log http access" logAccess: Boolean! + "Maximum log size" + logFileMaxSize: Int! "Array of video file extensions" videoExtensions: [String!]! "Array of image file extensions" @@ -309,6 +319,7 @@ input ConfigDisableDropdownCreateInput { tag: Boolean studio: Boolean movie: Boolean + gallery: Boolean } enum ImageLightboxDisplayMode { @@ -329,6 +340,7 @@ input ConfigImageLightboxInput { resetZoomOnNav: Boolean scrollMode: ImageLightboxScrollMode scrollAttemptsBeforeChange: Int + disableAnimation: Boolean } type ConfigImageLightboxResult { @@ -338,9 +350,13 @@ type ConfigImageLightboxResult { resetZoomOnNav: Boolean scrollMode: ImageLightboxScrollMode scrollAttemptsBeforeChange: Int! + disableAnimation: Boolean } input ConfigInterfaceInput { + "True if SFW content mode is enabled" + sfwContentMode: Boolean + "Ordered list of items that should be shown in the menu" menuItems: [String!] @@ -379,6 +395,9 @@ input ConfigInterfaceInput { customLocales: String customLocalesEnabled: Boolean + "When true, disables all customizations (plugins, CSS, JavaScript, locales) for troubleshooting" + disableCustomizations: Boolean + "Interface language" language: String @@ -404,9 +423,13 @@ type ConfigDisableDropdownCreate { tag: Boolean! studio: Boolean! movie: Boolean! + gallery: Boolean! } type ConfigInterfaceResult { + "True if SFW content mode is enabled" + sfwContentMode: Boolean! + "Ordered list of items that should be shown in the menu" menuItems: [String!] @@ -449,6 +472,9 @@ type ConfigInterfaceResult { customLocales: String customLocalesEnabled: Boolean + "When true, disables all customizations (plugins, CSS, JavaScript, locales) for troubleshooting" + disableCustomizations: Boolean + "Interface language" language: String diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index e68813fd2..52eec6785 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -84,13 +84,23 @@ input PHashDuplicationCriterionInput { input StashIDCriterionInput { """ If present, this value is treated as a predicate. - That is, it will filter based on stash_ids with the matching endpoint + That is, it will filter based on stash_id with the matching endpoint """ endpoint: String stash_id: String modifier: CriterionModifier! } +input StashIDsCriterionInput { + """ + If present, this value is treated as a predicate. + That is, it will filter based on stash_ids with the matching endpoint + """ + endpoint: String + stash_ids: [String] + modifier: CriterionModifier! +} + input CustomFieldCriterionInput { field: String! value: [Any!] @@ -156,6 +166,9 @@ input PerformerFilterType { o_counter: IntCriterionInput "Filter by StashID" stash_id_endpoint: StashIDCriterionInput + @deprecated(reason: "use stash_ids_endpoint instead") + "Filter by StashIDs" + stash_ids_endpoint: StashIDsCriterionInput # rating expressed as 1-100 rating100: IntCriterionInput "Filter by url" @@ -292,6 +305,11 @@ input SceneFilterType { performer_count: IntCriterionInput "Filter by StashID" stash_id_endpoint: StashIDCriterionInput + @deprecated(reason: "use stash_ids_endpoint instead") + "Filter by StashIDs" + stash_ids_endpoint: StashIDsCriterionInput + "Filter by StashID count" + stash_id_count: IntCriterionInput "Filter by url" url: StringCriterionInput "Filter by interactive" @@ -330,6 +348,8 @@ input SceneFilterType { groups_filter: GroupFilterType "Filter by related markers that meet this criteria" markers_filter: SceneMarkerFilterType + "Filter by related files that meet this criteria" + files_filter: FileFilterType } input MovieFilterType { @@ -401,6 +421,8 @@ input GroupFilterType { created_at: TimestampCriterionInput "Filter by last update time" updated_at: TimestampCriterionInput + "Filter by o-counter" + o_counter: IntCriterionInput "Filter by containing groups" containing_groups: HierarchicalMultiCriterionInput @@ -428,6 +450,9 @@ input StudioFilterType { parents: MultiCriterionInput "Filter by StashID" stash_id_endpoint: StashIDCriterionInput + @deprecated(reason: "use stash_ids_endpoint instead") + "Filter by StashIDs" + stash_ids_endpoint: StashIDsCriterionInput "Filter to only include studios with these tags" tags: HierarchicalMultiCriterionInput "Filter to only include studios missing this property" @@ -462,6 +487,8 @@ input StudioFilterType { created_at: TimestampCriterionInput "Filter by last update time" updated_at: TimestampCriterionInput + + custom_fields: [CustomFieldCriterionInput!] } input GalleryFilterType { @@ -534,6 +561,10 @@ input GalleryFilterType { studios_filter: StudioFilterType "Filter by related tags that meet this criteria" tags_filter: TagFilterType + "Filter by related files that meet this criteria" + files_filter: FileFilterType + "Filter by related folders that meet this criteria" + folders_filter: FolderFilterType } input TagFilterType { @@ -598,6 +629,13 @@ input TagFilterType { "Filter by autotag ignore value" ignore_auto_tag: Boolean + "Filter by StashID" + stash_id_endpoint: StashIDCriterionInput + @deprecated(reason: "use stash_ids_endpoint instead") + + "Filter by StashID" + stash_ids_endpoint: StashIDsCriterionInput + "Filter by related scenes that meet this criteria" scenes_filter: SceneFilterType "Filter by related images that meet this criteria" @@ -626,6 +664,8 @@ input ImageFilterType { id: IntCriterionInput "Filter by file checksum" checksum: StringCriterionInput + "Filter by file phash distance" + phash_distance: PhashDistanceCriterionInput "Filter by path" path: StringCriterionInput "Filter by file count" @@ -681,6 +721,8 @@ input ImageFilterType { studios_filter: StudioFilterType "Filter by related tags that meet this criteria" tags_filter: TagFilterType + "Filter by related files that meet this criteria" + files_filter: FileFilterType } input FileFilterType { diff --git a/graphql/schema/types/gallery.graphql b/graphql/schema/types/gallery.graphql index 999a743f7..f456157a7 100644 --- a/graphql/schema/types/gallery.graphql +++ b/graphql/schema/types/gallery.graphql @@ -100,6 +100,8 @@ input GalleryDestroyInput { """ delete_file: Boolean delete_generated: Boolean + "If true, delete the file entry from the database if the file is not assigned to any other objects" + destroy_file_entry: Boolean } type FindGalleriesResultType { diff --git a/graphql/schema/types/group.graphql b/graphql/schema/types/group.graphql index 35fc17a68..a46932054 100644 --- a/graphql/schema/types/group.graphql +++ b/graphql/schema/types/group.graphql @@ -30,6 +30,7 @@ type Group { performer_count(depth: Int): Int! # Resolver sub_group_count(depth: Int): Int! # Resolver scenes: [Scene!]! + o_counter: Int # Resolver } input GroupDescriptionInput { diff --git a/graphql/schema/types/image.graphql b/graphql/schema/types/image.graphql index fb95556f5..b7ec1a9f5 100644 --- a/graphql/schema/types/image.graphql +++ b/graphql/schema/types/image.graphql @@ -82,12 +82,16 @@ input ImageDestroyInput { id: ID! delete_file: Boolean delete_generated: Boolean + "If true, delete the file entry from the database if the file is not assigned to any other objects" + destroy_file_entry: Boolean } input ImagesDestroyInput { ids: [ID!]! delete_file: Boolean delete_generated: Boolean + "If true, delete the file entry from the database if the file is not assigned to any other objects" + destroy_file_entry: Boolean } type FindImagesResultType { diff --git a/graphql/schema/types/metadata.graphql b/graphql/schema/types/metadata.graphql index 923c25b4c..3d004ccb3 100644 --- a/graphql/schema/types/metadata.graphql +++ b/graphql/schema/types/metadata.graphql @@ -10,8 +10,11 @@ input GenerateMetadataInput { transcodes: Boolean "Generate transcodes even if not required" forceTranscodes: Boolean + "Generate video phashes during scan" phashes: Boolean interactiveHeatmapsSpeeds: Boolean + "Generate image phashes during scan" + imagePhashes: Boolean imageThumbnails: Boolean clipPreviews: Boolean @@ -19,6 +22,10 @@ input GenerateMetadataInput { sceneIDs: [ID!] "marker ids to generate for" markerIDs: [ID!] + "image ids to generate for" + imageIDs: [ID!] + "gallery ids to generate for" + galleryIDs: [ID!] "overwrite existing media" overwrite: Boolean @@ -85,8 +92,10 @@ input ScanMetadataInput { scanGenerateImagePreviews: Boolean "Generate sprites during scan" scanGenerateSprites: Boolean - "Generate phashes during scan" + "Generate video phashes during scan" scanGeneratePhashes: Boolean + "Generate image phashes during scan" + scanGenerateImagePhashes: Boolean "Generate image thumbnails during scan" scanGenerateThumbnails: Boolean "Generate image clip previews during scan" @@ -107,8 +116,10 @@ type ScanMetadataOptions { scanGenerateImagePreviews: Boolean! "Generate sprites during scan" scanGenerateSprites: Boolean! - "Generate phashes during scan" + "Generate video phashes during scan" scanGeneratePhashes: Boolean! + "Generate image phashes during scan" + scanGenerateImagePhashes: Boolean "Generate image thumbnails during scan" scanGenerateThumbnails: Boolean! "Generate image clip previews during scan" @@ -344,4 +355,6 @@ input CustomFieldsInput { full: Map "If populated, only the keys in this map will be updated" partial: Map + "Remove any keys in this list" + remove: [String!] } diff --git a/graphql/schema/types/performer.graphql b/graphql/schema/types/performer.graphql index fbb67ce8f..7275d4495 100644 --- a/graphql/schema/types/performer.graphql +++ b/graphql/schema/types/performer.graphql @@ -80,6 +80,7 @@ input PerformerCreateInput { career_length: String tattoos: String piercings: String + "Duplicate aliases and those equal to name will be ignored (case-insensitive)" alias_list: [String!] twitter: String @deprecated(reason: "Use urls") instagram: String @deprecated(reason: "Use urls") @@ -118,6 +119,7 @@ input PerformerUpdateInput { career_length: String tattoos: String piercings: String + "Duplicate aliases and those equal to name will be ignored (case-insensitive)" alias_list: [String!] twitter: String @deprecated(reason: "Use urls") instagram: String @deprecated(reason: "Use urls") @@ -161,6 +163,7 @@ input BulkPerformerUpdateInput { career_length: String tattoos: String piercings: String + "Duplicate aliases and those equal to name will result in an error (case-insensitive)" alias_list: BulkUpdateStrings twitter: String @deprecated(reason: "Use urls") instagram: String @deprecated(reason: "Use urls") @@ -185,3 +188,10 @@ type FindPerformersResultType { count: Int! performers: [Performer!]! } + +input PerformerMergeInput { + source: [ID!]! + destination: ID! + # values defined here will override values in the destination + values: PerformerUpdateInput +} diff --git a/graphql/schema/types/scene-marker.graphql b/graphql/schema/types/scene-marker.graphql index 6d1441213..9312c5aa3 100644 --- a/graphql/schema/types/scene-marker.graphql +++ b/graphql/schema/types/scene-marker.graphql @@ -42,6 +42,13 @@ input SceneMarkerUpdateInput { tag_ids: [ID!] } +input BulkSceneMarkerUpdateInput { + ids: [ID!] + title: String + primary_tag_id: ID + tag_ids: BulkUpdateIds +} + type FindSceneMarkersResultType { count: Int! scene_markers: [SceneMarker!]! diff --git a/graphql/schema/types/scene.graphql b/graphql/schema/types/scene.graphql index eca01d15e..5fba3819d 100644 --- a/graphql/schema/types/scene.graphql +++ b/graphql/schema/types/scene.graphql @@ -196,12 +196,16 @@ input SceneDestroyInput { id: ID! delete_file: Boolean delete_generated: Boolean + "If true, delete the file entry from the database if the file is not assigned to any other objects" + destroy_file_entry: Boolean } input ScenesDestroyInput { ids: [ID!]! delete_file: Boolean delete_generated: Boolean + "If true, delete the file entry from the database if the file is not assigned to any other objects" + destroy_file_entry: Boolean } type FindScenesResultType { diff --git a/graphql/schema/types/scraper.graphql b/graphql/schema/types/scraper.graphql index 8d430be5f..9c0e33fdf 100644 --- a/graphql/schema/types/scraper.graphql +++ b/graphql/schema/types/scraper.graphql @@ -55,9 +55,14 @@ type ScrapedStudio { "Set if studio matched" stored_id: ID name: String! - url: String + url: String @deprecated(reason: "use urls") + urls: [String!] parent: ScrapedStudio image: String + details: String + "Aliases must be comma-delimited to be parsed correctly" + aliases: String + tags: [ScrapedTag!] remote_site_id: String } @@ -66,6 +71,8 @@ type ScrapedTag { "Set if tag matched" stored_id: ID name: String! + "Remote site ID, if applicable" + remote_site_id: String } type ScrapedScene { @@ -191,6 +198,13 @@ input ScrapeSingleStudioInput { query: String } +input ScrapeSingleTagInput { + """ + Query can be either a name or a Stash ID + """ + query: String +} + input ScrapeSinglePerformerInput { "Instructs to query by string" query: String @@ -274,7 +288,10 @@ type StashBoxFingerprint { duration: Int! } -"If neither ids nor names are set, tag all items" +""" +Accepts either ids, or a combination of names and stash_ids. +If none are set, then all existing items will be tagged. +""" input StashBoxBatchTagInput { "Stash endpoint to use for the tagging" endpoint: Int @deprecated(reason: "use stash_box_endpoint") @@ -286,12 +303,17 @@ input StashBoxBatchTagInput { refresh: Boolean! "If batch adding studios, should their parent studios also be created?" createParent: Boolean! - "If set, only tag these ids" + """ + IDs in stash of the items to update. + If set, names and stash_ids fields will be ignored. + """ ids: [ID!] - "If set, only tag these names" + "Names of the items in the stash-box instance to search for and create" names: [String!] - "If set, only tag these performer ids" + "Stash IDs of the items in the stash-box instance to search for and create" + stash_ids: [String!] + "IDs in stash of the performers to update" performer_ids: [ID!] @deprecated(reason: "use ids") - "If set, only tag these performer names" + "Names of the performers in the stash-box instance to search for and create" performer_names: [String!] @deprecated(reason: "use names") } diff --git a/graphql/schema/types/studio.graphql b/graphql/schema/types/studio.graphql index 7823bf0c4..3e991ce96 100644 --- a/graphql/schema/types/studio.graphql +++ b/graphql/schema/types/studio.graphql @@ -1,7 +1,8 @@ type Studio { id: ID! name: String! - url: String + url: String @deprecated(reason: "Use urls") + urls: [String!]! parent_studio: Studio child_studios: [Studio!]! aliases: [String!]! @@ -24,11 +25,15 @@ type Studio { updated_at: Time! groups: [Group!]! movies: [Movie!]! @deprecated(reason: "use groups instead") + o_counter: Int + + custom_fields: Map! } input StudioCreateInput { name: String! - url: String + url: String @deprecated(reason: "Use urls") + urls: [String!] parent_id: ID "This should be a URL or a base64 encoded data URL" image: String @@ -37,15 +42,19 @@ input StudioCreateInput { rating100: Int favorite: Boolean details: String + "Duplicate aliases and those equal to name will be ignored (case-insensitive)" aliases: [String!] tag_ids: [ID!] ignore_auto_tag: Boolean + + custom_fields: Map } input StudioUpdateInput { id: ID! name: String - url: String + url: String @deprecated(reason: "Use urls") + urls: [String!] parent_id: ID "This should be a URL or a base64 encoded data URL" image: String @@ -54,9 +63,25 @@ input StudioUpdateInput { rating100: Int favorite: Boolean details: String + "Duplicate aliases and those equal to name will be ignored (case-insensitive)" aliases: [String!] tag_ids: [ID!] ignore_auto_tag: Boolean + + custom_fields: CustomFieldsInput +} + +input BulkStudioUpdateInput { + ids: [ID!]! + url: String @deprecated(reason: "Use urls") + urls: BulkUpdateStrings + parent_id: ID + # rating expressed as 1-100 + rating100: Int + favorite: Boolean + details: String + tag_ids: BulkUpdateIds + ignore_auto_tag: Boolean } input StudioDestroyInput { diff --git a/graphql/schema/types/tag.graphql b/graphql/schema/types/tag.graphql index 504f23e3d..a69b83548 100644 --- a/graphql/schema/types/tag.graphql +++ b/graphql/schema/types/tag.graphql @@ -9,6 +9,7 @@ type Tag { created_at: Time! updated_at: Time! favorite: Boolean! + stash_ids: [StashID!]! image_path: String # Resolver scene_count(depth: Int): Int! # Resolver scene_marker_count(depth: Int): Int! # Resolver @@ -30,11 +31,13 @@ input TagCreateInput { "Value that does not appear in the UI but overrides name for sorting" sort_name: String description: String + "Duplicate aliases and those equal to name will be ignored (case-insensitive)" aliases: [String!] ignore_auto_tag: Boolean favorite: Boolean "This should be a URL or a base64 encoded data URL" image: String + stash_ids: [StashIDInput!] parent_ids: [ID!] child_ids: [ID!] @@ -46,11 +49,13 @@ input TagUpdateInput { "Value that does not appear in the UI but overrides name for sorting" sort_name: String description: String + "Duplicate aliases and those equal to name will be ignored (case-insensitive)" aliases: [String!] ignore_auto_tag: Boolean favorite: Boolean "This should be a URL or a base64 encoded data URL" image: String + stash_ids: [StashIDInput!] parent_ids: [ID!] child_ids: [ID!] @@ -73,6 +78,7 @@ input TagsMergeInput { input BulkTagUpdateInput { ids: [ID!] description: String + "Duplicate aliases and those equal to name will result in an error (case-insensitive)" aliases: BulkUpdateStrings ignore_auto_tag: Boolean favorite: Boolean diff --git a/graphql/stash-box/query.graphql b/graphql/stash-box/query.graphql index f7528e728..2367e85cf 100644 --- a/graphql/stash-box/query.graphql +++ b/graphql/stash-box/query.graphql @@ -13,6 +13,7 @@ fragment ImageFragment on Image { fragment StudioFragment on Studio { name id + aliases urls { ...URLFragment } @@ -169,6 +170,21 @@ query FindStudio($id: ID, $name: String) { } } +query FindTag($id: ID, $name: String) { + findTag(id: $id, name: $name) { + ...TagFragment + } +} + +query QueryTags($input: TagQueryInput!) { + queryTags(input: $input) { + count + tags { + ...TagFragment + } + } +} + mutation SubmitFingerprint($input: FingerprintSubmission!) { submitFingerprint(input: $input) } diff --git a/internal/api/changeset_translator.go b/internal/api/changeset_translator.go index 5c81c12cb..45285bdde 100644 --- a/internal/api/changeset_translator.go +++ b/internal/api/changeset_translator.go @@ -98,7 +98,7 @@ func (t changesetTranslator) string(value *string) string { return "" } - return *value + return strings.TrimSpace(*value) } func (t changesetTranslator) optionalString(value *string, field string) models.OptionalString { @@ -106,7 +106,12 @@ func (t changesetTranslator) optionalString(value *string, field string) models. return models.OptionalString{} } - return models.NewOptionalStringPtr(value) + if value == nil { + return models.NewOptionalStringPtr(nil) + } + + trimmed := strings.TrimSpace(*value) + return models.NewOptionalString(trimmed) } func (t changesetTranslator) optionalDate(value *string, field string) (models.OptionalDate, error) { @@ -318,8 +323,14 @@ func (t changesetTranslator) updateStrings(value []string, field string) *models return nil } + // Trim whitespace from each string + trimmedValues := make([]string, len(value)) + for i, v := range value { + trimmedValues[i] = strings.TrimSpace(v) + } + return &models.UpdateStrings{ - Values: value, + Values: trimmedValues, Mode: models.RelationshipUpdateModeSet, } } @@ -329,8 +340,14 @@ func (t changesetTranslator) updateStringsBulk(value *BulkUpdateStrings, field s return nil } + // Trim whitespace from each string + trimmedValues := make([]string, len(value.Values)) + for i, v := range value.Values { + trimmedValues[i] = strings.TrimSpace(v) + } + return &models.UpdateStrings{ - Values: value.Values, + Values: trimmedValues, Mode: value.Mode, } } @@ -448,7 +465,7 @@ func groupsDescriptionsFromGroupInput(input []*GroupDescriptionInput) ([]models. GroupID: gID, } if v.Description != nil { - ret[i].Description = *v.Description + ret[i].Description = strings.TrimSpace(*v.Description) } } diff --git a/internal/api/check_version.go b/internal/api/check_version.go index 6279997d7..f4c2950f1 100644 --- a/internal/api/check_version.go +++ b/internal/api/check_version.go @@ -7,8 +7,10 @@ import ( "fmt" "io" "net/http" + "os" "regexp" "runtime" + "strings" "time" "golang.org/x/sys/cpu" @@ -36,6 +38,24 @@ var stashReleases = func() map[string]string { } } +// isMacOSBundle checks if the application is running from within a macOS .app bundle +func isMacOSBundle() bool { + exec, err := os.Executable() + return err == nil && strings.Contains(exec, "Stash.app/") +} + +// getWantedRelease determines which release variant to download based on platform and bundle type +func getWantedRelease(platform string) string { + release := stashReleases()[platform] + + // On macOS, check if running from .app bundle + if runtime.GOOS == "darwin" && isMacOSBundle() { + return "Stash.app.zip" + } + + return release +} + type githubReleasesResponse struct { Url string Assets_url string @@ -168,7 +188,7 @@ func GetLatestRelease(ctx context.Context) (*LatestRelease, error) { } platform := fmt.Sprintf("%s/%s", runtime.GOOS, arch) - wantedRelease := stashReleases()[platform] + wantedRelease := getWantedRelease(platform) url := apiReleases if build.IsDevelop() { diff --git a/internal/api/custom_fields.go b/internal/api/custom_fields.go new file mode 100644 index 000000000..5eaa6f67a --- /dev/null +++ b/internal/api/custom_fields.go @@ -0,0 +1,12 @@ +package api + +import "github.com/stashapp/stash/pkg/models" + +func handleUpdateCustomFields(input models.CustomFieldsInput) models.CustomFieldsInput { + ret := input + // convert json.Numbers to int/float + ret.Full = convertMapJSONNumbers(ret.Full) + ret.Partial = convertMapJSONNumbers(ret.Partial) + + return ret +} diff --git a/internal/api/images.go b/internal/api/images.go index 89a8e87b0..e0f11416a 100644 --- a/internal/api/images.go +++ b/internal/api/images.go @@ -26,6 +26,7 @@ var imageBoxExts = []string{ ".gif", ".svg", ".webp", + ".avif", } func newImageBox(box fs.FS) (*imageBox, error) { @@ -101,7 +102,7 @@ func initCustomPerformerImages(customPath string) { } } -func getDefaultPerformerImage(name string, gender *models.GenderEnum) []byte { +func getDefaultPerformerImage(name string, gender *models.GenderEnum, sfwMode bool) []byte { // try the custom box first if we have one if performerBoxCustom != nil { ret, err := performerBoxCustom.GetRandomImageByName(name) @@ -111,6 +112,10 @@ func getDefaultPerformerImage(name string, gender *models.GenderEnum) []byte { logger.Warnf("error loading custom default performer image: %v", err) } + if sfwMode { + return static.ReadAll(static.DefaultSFWPerformerImage) + } + var g models.GenderEnum if gender != nil { g = *gender diff --git a/internal/api/input.go b/internal/api/input.go new file mode 100644 index 000000000..1a720e965 --- /dev/null +++ b/internal/api/input.go @@ -0,0 +1,35 @@ +package api + +import ( + "fmt" + + "github.com/stashapp/stash/pkg/sliceutil/stringslice" +) + +// TODO - apply handleIDs to other resolvers that accept ID lists + +// handleIDList validates and converts a list of string IDs to integers +func handleIDList(idList []string, field string) ([]int, error) { + if err := validateIDList(idList); err != nil { + return nil, fmt.Errorf("validating %s: %w", field, err) + } + + ids, err := stringslice.StringSliceToIntSlice(idList) + if err != nil { + return nil, fmt.Errorf("converting %s: %w", field, err) + } + + return ids, nil +} + +// validateIDList returns an error if there are any duplicate ids in the list +func validateIDList(ids []string) error { + seen := make(map[string]struct{}) + for _, id := range ids { + if _, exists := seen[id]; exists { + return fmt.Errorf("duplicate id found: %s", id) + } + seen[id] = struct{}{} + } + return nil +} diff --git a/internal/api/loaders/dataloaders.go b/internal/api/loaders/dataloaders.go index 38f72b0a1..4676966c9 100644 --- a/internal/api/loaders/dataloaders.go +++ b/internal/api/loaders/dataloaders.go @@ -59,7 +59,9 @@ type Loaders struct { PerformerByID *PerformerLoader PerformerCustomFields *CustomFieldsLoader - StudioByID *StudioLoader + StudioByID *StudioLoader + StudioCustomFields *CustomFieldsLoader + TagByID *TagLoader GroupByID *GroupLoader FileByID *FileLoader @@ -99,6 +101,11 @@ func (m Middleware) Middleware(next http.Handler) http.Handler { maxBatch: maxBatch, fetch: m.fetchPerformerCustomFields(ctx), }, + StudioCustomFields: &CustomFieldsLoader{ + wait: wait, + maxBatch: maxBatch, + fetch: m.fetchStudioCustomFields(ctx), + }, StudioByID: &StudioLoader{ wait: wait, maxBatch: maxBatch, @@ -253,6 +260,18 @@ func (m Middleware) fetchStudios(ctx context.Context) func(keys []int) ([]*model } } +func (m Middleware) fetchStudioCustomFields(ctx context.Context) func(keys []int) ([]models.CustomFieldMap, []error) { + return func(keys []int) (ret []models.CustomFieldMap, errs []error) { + err := m.Repository.WithDB(ctx, func(ctx context.Context) error { + var err error + ret, err = m.Repository.Studio.GetCustomFieldsBulk(ctx, keys) + return err + }) + + return ret, toErrorSlice(err) + } +} + func (m Middleware) fetchTags(ctx context.Context) func(keys []int) ([]*models.Tag, []error) { return func(keys []int) (ret []*models.Tag, errs []error) { err := m.Repository.WithDB(ctx, func(ctx context.Context) error { diff --git a/internal/api/resolver_model_movie.go b/internal/api/resolver_model_movie.go index e3fba57c0..317123c6e 100644 --- a/internal/api/resolver_model_movie.go +++ b/internal/api/resolver_model_movie.go @@ -204,3 +204,14 @@ func (r *groupResolver) Scenes(ctx context.Context, obj *models.Group) (ret []*m return ret, nil } + +func (r *groupResolver) OCounter(ctx context.Context, obj *models.Group) (ret *int, err error) { + var count int + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + count, err = r.repository.Scene.OCountByGroupID(ctx, obj.ID) + return err + }); err != nil { + return nil, err + } + return &count, nil +} diff --git a/internal/api/resolver_model_studio.go b/internal/api/resolver_model_studio.go index 2111039c8..b54455920 100644 --- a/internal/api/resolver_model_studio.go +++ b/internal/api/resolver_model_studio.go @@ -40,6 +40,35 @@ func (r *studioResolver) Aliases(ctx context.Context, obj *models.Studio) ([]str return obj.Aliases.List(), nil } +func (r *studioResolver) URL(ctx context.Context, obj *models.Studio) (*string, error) { + if !obj.URLs.Loaded() { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + return obj.LoadURLs(ctx, r.repository.Studio) + }); err != nil { + return nil, err + } + } + + urls := obj.URLs.List() + if len(urls) == 0 { + return nil, nil + } + + return &urls[0], nil +} + +func (r *studioResolver) Urls(ctx context.Context, obj *models.Studio) ([]string, error) { + if !obj.URLs.Loaded() { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + return obj.LoadURLs(ctx, r.repository.Studio) + }); err != nil { + return nil, err + } + } + + return obj.URLs.List(), nil +} + func (r *studioResolver) Tags(ctx context.Context, obj *models.Studio) (ret []*models.Tag, err error) { if !obj.TagIDs.Loaded() { if err := r.withReadTxn(ctx, func(ctx context.Context) error { @@ -114,6 +143,24 @@ func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, dep return r.GroupCount(ctx, obj, depth) } +func (r *studioResolver) OCounter(ctx context.Context, obj *models.Studio) (ret *int, err error) { + var res_scene int + var res_image int + var res int + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + res_scene, err = r.repository.Scene.OCountByStudioID(ctx, obj.ID) + if err != nil { + return err + } + res_image, err = r.repository.Image.OCountByStudioID(ctx, obj.ID) + return err + }); err != nil { + return nil, err + } + res = res_scene + res_image + return &res, nil +} + func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) { if obj.ParentID == nil { return nil, nil @@ -160,6 +207,19 @@ func (r *studioResolver) Groups(ctx context.Context, obj *models.Studio) (ret [] return ret, nil } +func (r *studioResolver) CustomFields(ctx context.Context, obj *models.Studio) (map[string]interface{}, error) { + m, err := loaders.From(ctx).StudioCustomFields.Load(obj.ID) + if err != nil { + return nil, err + } + + if m == nil { + return make(map[string]interface{}), nil + } + + return m, nil +} + // deprecated func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Group, err error) { return r.Groups(ctx, obj) diff --git a/internal/api/resolver_model_tag.go b/internal/api/resolver_model_tag.go index 14237d2fe..deae41f21 100644 --- a/internal/api/resolver_model_tag.go +++ b/internal/api/resolver_model_tag.go @@ -54,6 +54,16 @@ func (r *tagResolver) Aliases(ctx context.Context, obj *models.Tag) (ret []strin return obj.Aliases.List(), nil } +func (r *tagResolver) StashIds(ctx context.Context, obj *models.Tag) ([]*models.StashID, error) { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + return obj.LoadStashIDs(ctx, r.repository.Tag) + }); err != nil { + return nil, err + } + + return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil +} + func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) { if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = scene.CountByTagID(ctx, r.repository.Scene, obj.ID, depth) diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index d9c71b09f..23b61c208 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -150,6 +150,15 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen c.SetString(config.BackupDirectoryPath, *input.BackupDirectoryPath) } + existingDeleteTrashPath := c.GetDeleteTrashPath() + if input.DeleteTrashPath != nil && existingDeleteTrashPath != *input.DeleteTrashPath { + if err := validateDir(config.DeleteTrashPath, *input.DeleteTrashPath, true); err != nil { + return makeConfigGeneralResult(), err + } + + c.SetString(config.DeleteTrashPath, *input.DeleteTrashPath) + } + existingGeneratedPath := c.GetGeneratedPath() if input.GeneratedPath != nil && existingGeneratedPath != *input.GeneratedPath { if err := validateDir(config.Generated, *input.GeneratedPath, false); err != nil { @@ -334,6 +343,10 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen logger.SetLogLevel(*input.LogLevel) } + if input.LogFileMaxSize != nil && *input.LogFileMaxSize != c.GetLogFileMaxSize() { + c.SetInt(config.LogFileMaxSize, *input.LogFileMaxSize) + } + if input.Excludes != nil { for _, r := range input.Excludes { _, err := regexp.Compile(r) @@ -445,6 +458,8 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigInterfaceInput) (*ConfigInterfaceResult, error) { c := config.GetInstance() + r.setConfigBool(config.SFWContentMode, input.SfwContentMode) + if input.MenuItems != nil { c.SetInterface(config.MenuItems, input.MenuItems) } @@ -478,6 +493,8 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI r.setConfigString(config.ImageLightboxScrollModeKey, (*string)(options.ScrollMode)) r.setConfigInt(config.ImageLightboxScrollAttemptsBeforeChange, options.ScrollAttemptsBeforeChange) + + r.setConfigBool(config.ImageLightboxDisableAnimation, options.DisableAnimation) } if input.CSS != nil { @@ -498,12 +515,15 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI r.setConfigBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled) + r.setConfigBool(config.DisableCustomizations, input.DisableCustomizations) + if input.DisableDropdownCreate != nil { ddc := input.DisableDropdownCreate r.setConfigBool(config.DisableDropdownCreatePerformer, ddc.Performer) r.setConfigBool(config.DisableDropdownCreateStudio, ddc.Studio) r.setConfigBool(config.DisableDropdownCreateTag, ddc.Tag) r.setConfigBool(config.DisableDropdownCreateMovie, ddc.Movie) + r.setConfigBool(config.DisableDropdownCreateGallery, ddc.Gallery) } r.setConfigString(config.HandyKey, input.HandyKey) diff --git a/internal/api/resolver_mutation_file.go b/internal/api/resolver_mutation_file.go index c303446e1..afbefe554 100644 --- a/internal/api/resolver_mutation_file.go +++ b/internal/api/resolver_mutation_file.go @@ -149,7 +149,9 @@ func (r *mutationResolver) DeleteFiles(ctx context.Context, ids []string) (ret b return false, fmt.Errorf("converting ids: %w", err) } - fileDeleter := file.NewDeleter() + trashPath := manager.GetInstance().Config.GetDeleteTrashPath() + + fileDeleter := file.NewDeleterWithTrash(trashPath) destroyer := &file.ZipDestroyer{ FileDestroyer: r.repository.File, FolderDestroyer: r.repository.Folder, @@ -208,6 +210,58 @@ func (r *mutationResolver) DeleteFiles(ctx context.Context, ids []string) (ret b return true, nil } +func (r *mutationResolver) DestroyFiles(ctx context.Context, ids []string) (ret bool, err error) { + fileIDs, err := stringslice.StringSliceToIntSlice(ids) + if err != nil { + return false, fmt.Errorf("converting ids: %w", err) + } + + destroyer := &file.ZipDestroyer{ + FileDestroyer: r.repository.File, + FolderDestroyer: r.repository.Folder, + } + + if err := r.withTxn(ctx, func(ctx context.Context) error { + qb := r.repository.File + + for _, fileIDInt := range fileIDs { + fileID := models.FileID(fileIDInt) + f, err := qb.Find(ctx, fileID) + if err != nil { + return err + } + + if len(f) == 0 { + return fmt.Errorf("file with id %d not found", fileID) + } + + path := f[0].Base().Path + + // ensure not a primary file + isPrimary, err := qb.IsPrimary(ctx, fileID) + if err != nil { + return fmt.Errorf("checking if file %s is primary: %w", path, err) + } + + if isPrimary { + return fmt.Errorf("cannot destroy primary file entry %s", path) + } + + // destroy DB entries only (no filesystem deletion) + const deleteFile = false + if err := destroyer.DestroyZip(ctx, f[0], nil, deleteFile); err != nil { + return fmt.Errorf("destroying file entry %s: %w", path, err) + } + } + + return nil + }); err != nil { + return false, err + } + + return true, nil +} + func (r *mutationResolver) FileSetFingerprints(ctx context.Context, input FileSetFingerprintsInput) (bool, error) { fileIDInt, err := strconv.Atoi(input.ID) if err != nil { diff --git a/internal/api/resolver_mutation_gallery.go b/internal/api/resolver_mutation_gallery.go index 5d5cd4b37..e7f853922 100644 --- a/internal/api/resolver_mutation_gallery.go +++ b/internal/api/resolver_mutation_gallery.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "strconv" + "strings" "github.com/stashapp/stash/internal/manager" "github.com/stashapp/stash/pkg/file" @@ -43,11 +44,12 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat // Populate a new gallery from the input newGallery := models.NewGallery() - newGallery.Title = input.Title + newGallery.Title = strings.TrimSpace(input.Title) newGallery.Code = translator.string(input.Code) newGallery.Details = translator.string(input.Details) newGallery.Photographer = translator.string(input.Photographer) newGallery.Rating = input.Rating100 + newGallery.Organized = translator.bool(input.Organized) var err error @@ -74,9 +76,9 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat } if input.Urls != nil { - newGallery.URLs = models.NewRelatedStrings(input.Urls) + newGallery.URLs = models.NewRelatedStrings(stringslice.TrimSpace(input.Urls)) } else if input.URL != nil { - newGallery.URLs = models.NewRelatedStrings([]string{*input.URL}) + newGallery.URLs = models.NewRelatedStrings([]string{strings.TrimSpace(*input.URL)}) } // Start the transaction and save the gallery @@ -333,15 +335,18 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall return false, fmt.Errorf("converting ids: %w", err) } + trashPath := manager.GetInstance().Config.GetDeleteTrashPath() + var galleries []*models.Gallery var imgsDestroyed []*models.Image fileDeleter := &image.FileDeleter{ - Deleter: file.NewDeleter(), + Deleter: file.NewDeleterWithTrash(trashPath), Paths: manager.GetInstance().Paths, } deleteGenerated := utils.IsTrue(input.DeleteGenerated) deleteFile := utils.IsTrue(input.DeleteFile) + destroyFileEntry := utils.IsTrue(input.DestroyFileEntry) if err := r.withTxn(ctx, func(ctx context.Context) error { qb := r.repository.Gallery @@ -362,7 +367,7 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall galleries = append(galleries, gallery) - imgsDestroyed, err = r.galleryService.Destroy(ctx, gallery, fileDeleter, deleteGenerated, deleteFile) + imgsDestroyed, err = r.galleryService.Destroy(ctx, gallery, fileDeleter, deleteGenerated, deleteFile, destroyFileEntry) if err != nil { return err } diff --git a/internal/api/resolver_mutation_group.go b/internal/api/resolver_mutation_group.go index d75994d14..14dc817b9 100644 --- a/internal/api/resolver_mutation_group.go +++ b/internal/api/resolver_mutation_group.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strconv" + "strings" "github.com/stashapp/stash/internal/static" "github.com/stashapp/stash/pkg/group" @@ -21,7 +22,7 @@ func groupFromGroupCreateInput(ctx context.Context, input GroupCreateInput) (*mo // Populate a new group from the input newGroup := models.NewGroup() - newGroup.Name = input.Name + newGroup.Name = strings.TrimSpace(input.Name) newGroup.Aliases = translator.string(input.Aliases) newGroup.Duration = input.Duration newGroup.Rating = input.Rating100 @@ -55,7 +56,7 @@ func groupFromGroupCreateInput(ctx context.Context, input GroupCreateInput) (*mo } if input.Urls != nil { - newGroup.URLs = models.NewRelatedStrings(input.Urls) + newGroup.URLs = models.NewRelatedStrings(stringslice.TrimSpace(input.Urls)) } return &newGroup, nil diff --git a/internal/api/resolver_mutation_image.go b/internal/api/resolver_mutation_image.go index 721598634..230d48358 100644 --- a/internal/api/resolver_mutation_image.go +++ b/internal/api/resolver_mutation_image.go @@ -308,9 +308,11 @@ func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageD return false, fmt.Errorf("converting id: %w", err) } + trashPath := manager.GetInstance().Config.GetDeleteTrashPath() + var i *models.Image fileDeleter := &image.FileDeleter{ - Deleter: file.NewDeleter(), + Deleter: file.NewDeleterWithTrash(trashPath), Paths: manager.GetInstance().Paths, } if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -323,7 +325,7 @@ func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageD return fmt.Errorf("image with id %d not found", imageID) } - return r.imageService.Destroy(ctx, i, fileDeleter, utils.IsTrue(input.DeleteGenerated), utils.IsTrue(input.DeleteFile)) + return r.imageService.Destroy(ctx, i, fileDeleter, utils.IsTrue(input.DeleteGenerated), utils.IsTrue(input.DeleteFile), utils.IsTrue(input.DestroyFileEntry)) }); err != nil { fileDeleter.Rollback() return false, err @@ -348,9 +350,11 @@ func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.Image return false, fmt.Errorf("converting ids: %w", err) } + trashPath := manager.GetInstance().Config.GetDeleteTrashPath() + var images []*models.Image fileDeleter := &image.FileDeleter{ - Deleter: file.NewDeleter(), + Deleter: file.NewDeleterWithTrash(trashPath), Paths: manager.GetInstance().Paths, } if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -368,7 +372,7 @@ func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.Image images = append(images, i) - if err := r.imageService.Destroy(ctx, i, fileDeleter, utils.IsTrue(input.DeleteGenerated), utils.IsTrue(input.DeleteFile)); err != nil { + if err := r.imageService.Destroy(ctx, i, fileDeleter, utils.IsTrue(input.DeleteGenerated), utils.IsTrue(input.DeleteFile), utils.IsTrue(input.DestroyFileEntry)); err != nil { return err } } diff --git a/internal/api/resolver_mutation_movie.go b/internal/api/resolver_mutation_movie.go index 2e1011083..cb19e540f 100644 --- a/internal/api/resolver_mutation_movie.go +++ b/internal/api/resolver_mutation_movie.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strconv" + "strings" "github.com/stashapp/stash/internal/static" "github.com/stashapp/stash/pkg/models" @@ -32,7 +33,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp // Populate a new group from the input newGroup := models.NewGroup() - newGroup.Name = input.Name + newGroup.Name = strings.TrimSpace(input.Name) newGroup.Aliases = translator.string(input.Aliases) newGroup.Duration = input.Duration newGroup.Rating = input.Rating100 @@ -56,9 +57,9 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp } if input.Urls != nil { - newGroup.URLs = models.NewRelatedStrings(input.Urls) + newGroup.URLs = models.NewRelatedStrings(stringslice.TrimSpace(input.Urls)) } else if input.URL != nil { - newGroup.URLs = models.NewRelatedStrings([]string{*input.URL}) + newGroup.URLs = models.NewRelatedStrings([]string{strings.TrimSpace(*input.URL)}) } // Process the base 64 encoded image string diff --git a/internal/api/resolver_mutation_performer.go b/internal/api/resolver_mutation_performer.go index 47b02147d..fd18ecb95 100644 --- a/internal/api/resolver_mutation_performer.go +++ b/internal/api/resolver_mutation_performer.go @@ -2,12 +2,16 @@ package api import ( "context" + "errors" "fmt" + "slices" "strconv" + "strings" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/performer" "github.com/stashapp/stash/pkg/plugin/hook" + "github.com/stashapp/stash/pkg/sliceutil" "github.com/stashapp/stash/pkg/sliceutil/stringslice" "github.com/stashapp/stash/pkg/utils" ) @@ -37,9 +41,9 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per // Populate a new performer from the input newPerformer := models.NewPerformer() - newPerformer.Name = input.Name + newPerformer.Name = strings.TrimSpace(input.Name) newPerformer.Disambiguation = translator.string(input.Disambiguation) - newPerformer.Aliases = models.NewRelatedStrings(input.AliasList) + newPerformer.Aliases = models.NewRelatedStrings(stringslice.UniqueExcludeFold(stringslice.TrimSpace(input.AliasList), newPerformer.Name)) newPerformer.Gender = input.Gender newPerformer.Ethnicity = translator.string(input.Ethnicity) newPerformer.Country = translator.string(input.Country) @@ -62,17 +66,17 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per newPerformer.URLs = models.NewRelatedStrings([]string{}) if input.URL != nil { - newPerformer.URLs.Add(*input.URL) + newPerformer.URLs.Add(strings.TrimSpace(*input.URL)) } if input.Twitter != nil { - newPerformer.URLs.Add(utils.URLFromHandle(*input.Twitter, twitterURL)) + newPerformer.URLs.Add(utils.URLFromHandle(strings.TrimSpace(*input.Twitter), twitterURL)) } if input.Instagram != nil { - newPerformer.URLs.Add(utils.URLFromHandle(*input.Instagram, instagramURL)) + newPerformer.URLs.Add(utils.URLFromHandle(strings.TrimSpace(*input.Instagram), instagramURL)) } if input.Urls != nil { - newPerformer.URLs.Add(input.Urls...) + newPerformer.URLs.Add(stringslice.TrimSpace(input.Urls)...) } var err error @@ -135,7 +139,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per return r.getPerformer(ctx, newPerformer.ID) } -func (r *mutationResolver) validateNoLegacyURLs(translator changesetTranslator) error { +func validateNoLegacyURLs(translator changesetTranslator) error { // ensure url/twitter/instagram are not included in the input if translator.hasField("url") { return fmt.Errorf("url field must not be included if urls is included") @@ -150,7 +154,7 @@ func (r *mutationResolver) validateNoLegacyURLs(translator changesetTranslator) return nil } -func (r *mutationResolver) handleLegacyURLs(ctx context.Context, performerID int, legacyURL, legacyTwitter, legacyInstagram models.OptionalString, updatedPerformer *models.PerformerPartial) error { +func (r *mutationResolver) handleLegacyURLs(ctx context.Context, performerID int, legacyURLs legacyPerformerURLs, updatedPerformer *models.PerformerPartial) error { qb := r.repository.Performer // we need to be careful with URL/Twitter/Instagram @@ -169,23 +173,23 @@ func (r *mutationResolver) handleLegacyURLs(ctx context.Context, performerID int existingURLs := p.URLs.List() // performer partial URLs should be empty - if legacyURL.Set { + if legacyURLs.URL.Set { replaced := false for i, url := range existingURLs { if !performer.IsTwitterURL(url) && !performer.IsInstagramURL(url) { - existingURLs[i] = legacyURL.Value + existingURLs[i] = legacyURLs.URL.Value replaced = true break } } if !replaced { - existingURLs = append(existingURLs, legacyURL.Value) + existingURLs = append(existingURLs, legacyURLs.URL.Value) } } - if legacyTwitter.Set { - value := utils.URLFromHandle(legacyTwitter.Value, twitterURL) + if legacyURLs.Twitter.Set { + value := utils.URLFromHandle(legacyURLs.Twitter.Value, twitterURL) found := false // find and replace the first twitter URL for i, url := range existingURLs { @@ -200,9 +204,9 @@ func (r *mutationResolver) handleLegacyURLs(ctx context.Context, performerID int existingURLs = append(existingURLs, value) } } - if legacyInstagram.Set { + if legacyURLs.Instagram.Set { found := false - value := utils.URLFromHandle(legacyInstagram.Value, instagramURL) + value := utils.URLFromHandle(legacyURLs.Instagram.Value, instagramURL) // find and replace the first instagram URL for i, url := range existingURLs { if performer.IsInstagramURL(url) { @@ -225,16 +229,25 @@ func (r *mutationResolver) handleLegacyURLs(ctx context.Context, performerID int return nil } -func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.PerformerUpdateInput) (*models.Performer, error) { - performerID, err := strconv.Atoi(input.ID) - if err != nil { - return nil, fmt.Errorf("converting id: %w", err) - } +type legacyPerformerURLs struct { + URL models.OptionalString + Twitter models.OptionalString + Instagram models.OptionalString +} - translator := changesetTranslator{ - inputMap: getUpdateInputMap(ctx), - } +func (u *legacyPerformerURLs) AnySet() bool { + return u.URL.Set || u.Twitter.Set || u.Instagram.Set +} +func legacyPerformerURLsFromInput(input models.PerformerUpdateInput, translator changesetTranslator) legacyPerformerURLs { + return legacyPerformerURLs{ + URL: translator.optionalString(input.URL, "url"), + Twitter: translator.optionalString(input.Twitter, "twitter"), + Instagram: translator.optionalString(input.Instagram, "instagram"), + } +} + +func performerPartialFromInput(input models.PerformerUpdateInput, translator changesetTranslator) (*models.PerformerPartial, error) { // Populate performer from the input updatedPerformer := models.NewPerformerPartial() @@ -259,19 +272,17 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") updatedPerformer.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids") + var err error + if translator.hasField("urls") { // ensure url/twitter/instagram are not included in the input - if err := r.validateNoLegacyURLs(translator); err != nil { + if err := validateNoLegacyURLs(translator); err != nil { return nil, err } updatedPerformer.URLs = translator.updateStrings(input.Urls, "urls") } - legacyURL := translator.optionalString(input.URL, "url") - legacyTwitter := translator.optionalString(input.Twitter, "twitter") - legacyInstagram := translator.optionalString(input.Instagram, "instagram") - updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate") if err != nil { return nil, fmt.Errorf("converting birthdate: %w", err) @@ -296,10 +307,27 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per return nil, fmt.Errorf("converting tag ids: %w", err) } - updatedPerformer.CustomFields = input.CustomFields - // convert json.Numbers to int/float - updatedPerformer.CustomFields.Full = convertMapJSONNumbers(updatedPerformer.CustomFields.Full) - updatedPerformer.CustomFields.Partial = convertMapJSONNumbers(updatedPerformer.CustomFields.Partial) + updatedPerformer.CustomFields = handleUpdateCustomFields(input.CustomFields) + + return &updatedPerformer, nil +} + +func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.PerformerUpdateInput) (*models.Performer, error) { + performerID, err := strconv.Atoi(input.ID) + if err != nil { + return nil, fmt.Errorf("converting id: %w", err) + } + + translator := changesetTranslator{ + inputMap: getUpdateInputMap(ctx), + } + + updatedPerformer, err := performerPartialFromInput(input, translator) + if err != nil { + return nil, err + } + + legacyURLs := legacyPerformerURLsFromInput(input, translator) var imageData []byte imageIncluded := translator.hasField("image") @@ -314,17 +342,38 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per if err := r.withTxn(ctx, func(ctx context.Context) error { qb := r.repository.Performer - if legacyURL.Set || legacyTwitter.Set || legacyInstagram.Set { - if err := r.handleLegacyURLs(ctx, performerID, legacyURL, legacyTwitter, legacyInstagram, &updatedPerformer); err != nil { + if legacyURLs.AnySet() { + if err := r.handleLegacyURLs(ctx, performerID, legacyURLs, updatedPerformer); err != nil { return err } } - if err := performer.ValidateUpdate(ctx, performerID, updatedPerformer, qb); err != nil { + if updatedPerformer.Aliases != nil { + p, err := qb.Find(ctx, performerID) + if err != nil { + return err + } + if p != nil { + if err := p.LoadAliases(ctx, qb); err != nil { + return err + } + + effectiveAliases := updatedPerformer.Aliases.Apply(p.Aliases.List()) + name := p.Name + if updatedPerformer.Name.Set { + name = updatedPerformer.Name.Value + } + + sanitized := stringslice.UniqueExcludeFold(effectiveAliases, name) + updatedPerformer.Aliases.Values = sanitized + updatedPerformer.Aliases.Mode = models.RelationshipUpdateModeSet + } + } + if err := performer.ValidateUpdate(ctx, performerID, *updatedPerformer, qb); err != nil { return err } - _, err = qb.UpdatePartial(ctx, performerID, updatedPerformer) + _, err = qb.UpdatePartial(ctx, performerID, *updatedPerformer) if err != nil { return err } @@ -381,16 +430,18 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe if translator.hasField("urls") { // ensure url/twitter/instagram are not included in the input - if err := r.validateNoLegacyURLs(translator); err != nil { + if err := validateNoLegacyURLs(translator); err != nil { return nil, err } updatedPerformer.URLs = translator.updateStringsBulk(input.Urls, "urls") } - legacyURL := translator.optionalString(input.URL, "url") - legacyTwitter := translator.optionalString(input.Twitter, "twitter") - legacyInstagram := translator.optionalString(input.Instagram, "instagram") + legacyURLs := legacyPerformerURLs{ + URL: translator.optionalString(input.URL, "url"), + Twitter: translator.optionalString(input.Twitter, "twitter"), + Instagram: translator.optionalString(input.Instagram, "instagram"), + } updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate") if err != nil { @@ -416,6 +467,10 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe return nil, fmt.Errorf("converting tag ids: %w", err) } + if input.CustomFields != nil { + updatedPerformer.CustomFields = handleUpdateCustomFields(*input.CustomFields) + } + ret := []*models.Performer{} // Start the transaction and save the performers @@ -423,8 +478,8 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe qb := r.repository.Performer for _, performerID := range performerIDs { - if legacyURL.Set || legacyTwitter.Set || legacyInstagram.Set { - if err := r.handleLegacyURLs(ctx, performerID, legacyURL, legacyTwitter, legacyInstagram, &updatedPerformer); err != nil { + if legacyURLs.AnySet() { + if err := r.handleLegacyURLs(ctx, performerID, legacyURLs, &updatedPerformer); err != nil { return err } } @@ -504,3 +559,87 @@ func (r *mutationResolver) PerformersDestroy(ctx context.Context, performerIDs [ return true, nil } + +func (r *mutationResolver) PerformerMerge(ctx context.Context, input PerformerMergeInput) (*models.Performer, error) { + srcIDs, err := stringslice.StringSliceToIntSlice(input.Source) + if err != nil { + return nil, fmt.Errorf("converting source ids: %w", err) + } + + // ensure source ids are unique + srcIDs = sliceutil.AppendUniques(nil, srcIDs) + + destID, err := strconv.Atoi(input.Destination) + if err != nil { + return nil, fmt.Errorf("converting destination id: %w", err) + } + + // ensure destination is not in source list + if slices.Contains(srcIDs, destID) { + return nil, errors.New("destination performer cannot be in source list") + } + + var values *models.PerformerPartial + var imageData []byte + + if input.Values != nil { + translator := changesetTranslator{ + inputMap: getNamedUpdateInputMap(ctx, "input.values"), + } + + values, err = performerPartialFromInput(*input.Values, translator) + if err != nil { + return nil, err + } + legacyURLs := legacyPerformerURLsFromInput(*input.Values, translator) + if legacyURLs.AnySet() { + return nil, errors.New("Merging legacy performer URLs is not supported") + } + + if input.Values.Image != nil { + var err error + imageData, err = utils.ProcessImageInput(ctx, *input.Values.Image) + if err != nil { + return nil, fmt.Errorf("processing cover image: %w", err) + } + } + } else { + v := models.NewPerformerPartial() + values = &v + } + + var dest *models.Performer + if err := r.withTxn(ctx, func(ctx context.Context) error { + qb := r.repository.Performer + + dest, err = qb.Find(ctx, destID) + if err != nil { + return fmt.Errorf("finding destination performer ID %d: %w", destID, err) + } + + // ensure source performers exist + if _, err := qb.FindMany(ctx, srcIDs); err != nil { + return fmt.Errorf("finding source performers: %w", err) + } + + if _, err := qb.UpdatePartial(ctx, destID, *values); err != nil { + return fmt.Errorf("updating performer: %w", err) + } + + if err := qb.Merge(ctx, srcIDs, destID); err != nil { + return fmt.Errorf("merging performers: %w", err) + } + + if len(imageData) > 0 { + if err := qb.UpdateImage(ctx, destID, imageData); err != nil { + return err + } + } + + return nil + }); err != nil { + return nil, err + } + + return dest, nil +} diff --git a/internal/api/resolver_mutation_saved_filter.go b/internal/api/resolver_mutation_saved_filter.go index e49c1214c..6e825e3d5 100644 --- a/internal/api/resolver_mutation_saved_filter.go +++ b/internal/api/resolver_mutation_saved_filter.go @@ -32,7 +32,7 @@ func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput f := models.SavedFilter{ Mode: input.Mode, - Name: input.Name, + Name: strings.TrimSpace(input.Name), FindFilter: input.FindFilter, ObjectFilter: input.ObjectFilter, UIOptions: input.UIOptions, diff --git a/internal/api/resolver_mutation_scene.go b/internal/api/resolver_mutation_scene.go index b740955d0..5347de806 100644 --- a/internal/api/resolver_mutation_scene.go +++ b/internal/api/resolver_mutation_scene.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strconv" + "strings" "time" "github.com/stashapp/stash/internal/manager" @@ -62,9 +63,9 @@ func (r *mutationResolver) SceneCreate(ctx context.Context, input models.SceneCr } if input.Urls != nil { - newScene.URLs = models.NewRelatedStrings(input.Urls) + newScene.URLs = models.NewRelatedStrings(stringslice.TrimSpace(input.Urls)) } else if input.URL != nil { - newScene.URLs = models.NewRelatedStrings([]string{*input.URL}) + newScene.URLs = models.NewRelatedStrings([]string{strings.TrimSpace(*input.URL)}) } newScene.PerformerIDs, err = translator.relatedIds(input.PerformerIds) @@ -296,6 +297,7 @@ func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUp } var coverImageData []byte + coverImageIncluded := translator.hasField("cover_image") if input.CoverImage != nil { var err error coverImageData, err = utils.ProcessImageInput(ctx, *input.CoverImage) @@ -309,21 +311,21 @@ func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUp return nil, err } - if err := r.sceneUpdateCoverImage(ctx, scene, coverImageData); err != nil { - return nil, err + if coverImageIncluded { + if err := r.sceneUpdateCoverImage(ctx, scene, coverImageData); err != nil { + return nil, err + } } return scene, nil } func (r *mutationResolver) sceneUpdateCoverImage(ctx context.Context, s *models.Scene, coverImageData []byte) error { - if len(coverImageData) > 0 { - qb := r.repository.Scene + qb := r.repository.Scene - // update cover table - if err := qb.UpdateCover(ctx, s.ID, coverImageData); err != nil { - return err - } + // update cover table - empty data will clear the cover + if err := qb.UpdateCover(ctx, s.ID, coverImageData); err != nil { + return err } return nil @@ -428,16 +430,18 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD } fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm() + trashPath := manager.GetInstance().Config.GetDeleteTrashPath() var s *models.Scene fileDeleter := &scene.FileDeleter{ - Deleter: file.NewDeleter(), + Deleter: file.NewDeleterWithTrash(trashPath), FileNamingAlgo: fileNamingAlgo, Paths: manager.GetInstance().Paths, } deleteGenerated := utils.IsTrue(input.DeleteGenerated) deleteFile := utils.IsTrue(input.DeleteFile) + destroyFileEntry := utils.IsTrue(input.DestroyFileEntry) if err := r.withTxn(ctx, func(ctx context.Context) error { qb := r.repository.Scene @@ -454,7 +458,7 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD // kill any running encoders manager.KillRunningStreams(s, fileNamingAlgo) - return r.sceneService.Destroy(ctx, s, fileDeleter, deleteGenerated, deleteFile) + return r.sceneService.Destroy(ctx, s, fileDeleter, deleteGenerated, deleteFile, destroyFileEntry) }); err != nil { fileDeleter.Rollback() return false, err @@ -482,15 +486,17 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene var scenes []*models.Scene fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm() + trashPath := manager.GetInstance().Config.GetDeleteTrashPath() fileDeleter := &scene.FileDeleter{ - Deleter: file.NewDeleter(), + Deleter: file.NewDeleterWithTrash(trashPath), FileNamingAlgo: fileNamingAlgo, Paths: manager.GetInstance().Paths, } deleteGenerated := utils.IsTrue(input.DeleteGenerated) deleteFile := utils.IsTrue(input.DeleteFile) + destroyFileEntry := utils.IsTrue(input.DestroyFileEntry) if err := r.withTxn(ctx, func(ctx context.Context) error { qb := r.repository.Scene @@ -509,7 +515,7 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene // kill any running encoders manager.KillRunningStreams(scene, fileNamingAlgo) - if err := r.sceneService.Destroy(ctx, scene, fileDeleter, deleteGenerated, deleteFile); err != nil { + if err := r.sceneService.Destroy(ctx, scene, fileDeleter, deleteGenerated, deleteFile, destroyFileEntry); err != nil { return err } } @@ -593,8 +599,9 @@ func (r *mutationResolver) SceneMerge(ctx context.Context, input SceneMergeInput } mgr := manager.GetInstance() + trashPath := mgr.Config.GetDeleteTrashPath() fileDeleter := &scene.FileDeleter{ - Deleter: file.NewDeleter(), + Deleter: file.NewDeleterWithTrash(trashPath), FileNamingAlgo: mgr.Config.GetVideoFileNamingAlgorithm(), Paths: mgr.Paths, } @@ -617,7 +624,12 @@ func (r *mutationResolver) SceneMerge(ctx context.Context, input SceneMergeInput return fmt.Errorf("scene with id %d not found", destID) } - return r.sceneUpdateCoverImage(ctx, ret, coverImageData) + // only update cover image if one was provided + if len(coverImageData) > 0 { + return r.sceneUpdateCoverImage(ctx, ret, coverImageData) + } + + return nil }); err != nil { return nil, err } @@ -650,7 +662,7 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMar // Populate a new scene marker from the input newMarker := models.NewSceneMarker() - newMarker.Title = input.Title + newMarker.Title = strings.TrimSpace(input.Title) newMarker.Seconds = input.Seconds newMarker.PrimaryTagID = primaryTagID newMarker.SceneID = sceneID @@ -736,9 +748,10 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar } mgr := manager.GetInstance() + trashPath := mgr.Config.GetDeleteTrashPath() fileDeleter := &scene.FileDeleter{ - Deleter: file.NewDeleter(), + Deleter: file.NewDeleterWithTrash(trashPath), FileNamingAlgo: mgr.Config.GetVideoFileNamingAlgorithm(), Paths: mgr.Paths, } @@ -820,6 +833,123 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar return r.getSceneMarker(ctx, markerID) } +func (r *mutationResolver) BulkSceneMarkerUpdate(ctx context.Context, input BulkSceneMarkerUpdateInput) ([]*models.SceneMarker, error) { + ids, err := stringslice.StringSliceToIntSlice(input.Ids) + if err != nil { + return nil, fmt.Errorf("converting ids: %w", err) + } + + translator := changesetTranslator{ + inputMap: getUpdateInputMap(ctx), + } + + // Populate performer from the input + partial := models.NewSceneMarkerPartial() + + partial.Title = translator.optionalString(input.Title, "title") + + partial.PrimaryTagID, err = translator.optionalIntFromString(input.PrimaryTagID, "primary_tag_id") + if err != nil { + return nil, fmt.Errorf("converting primary tag id: %w", err) + } + + partial.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids") + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) + } + + ret := []*models.SceneMarker{} + + // Start the transaction and save the performers + if err := r.withTxn(ctx, func(ctx context.Context) error { + qb := r.repository.SceneMarker + + for _, id := range ids { + l := partial + + if err := adjustMarkerPartialForTagExclusion(ctx, r.repository.SceneMarker, id, &l); err != nil { + return err + } + + updated, err := qb.UpdatePartial(ctx, id, l) + if err != nil { + return err + } + + ret = append(ret, updated) + } + + return nil + }); err != nil { + return nil, err + } + + // execute post hooks outside of txn + var newRet []*models.SceneMarker + for _, m := range ret { + r.hookExecutor.ExecutePostHooks(ctx, m.ID, hook.SceneMarkerUpdatePost, input, translator.getFields()) + + m, err = r.getSceneMarker(ctx, m.ID) + if err != nil { + return nil, err + } + + newRet = append(newRet, m) + } + + return newRet, nil +} + +// adjustMarkerPartialForTagExclusion adjusts the SceneMarkerPartial to exclude the primary tag from tag updates. +func adjustMarkerPartialForTagExclusion(ctx context.Context, r models.SceneMarkerReader, id int, partial *models.SceneMarkerPartial) error { + if partial.TagIDs == nil && !partial.PrimaryTagID.Set { + return nil + } + + // exclude primary tag from tag updates + var primaryTagID int + if partial.PrimaryTagID.Set { + primaryTagID = partial.PrimaryTagID.Value + } else { + existing, err := r.Find(ctx, id) + if err != nil { + return fmt.Errorf("finding existing primary tag id: %w", err) + } + + primaryTagID = existing.PrimaryTagID + } + + existingTagIDs, err := r.GetTagIDs(ctx, id) + if err != nil { + return fmt.Errorf("getting existing tag ids: %w", err) + } + + tagIDAttr := partial.TagIDs + + if tagIDAttr == nil { + tagIDAttr = &models.UpdateIDs{ + IDs: existingTagIDs, + Mode: models.RelationshipUpdateModeSet, + } + } + + newTagIDs := tagIDAttr.Apply(existingTagIDs) + // Remove primary tag from newTagIDs if present + newTagIDs = sliceutil.Exclude(newTagIDs, []int{primaryTagID}) + + if len(existingTagIDs) != len(newTagIDs) { + partial.TagIDs = &models.UpdateIDs{ + IDs: newTagIDs, + Mode: models.RelationshipUpdateModeSet, + } + } else { + // no change to tags required + partial.TagIDs = nil + } + + return nil +} + func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (bool, error) { return r.SceneMarkersDestroy(ctx, []string{id}) } @@ -832,9 +962,10 @@ func (r *mutationResolver) SceneMarkersDestroy(ctx context.Context, markerIDs [] var markers []*models.SceneMarker fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm() + trashPath := manager.GetInstance().Config.GetDeleteTrashPath() fileDeleter := &scene.FileDeleter{ - Deleter: file.NewDeleter(), + Deleter: file.NewDeleterWithTrash(trashPath), FileNamingAlgo: fileNamingAlgo, Paths: manager.GetInstance().Paths, } diff --git a/internal/api/resolver_mutation_stash_box.go b/internal/api/resolver_mutation_stash_box.go index bbfe8b854..436937511 100644 --- a/internal/api/resolver_mutation_stash_box.go +++ b/internal/api/resolver_mutation_stash_box.go @@ -39,7 +39,7 @@ func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input } func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input manager.StashBoxBatchTagInput) (string, error) { - b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint) + b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint) //nolint:staticcheck if err != nil { return "", err } @@ -49,7 +49,7 @@ func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input } func (r *mutationResolver) StashBoxBatchStudioTag(ctx context.Context, input manager.StashBoxBatchTagInput) (string, error) { - b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint) + b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint) //nolint:staticcheck if err != nil { return "", err } @@ -153,6 +153,14 @@ func (r *mutationResolver) makeSceneDraft(ctx context.Context, s *models.Scene, return nil, err } + // Load StashIDs for tags + tqb := r.repository.Tag + for _, t := range draft.Tags { + if err := t.LoadStashIDs(ctx, tqb); err != nil { + return nil, err + } + } + draft.Cover = cover return draft, nil diff --git a/internal/api/resolver_mutation_studio.go b/internal/api/resolver_mutation_studio.go index 727951755..e3e1c6395 100644 --- a/internal/api/resolver_mutation_studio.go +++ b/internal/api/resolver_mutation_studio.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strconv" + "strings" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/plugin/hook" @@ -30,19 +31,27 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio } // Populate a new studio from the input - newStudio := models.NewStudio() + newStudio := models.NewCreateStudioInput() - newStudio.Name = input.Name - newStudio.URL = translator.string(input.URL) + newStudio.Name = strings.TrimSpace(input.Name) newStudio.Rating = input.Rating100 newStudio.Favorite = translator.bool(input.Favorite) newStudio.Details = translator.string(input.Details) newStudio.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag) - newStudio.Aliases = models.NewRelatedStrings(input.Aliases) + newStudio.Aliases = models.NewRelatedStrings(stringslice.UniqueExcludeFold(stringslice.TrimSpace(input.Aliases), newStudio.Name)) newStudio.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs()) var err error + newStudio.URLs = models.NewRelatedStrings([]string{}) + if input.URL != nil { + newStudio.URLs.Add(strings.TrimSpace(*input.URL)) + } + + if input.Urls != nil { + newStudio.URLs.Add(stringslice.TrimSpace(input.Urls)...) + } + newStudio.ParentID, err = translator.intPtrFromString(input.ParentID) if err != nil { return nil, fmt.Errorf("converting parent id: %w", err) @@ -52,6 +61,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio if err != nil { return nil, fmt.Errorf("converting tag ids: %w", err) } + newStudio.CustomFields = convertMapJSONNumbers(input.CustomFields) // Process the base 64 encoded image string var imageData []byte @@ -106,7 +116,6 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio updatedStudio.ID = studioID updatedStudio.Name = translator.optionalString(input.Name, "name") - updatedStudio.URL = translator.optionalString(input.URL, "url") updatedStudio.Details = translator.optionalString(input.Details, "details") updatedStudio.Rating = translator.optionalInt(input.Rating100, "rating100") updatedStudio.Favorite = translator.optionalBool(input.Favorite, "favorite") @@ -124,6 +133,31 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio return nil, fmt.Errorf("converting tag ids: %w", err) } + if translator.hasField("urls") { + // ensure url not included in the input + if err := validateNoLegacyURLs(translator); err != nil { + return nil, err + } + + updatedStudio.URLs = translator.updateStrings(input.Urls, "urls") + } else if translator.hasField("url") { + // handle legacy url field + legacyURLs := []string{} + if input.URL != nil { + legacyURLs = append(legacyURLs, *input.URL) + } + + updatedStudio.URLs = &models.UpdateStrings{ + Mode: models.RelationshipUpdateModeSet, + Values: legacyURLs, + } + } + + updatedStudio.CustomFields = input.CustomFields + // convert json.Numbers to int/float + updatedStudio.CustomFields.Full = convertMapJSONNumbers(updatedStudio.CustomFields.Full) + updatedStudio.CustomFields.Partial = convertMapJSONNumbers(updatedStudio.CustomFields.Partial) + // Process the base 64 encoded image string var imageData []byte imageIncluded := translator.hasField("image") @@ -139,6 +173,28 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio if err := r.withTxn(ctx, func(ctx context.Context) error { qb := r.repository.Studio + if updatedStudio.Aliases != nil { + s, err := qb.Find(ctx, studioID) + if err != nil { + return err + } + if s != nil { + if err := s.LoadAliases(ctx, qb); err != nil { + return err + } + + effectiveAliases := updatedStudio.Aliases.Apply(s.Aliases.List()) + name := s.Name + if updatedStudio.Name.Set { + name = updatedStudio.Name.Value + } + + sanitized := stringslice.UniqueExcludeFold(effectiveAliases, name) + updatedStudio.Aliases.Values = sanitized + updatedStudio.Aliases.Mode = models.RelationshipUpdateModeSet + } + } + if err := studio.ValidateModify(ctx, updatedStudio, qb); err != nil { return err } @@ -163,6 +219,96 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio return r.getStudio(ctx, studioID) } +func (r *mutationResolver) BulkStudioUpdate(ctx context.Context, input BulkStudioUpdateInput) ([]*models.Studio, error) { + ids, err := stringslice.StringSliceToIntSlice(input.Ids) + if err != nil { + return nil, fmt.Errorf("converting ids: %w", err) + } + + translator := changesetTranslator{ + inputMap: getUpdateInputMap(ctx), + } + + // Populate performer from the input + partial := models.NewStudioPartial() + + partial.ParentID, err = translator.optionalIntFromString(input.ParentID, "parent_id") + if err != nil { + return nil, fmt.Errorf("converting parent id: %w", err) + } + + if translator.hasField("urls") { + // ensure url/twitter/instagram are not included in the input + if err := validateNoLegacyURLs(translator); err != nil { + return nil, err + } + + partial.URLs = translator.updateStringsBulk(input.Urls, "urls") + } else if translator.hasField("url") { + // handle legacy url field + legacyURLs := []string{} + if input.URL != nil { + legacyURLs = append(legacyURLs, *input.URL) + } + + partial.URLs = &models.UpdateStrings{ + Mode: models.RelationshipUpdateModeSet, + Values: legacyURLs, + } + } + + partial.Favorite = translator.optionalBool(input.Favorite, "favorite") + partial.Rating = translator.optionalInt(input.Rating100, "rating100") + partial.Details = translator.optionalString(input.Details, "details") + partial.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") + + partial.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids") + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) + } + + ret := []*models.Studio{} + + // Start the transaction and save the performers + if err := r.withTxn(ctx, func(ctx context.Context) error { + qb := r.repository.Studio + + for _, id := range ids { + local := partial + local.ID = id + if err := studio.ValidateModify(ctx, local, qb); err != nil { + return err + } + + updated, err := qb.UpdatePartial(ctx, local) + if err != nil { + return err + } + + ret = append(ret, updated) + } + + return nil + }); err != nil { + return nil, err + } + + // execute post hooks outside of txn + var newRet []*models.Studio + for _, studio := range ret { + r.hookExecutor.ExecutePostHooks(ctx, studio.ID, hook.StudioUpdatePost, input, translator.getFields()) + + studio, err = r.getStudio(ctx, studio.ID) + if err != nil { + return nil, err + } + + newRet = append(newRet, studio) + } + + return newRet, nil +} + func (r *mutationResolver) StudioDestroy(ctx context.Context, input StudioDestroyInput) (bool, error) { id, err := strconv.Atoi(input.ID) if err != nil { diff --git a/internal/api/resolver_mutation_tag.go b/internal/api/resolver_mutation_tag.go index 1e8b6066a..8fb295d40 100644 --- a/internal/api/resolver_mutation_tag.go +++ b/internal/api/resolver_mutation_tag.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strconv" + "strings" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" @@ -32,13 +33,21 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput) // Populate a new tag from the input newTag := models.NewTag() - newTag.Name = input.Name + newTag.Name = strings.TrimSpace(input.Name) newTag.SortName = translator.string(input.SortName) - newTag.Aliases = models.NewRelatedStrings(input.Aliases) + newTag.Aliases = models.NewRelatedStrings(stringslice.UniqueExcludeFold(stringslice.TrimSpace(input.Aliases), newTag.Name)) newTag.Favorite = translator.bool(input.Favorite) newTag.Description = translator.string(input.Description) newTag.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag) + var stashIDInputs models.StashIDInputs + for _, sid := range input.StashIds { + if sid != nil { + stashIDInputs = append(stashIDInputs, *sid) + } + } + newTag.StashIDs = models.NewRelatedStashIDs(stashIDInputs.ToStashIDs()) + var err error newTag.ParentIDs, err = translator.relatedIds(input.ParentIds) @@ -110,6 +119,14 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput) updatedTag.Aliases = translator.updateStrings(input.Aliases, "aliases") + var updateStashIDInputs models.StashIDInputs + for _, sid := range input.StashIds { + if sid != nil { + updateStashIDInputs = append(updateStashIDInputs, *sid) + } + } + updatedTag.StashIDs = translator.updateStashIDs(updateStashIDInputs, "stash_ids") + updatedTag.ParentIDs, err = translator.updateIds(input.ParentIds, "parent_ids") if err != nil { return nil, fmt.Errorf("converting parent tag ids: %w", err) @@ -134,6 +151,28 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput) if err := r.withTxn(ctx, func(ctx context.Context) error { qb := r.repository.Tag + if updatedTag.Aliases != nil { + t, err := qb.Find(ctx, tagID) + if err != nil { + return err + } + if t != nil { + if err := t.LoadAliases(ctx, qb); err != nil { + return err + } + + newAliases := updatedTag.Aliases.Apply(t.Aliases.List()) + name := t.Name + if updatedTag.Name.Set { + name = updatedTag.Name.Value + } + + sanitized := stringslice.UniqueExcludeFold(newAliases, name) + updatedTag.Aliases.Values = sanitized + updatedTag.Aliases.Mode = models.RelationshipUpdateModeSet + } + } + if err := tag.ValidateUpdate(ctx, tagID, updatedTag, qb); err != nil { return err } diff --git a/internal/api/resolver_query_configuration.go b/internal/api/resolver_query_configuration.go index cfa22720b..bc76212eb 100644 --- a/internal/api/resolver_query_configuration.go +++ b/internal/api/resolver_query_configuration.go @@ -82,6 +82,7 @@ func makeConfigGeneralResult() *ConfigGeneralResult { Stashes: config.GetStashPaths(), DatabasePath: config.GetDatabasePath(), BackupDirectoryPath: config.GetBackupDirectoryPath(), + DeleteTrashPath: config.GetDeleteTrashPath(), GeneratedPath: config.GetGeneratedPath(), MetadataPath: config.GetMetadataPath(), ConfigFilePath: config.GetConfigFile(), @@ -115,6 +116,7 @@ func makeConfigGeneralResult() *ConfigGeneralResult { LogOut: config.GetLogOut(), LogLevel: config.GetLogLevel(), LogAccess: config.GetLogAccess(), + LogFileMaxSize: config.GetLogFileMaxSize(), VideoExtensions: config.GetVideoExtensions(), ImageExtensions: config.GetImageExtensions(), GalleryExtensions: config.GetGalleryExtensions(), @@ -154,6 +156,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult { javascriptEnabled := config.GetJavascriptEnabled() customLocales := config.GetCustomLocales() customLocalesEnabled := config.GetCustomLocalesEnabled() + disableCustomizations := config.GetDisableCustomizations() language := config.GetLanguage() handyKey := config.GetHandyKey() scriptOffset := config.GetFunscriptOffset() @@ -162,6 +165,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult { disableDropdownCreate := config.GetDisableDropdownCreate() return &ConfigInterfaceResult{ + SfwContentMode: config.GetSFWContentMode(), MenuItems: menuItems, SoundOnPreview: &soundOnPreview, WallShowTitle: &wallShowTitle, @@ -180,6 +184,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult { JavascriptEnabled: &javascriptEnabled, CustomLocales: &customLocales, CustomLocalesEnabled: &customLocalesEnabled, + DisableCustomizations: &disableCustomizations, Language: &language, ImageLightbox: &imageLightboxOptions, diff --git a/internal/api/resolver_query_find_file.go b/internal/api/resolver_query_find_file.go index ae53a89b4..01c14b1ed 100644 --- a/internal/api/resolver_query_find_file.go +++ b/internal/api/resolver_query_find_file.go @@ -29,7 +29,7 @@ func (r *queryResolver) FindFile(ctx context.Context, id *string, path *string) ret = files[0] } case path != nil: - ret, err = qb.FindByPath(ctx, *path) + ret, err = qb.FindByPath(ctx, *path, true) if err == nil && ret == nil { return errors.New("file not found") } diff --git a/internal/api/resolver_query_find_folder.go b/internal/api/resolver_query_find_folder.go index a7a798dd1..60088e2a3 100644 --- a/internal/api/resolver_query_find_folder.go +++ b/internal/api/resolver_query_find_folder.go @@ -6,7 +6,6 @@ import ( "strconv" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) func (r *queryResolver) FindFolder(ctx context.Context, id *string, path *string) (*models.Folder, error) { @@ -25,7 +24,7 @@ func (r *queryResolver) FindFolder(ctx context.Context, id *string, path *string return err } case path != nil: - ret, err = qb.FindByPath(ctx, *path) + ret, err = qb.FindByPath(ctx, *path, true) if err == nil && ret == nil { return errors.New("folder not found") } @@ -49,7 +48,7 @@ func (r *queryResolver) FindFolders( ) (ret *FindFoldersResultType, err error) { var folderIDs []models.FolderID if len(ids) > 0 { - folderIDsInt, err := stringslice.StringSliceToIntSlice(ids) + folderIDsInt, err := handleIDList(ids, "ids") if err != nil { return nil, err } diff --git a/internal/api/resolver_query_find_gallery.go b/internal/api/resolver_query_find_gallery.go index 724a48b12..09c0387cd 100644 --- a/internal/api/resolver_query_find_gallery.go +++ b/internal/api/resolver_query_find_gallery.go @@ -5,7 +5,6 @@ import ( "strconv" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models.Gallery, err error) { @@ -25,7 +24,7 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models } func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType, ids []string) (ret *FindGalleriesResultType, err error) { - idInts, err := stringslice.StringSliceToIntSlice(ids) + idInts, err := handleIDList(ids, "ids") if err != nil { return nil, err } diff --git a/internal/api/resolver_query_find_group.go b/internal/api/resolver_query_find_group.go index 6f8a6c6ba..14d282379 100644 --- a/internal/api/resolver_query_find_group.go +++ b/internal/api/resolver_query_find_group.go @@ -5,7 +5,6 @@ import ( "strconv" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) func (r *queryResolver) FindGroup(ctx context.Context, id string) (ret *models.Group, err error) { @@ -25,7 +24,7 @@ func (r *queryResolver) FindGroup(ctx context.Context, id string) (ret *models.G } func (r *queryResolver) FindGroups(ctx context.Context, groupFilter *models.GroupFilterType, filter *models.FindFilterType, ids []string) (ret *FindGroupsResultType, err error) { - idInts, err := stringslice.StringSliceToIntSlice(ids) + idInts, err := handleIDList(ids, "ids") if err != nil { return nil, err } diff --git a/internal/api/resolver_query_find_image.go b/internal/api/resolver_query_find_image.go index 48b926345..90eaf33c0 100644 --- a/internal/api/resolver_query_find_image.go +++ b/internal/api/resolver_query_find_image.go @@ -7,7 +7,6 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) { @@ -55,7 +54,7 @@ func (r *queryResolver) FindImages( filter *models.FindFilterType, ) (ret *FindImagesResultType, err error) { if len(ids) > 0 { - imageIds, err = stringslice.StringSliceToIntSlice(ids) + imageIds, err = handleIDList(ids, "ids") if err != nil { return nil, err } diff --git a/internal/api/resolver_query_find_movie.go b/internal/api/resolver_query_find_movie.go index 2f80d6f59..c9dd3f846 100644 --- a/internal/api/resolver_query_find_movie.go +++ b/internal/api/resolver_query_find_movie.go @@ -5,7 +5,6 @@ import ( "strconv" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.Group, err error) { @@ -25,7 +24,7 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.G } func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.GroupFilterType, filter *models.FindFilterType, ids []string) (ret *FindMoviesResultType, err error) { - idInts, err := stringslice.StringSliceToIntSlice(ids) + idInts, err := handleIDList(ids, "ids") if err != nil { return nil, err } diff --git a/internal/api/resolver_query_find_performer.go b/internal/api/resolver_query_find_performer.go index 150c99d20..7ea1f90c8 100644 --- a/internal/api/resolver_query_find_performer.go +++ b/internal/api/resolver_query_find_performer.go @@ -5,7 +5,6 @@ import ( "strconv" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *models.Performer, err error) { @@ -26,7 +25,7 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType, performerIDs []int, ids []string) (ret *FindPerformersResultType, err error) { if len(ids) > 0 { - performerIDs, err = stringslice.StringSliceToIntSlice(ids) + performerIDs, err = handleIDList(ids, "ids") if err != nil { return nil, err } diff --git a/internal/api/resolver_query_find_scene.go b/internal/api/resolver_query_find_scene.go index 44b5cfd5e..135ec43b7 100644 --- a/internal/api/resolver_query_find_scene.go +++ b/internal/api/resolver_query_find_scene.go @@ -9,7 +9,6 @@ import ( "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/scene" - "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) { @@ -83,7 +82,7 @@ func (r *queryResolver) FindScenes( filter *models.FindFilterType, ) (ret *FindScenesResultType, err error) { if len(ids) > 0 { - sceneIDs, err = stringslice.StringSliceToIntSlice(ids) + sceneIDs, err = handleIDList(ids, "ids") if err != nil { return nil, err } diff --git a/internal/api/resolver_query_find_scene_marker.go b/internal/api/resolver_query_find_scene_marker.go index d3e47ce8d..e244bafef 100644 --- a/internal/api/resolver_query_find_scene_marker.go +++ b/internal/api/resolver_query_find_scene_marker.go @@ -4,11 +4,10 @@ import ( "context" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, filter *models.FindFilterType, ids []string) (ret *FindSceneMarkersResultType, err error) { - idInts, err := stringslice.StringSliceToIntSlice(ids) + idInts, err := handleIDList(ids, "ids") if err != nil { return nil, err } diff --git a/internal/api/resolver_query_find_studio.go b/internal/api/resolver_query_find_studio.go index 843592953..636772fe8 100644 --- a/internal/api/resolver_query_find_studio.go +++ b/internal/api/resolver_query_find_studio.go @@ -5,7 +5,6 @@ import ( "strconv" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.Studio, err error) { @@ -26,7 +25,7 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models. } func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType, ids []string) (ret *FindStudiosResultType, err error) { - idInts, err := stringslice.StringSliceToIntSlice(ids) + idInts, err := handleIDList(ids, "ids") if err != nil { return nil, err } diff --git a/internal/api/resolver_query_find_tag.go b/internal/api/resolver_query_find_tag.go index f0e1d8b97..7dca0b481 100644 --- a/internal/api/resolver_query_find_tag.go +++ b/internal/api/resolver_query_find_tag.go @@ -5,7 +5,6 @@ import ( "strconv" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag, err error) { @@ -25,7 +24,7 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag } func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType, ids []string) (ret *FindTagsResultType, err error) { - idInts, err := stringslice.StringSliceToIntSlice(ids) + idInts, err := handleIDList(ids, "ids") if err != nil { return nil, err } diff --git a/internal/api/resolver_query_scraper.go b/internal/api/resolver_query_scraper.go index f0e89cd34..86d449921 100644 --- a/internal/api/resolver_query_scraper.go +++ b/internal/api/resolver_query_scraper.go @@ -201,7 +201,7 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.So } // TODO - this should happen after any scene is scraped - if err := r.matchScenesRelationships(ctx, ret, *source.StashBoxEndpoint); err != nil { + if err := r.matchScenesRelationships(ctx, ret, b.Endpoint); err != nil { return nil, err } default: @@ -245,7 +245,7 @@ func (r *queryResolver) ScrapeMultiScenes(ctx context.Context, source scraper.So // just flatten the slice and pass it in flat := sliceutil.Flatten(ret) - if err := r.matchScenesRelationships(ctx, flat, *source.StashBoxEndpoint); err != nil { + if err := r.matchScenesRelationships(ctx, flat, b.Endpoint); err != nil { return nil, err } @@ -335,7 +335,7 @@ func (r *queryResolver) ScrapeSingleStudio(ctx context.Context, source scraper.S if len(ret) > 0 { if err := r.withReadTxn(ctx, func(ctx context.Context) error { for _, studio := range ret { - if err := match.ScrapedStudioHierarchy(ctx, r.repository.Studio, studio, *source.StashBoxEndpoint); err != nil { + if err := match.ScrapedStudioHierarchy(ctx, r.repository.Studio, studio, b.Endpoint); err != nil { return err } } @@ -350,7 +350,46 @@ func (r *queryResolver) ScrapeSingleStudio(ctx context.Context, source scraper.S return nil, nil } - return nil, errors.New("stash_box_index must be set") + return nil, errors.New("stash_box_endpoint must be set") +} + +func (r *queryResolver) ScrapeSingleTag(ctx context.Context, source scraper.Source, input ScrapeSingleTagInput) ([]*models.ScrapedTag, error) { + if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil { + b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint) + if err != nil { + return nil, err + } + + client := r.newStashBoxClient(*b) + + var ret []*models.ScrapedTag + out, err := client.QueryTag(ctx, *input.Query) + + if err != nil { + return nil, err + } else if out != nil { + ret = append(ret, out...) + } + + if len(ret) > 0 { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + for _, tag := range ret { + if err := match.ScrapedTag(ctx, r.repository.Tag, tag, b.Endpoint); err != nil { + return err + } + } + + return nil + }); err != nil { + return nil, err + } + return ret, nil + } + + return nil, nil + } + + return nil, errors.New("stash_box_endpoint must be set") } func (r *queryResolver) ScrapeSinglePerformer(ctx context.Context, source scraper.Source, input ScrapeSinglePerformerInput) ([]*models.ScrapedPerformer, error) { diff --git a/internal/api/routes_performer.go b/internal/api/routes_performer.go index b27fdbd6c..8d5463d63 100644 --- a/internal/api/routes_performer.go +++ b/internal/api/routes_performer.go @@ -18,9 +18,14 @@ type PerformerFinder interface { GetImage(ctx context.Context, performerID int) ([]byte, error) } +type sfwConfig interface { + GetSFWContentMode() bool +} + type performerRoutes struct { routes performerFinder PerformerFinder + sfwConfig sfwConfig } func (rs performerRoutes) Routes() chi.Router { @@ -54,7 +59,7 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) { } if len(image) == 0 { - image = getDefaultPerformerImage(performer.Name, performer.Gender) + image = getDefaultPerformerImage(performer.Name, performer.Gender, rs.sfwConfig.GetSFWContentMode()) } utils.ServeImage(w, r, image) diff --git a/internal/api/routes_scene.go b/internal/api/routes_scene.go index 95e7c9d44..2905bd53a 100644 --- a/internal/api/routes_scene.go +++ b/internal/api/routes_scene.go @@ -12,6 +12,7 @@ import ( "github.com/stashapp/stash/internal/manager" "github.com/stashapp/stash/internal/manager/config" + "github.com/stashapp/stash/internal/static" "github.com/stashapp/stash/pkg/ffmpeg" "github.com/stashapp/stash/pkg/file/video" "github.com/stashapp/stash/pkg/fsutil" @@ -243,6 +244,12 @@ func (rs sceneRoutes) streamSegment(w http.ResponseWriter, r *http.Request, stre } func (rs sceneRoutes) Screenshot(w http.ResponseWriter, r *http.Request) { + // if default flag is set, return the default image + if r.URL.Query().Get("default") == "true" { + utils.ServeImage(w, r, static.ReadAll(static.DefaultSceneImage)) + return + } + scene := r.Context().Value(sceneKey).(*models.Scene) ss := manager.SceneServer{ diff --git a/internal/api/server.go b/internal/api/server.go index 5059e9a2a..a7516da52 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -11,6 +11,7 @@ import ( "net/http" "os" "path" + "path/filepath" "runtime/debug" "strconv" "strings" @@ -255,6 +256,9 @@ func Initialize() (*Server, error) { staticUI = statigz.FileServer(ui.UIBox.(fs.ReadDirFS)) } + // handle favicon override + r.HandleFunc("/favicon.ico", handleFavicon(staticUI)) + // Serve the web app r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { ext := path.Ext(r.URL.Path) @@ -295,6 +299,31 @@ func Initialize() (*Server, error) { return server, nil } +func handleFavicon(staticUI *statigz.Server) func(w http.ResponseWriter, r *http.Request) { + mgr := manager.GetInstance() + cfg := mgr.Config + + // check if favicon.ico exists in the config directory + // if so, use that + // otherwise, use the embedded one + iconPath := filepath.Join(cfg.GetConfigPath(), "favicon.ico") + exists, _ := fsutil.FileExists(iconPath) + + if exists { + logger.Debugf("Using custom favicon at %s", iconPath) + } + + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-cache") + + if exists { + http.ServeFile(w, r, iconPath) + } else { + staticUI.ServeHTTP(w, r) + } + } +} + // Start starts the server. It listens on the configured address and port. // It calls ListenAndServeTLS if TLS is configured, otherwise it calls ListenAndServe. // Calls to Start are blocked until the server is shutdown. @@ -322,6 +351,7 @@ func (s *Server) getPerformerRoutes() chi.Router { return performerRoutes{ routes: routes{txnManager: repo.TxnManager}, performerFinder: repo.Performer, + sfwConfig: s.manager.Config, }.Routes() } @@ -420,7 +450,7 @@ func cssHandler(c *config.Config) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var paths []string - if c.GetCSSEnabled() { + if c.GetCSSEnabled() && !c.GetDisableCustomizations() { // search for custom.css in current directory, then $HOME/.stash fn := c.GetCSSPath() exists, _ := fsutil.FileExists(fn) @@ -438,7 +468,7 @@ func javascriptHandler(c *config.Config) func(w http.ResponseWriter, r *http.Req return func(w http.ResponseWriter, r *http.Request) { var paths []string - if c.GetJavascriptEnabled() { + if c.GetJavascriptEnabled() && !c.GetDisableCustomizations() { // search for custom.js in current directory, then $HOME/.stash fn := c.GetJavascriptPath() exists, _ := fsutil.FileExists(fn) @@ -456,7 +486,7 @@ func customLocalesHandler(c *config.Config) func(w http.ResponseWriter, r *http. return func(w http.ResponseWriter, r *http.Request) { buffer := bytes.Buffer{} - if c.GetCustomLocalesEnabled() { + if c.GetCustomLocalesEnabled() && !c.GetDisableCustomizations() { // search for custom-locales.json in current directory, then $HOME/.stash path := c.GetCustomLocalesPath() exists, _ := fsutil.FileExists(path) diff --git a/internal/api/urlbuilders/gallery.go b/internal/api/urlbuilders/gallery.go index 3e6c5ef08..2723781f2 100644 --- a/internal/api/urlbuilders/gallery.go +++ b/internal/api/urlbuilders/gallery.go @@ -9,12 +9,14 @@ import ( type GalleryURLBuilder struct { BaseURL string GalleryID string + UpdatedAt string } func NewGalleryURLBuilder(baseURL string, gallery *models.Gallery) GalleryURLBuilder { return GalleryURLBuilder{ BaseURL: baseURL, GalleryID: strconv.Itoa(gallery.ID), + UpdatedAt: strconv.FormatInt(gallery.UpdatedAt.Unix(), 10), } } @@ -23,5 +25,5 @@ func (b GalleryURLBuilder) GetPreviewURL() string { } func (b GalleryURLBuilder) GetCoverURL() string { - return b.BaseURL + "/gallery/" + b.GalleryID + "/cover" + return b.BaseURL + "/gallery/" + b.GalleryID + "/cover?t=" + b.UpdatedAt } diff --git a/internal/autotag/integration_test.go b/internal/autotag/integration_test.go index 565d73853..605082b98 100644 --- a/internal/autotag/integration_test.go +++ b/internal/autotag/integration_test.go @@ -101,16 +101,15 @@ func createPerformer(ctx context.Context, pqb models.PerformerWriter) error { func createStudio(ctx context.Context, qb models.StudioWriter, name string) (*models.Studio, error) { // create the studio - studio := models.Studio{ - Name: name, - } + studio := models.NewCreateStudioInput() + studio.Name = name err := qb.Create(ctx, &studio) if err != nil { return nil, err } - return &studio, nil + return studio.Studio, nil } func createTag(ctx context.Context, qb models.TagWriter) error { @@ -225,7 +224,7 @@ func createSceneFile(ctx context.Context, name string, folderStore models.Folder } func getOrCreateFolder(ctx context.Context, folderStore models.FolderFinderCreator, folderPath string) (*models.Folder, error) { - f, err := folderStore.FindByPath(ctx, folderPath) + f, err := folderStore.FindByPath(ctx, folderPath, true) if err != nil { return nil, fmt.Errorf("getting folder by path: %w", err) } diff --git a/internal/desktop/desktop.go b/internal/desktop/desktop.go index a89a3c962..06d400793 100644 --- a/internal/desktop/desktop.go +++ b/internal/desktop/desktop.go @@ -17,6 +17,16 @@ import ( "golang.org/x/term" ) +var isDesktop bool + +// InitIsDesktop sets the value of isDesktop. +// Changed IsDesktop to be evaluated once at startup because if it is +// checked while there are open terminal sessions (such as the ffmpeg hardware +// encoding checks), it may return false. +func InitIsDesktop() { + isDesktop = isDesktopCheck() +} + type FaviconProvider interface { GetFavicon() []byte GetFaviconPng() []byte @@ -59,22 +69,33 @@ func SendNotification(title string, text string) { } func IsDesktop() bool { + return isDesktop +} + +// isDesktop tries to determine if the application is running in a desktop environment +// where desktop features like system tray and notifications should be enabled. +func isDesktopCheck() bool { if isDoubleClickLaunched() { + logger.Debug("Detected double-click launch") return true } // Check if running under root if os.Getuid() == 0 { + logger.Debug("Running as root, disabling desktop features") return false } // Check if stdin is a terminal if term.IsTerminal(int(os.Stdin.Fd())) { + logger.Debug("Running in terminal, disabling desktop features") return false } if isService() { + logger.Debug("Running as a service, disabling desktop features") return false } if IsServerDockerized() { + logger.Debug("Running in docker, disabling desktop features") return false } diff --git a/internal/desktop/systray_nonlinux.go b/internal/desktop/systray_nonlinux.go index 450e503ea..6b6055f11 100644 --- a/internal/desktop/systray_nonlinux.go +++ b/internal/desktop/systray_nonlinux.go @@ -3,6 +3,8 @@ package desktop import ( + "fmt" + "runtime" "strings" "github.com/kermieisinthehouse/systray" @@ -20,7 +22,12 @@ func startSystray(exit chan int, faviconProvider FaviconProvider) { // system is started from a non-terminal method, e.g. double-clicking an icon. c := config.GetInstance() if c.GetShowOneTimeMovedNotification() { - SendNotification("Stash has moved!", "Stash now runs in your tray, instead of a terminal window.") + // Use platform-appropriate terminology + location := "tray" + if runtime.GOOS == "darwin" { + location = "menu bar" + } + SendNotification("Stash has moved!", "Stash now runs in your "+location+", instead of a terminal window.") c.SetBool(config.ShowOneTimeMovedNotification, false) if err := c.Write(); err != nil { logger.Errorf("Error while writing configuration file: %v", err) @@ -52,12 +59,12 @@ func startSystray(exit chan int, faviconProvider FaviconProvider) { func systrayInitialize(exit chan<- int, faviconProvider FaviconProvider) { favicon := faviconProvider.GetFavicon() systray.SetTemplateIcon(favicon, favicon) - systray.SetTooltip("🟢 Stash is Running.") + c := config.GetInstance() + systray.SetTooltip(fmt.Sprintf("🟢 Stash is Running on port %d.", c.GetPort())) openStashButton := systray.AddMenuItem("Open Stash", "Open a browser window to Stash") var menuItems []string systray.AddSeparator() - c := config.GetInstance() if !c.IsNewSystem() { menuItems = c.GetMenuItems() for _, item := range menuItems { diff --git a/internal/dlna/activity.go b/internal/dlna/activity.go new file mode 100644 index 000000000..a9a5d9b2d --- /dev/null +++ b/internal/dlna/activity.go @@ -0,0 +1,333 @@ +package dlna + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/txn" +) + +const ( + // DefaultSessionTimeout is the time after which a session is considered complete + // if no new requests are received. + // This is set high (5 minutes) because DLNA clients buffer aggressively and may not + // send any HTTP requests for extended periods while the user is still watching. + DefaultSessionTimeout = 5 * time.Minute + + // monitorInterval is how often we check for expired sessions. + monitorInterval = 10 * time.Second +) + +// ActivityConfig provides configuration options for DLNA activity tracking. +type ActivityConfig interface { + // GetDLNAActivityTrackingEnabled returns true if activity tracking should be enabled. + // If not implemented, defaults to true. + GetDLNAActivityTrackingEnabled() bool + + // GetMinimumPlayPercent returns the minimum percentage of a video that must be + // watched before incrementing the play count. Uses UI setting if available. + GetMinimumPlayPercent() int +} + +// SceneActivityWriter provides methods for saving scene activity. +type SceneActivityWriter interface { + SaveActivity(ctx context.Context, sceneID int, resumeTime *float64, playDuration *float64) (bool, error) + AddViews(ctx context.Context, sceneID int, dates []time.Time) ([]time.Time, error) +} + +// streamSession represents an active DLNA streaming session. +type streamSession struct { + SceneID int + ClientIP string + StartTime time.Time + LastActivity time.Time + VideoDuration float64 + PlayCountAdded bool +} + +// sessionKey generates a unique key for a session based on client IP and scene ID. +func sessionKey(clientIP string, sceneID int) string { + return fmt.Sprintf("%s:%d", clientIP, sceneID) +} + +// percentWatched calculates the estimated percentage of video watched. +// Uses a time-based approach since DLNA clients buffer aggressively and byte +// positions don't correlate with actual playback position. +// +// The key insight: you cannot have watched more of the video than time has elapsed. +// If the video is 30 minutes and only 1 minute has passed, maximum watched is ~3.3%. +func (s *streamSession) percentWatched() float64 { + if s.VideoDuration <= 0 { + return 0 + } + + // Calculate elapsed time from session start to last activity + elapsed := s.LastActivity.Sub(s.StartTime).Seconds() + if elapsed <= 0 { + return 0 + } + + // Maximum possible percent is based on elapsed time + // You can't watch more of the video than time has passed + timeBasedPercent := (elapsed / s.VideoDuration) * 100 + + // Cap at 100% + if timeBasedPercent > 100 { + return 100 + } + + return timeBasedPercent +} + +// estimatedResumeTime calculates the estimated resume time based on elapsed time. +// Since DLNA clients buffer aggressively, byte positions don't correlate with playback. +// Instead, we estimate based on how long the session has been active. +// Returns the time in seconds, or 0 if the video is nearly complete (>=98%). +func (s *streamSession) estimatedResumeTime() float64 { + if s.VideoDuration <= 0 { + return 0 + } + + // Calculate elapsed time from session start + elapsed := s.LastActivity.Sub(s.StartTime).Seconds() + if elapsed <= 0 { + return 0 + } + + // If elapsed time exceeds 98% of video duration, reset resume time (matches frontend behavior) + if elapsed >= s.VideoDuration*0.98 { + return 0 + } + + // Resume time is approximately where the user was watching + // Capped by video duration + if elapsed > s.VideoDuration { + elapsed = s.VideoDuration + } + + return elapsed +} + +// ActivityTracker tracks DLNA streaming activity and saves it to the database. +type ActivityTracker struct { + txnManager txn.Manager + sceneWriter SceneActivityWriter + config ActivityConfig + sessionTimeout time.Duration + + sessions map[string]*streamSession + mutex sync.RWMutex + + ctx context.Context + cancelFunc context.CancelFunc + wg sync.WaitGroup +} + +// NewActivityTracker creates a new ActivityTracker. +func NewActivityTracker( + txnManager txn.Manager, + sceneWriter SceneActivityWriter, + config ActivityConfig, +) *ActivityTracker { + ctx, cancel := context.WithCancel(context.Background()) + + tracker := &ActivityTracker{ + txnManager: txnManager, + sceneWriter: sceneWriter, + config: config, + sessionTimeout: DefaultSessionTimeout, + sessions: make(map[string]*streamSession), + ctx: ctx, + cancelFunc: cancel, + } + + // Start the session monitor goroutine + tracker.wg.Add(1) + go tracker.monitorSessions() + + return tracker +} + +// Stop stops the activity tracker and processes any remaining sessions. +func (t *ActivityTracker) Stop() { + t.cancelFunc() + t.wg.Wait() + + // Process any remaining sessions + t.mutex.Lock() + sessions := make([]*streamSession, 0, len(t.sessions)) + for _, session := range t.sessions { + sessions = append(sessions, session) + } + t.sessions = make(map[string]*streamSession) + t.mutex.Unlock() + + for _, session := range sessions { + t.processCompletedSession(session) + } +} + +// RecordRequest records a streaming request for activity tracking. +// Each request updates the session's LastActivity time, which is used for +// time-based tracking of watch progress. +func (t *ActivityTracker) RecordRequest(sceneID int, clientIP string, videoDuration float64) { + if !t.isEnabled() { + return + } + + key := sessionKey(clientIP, sceneID) + now := time.Now() + + t.mutex.Lock() + defer t.mutex.Unlock() + + session, exists := t.sessions[key] + if !exists { + session = &streamSession{ + SceneID: sceneID, + ClientIP: clientIP, + StartTime: now, + VideoDuration: videoDuration, + } + t.sessions[key] = session + logger.Debugf("[DLNA Activity] New session started: scene=%d, client=%s", sceneID, clientIP) + } + + session.LastActivity = now +} + +// monitorSessions periodically checks for expired sessions and processes them. +func (t *ActivityTracker) monitorSessions() { + defer t.wg.Done() + + ticker := time.NewTicker(monitorInterval) + defer ticker.Stop() + + for { + select { + case <-t.ctx.Done(): + return + case <-ticker.C: + t.processExpiredSessions() + } + } +} + +// processExpiredSessions finds and processes sessions that have timed out. +func (t *ActivityTracker) processExpiredSessions() { + now := time.Now() + var expiredSessions []*streamSession + + t.mutex.Lock() + for key, session := range t.sessions { + timeSinceStart := now.Sub(session.StartTime) + timeSinceActivity := now.Sub(session.LastActivity) + + // Must have no HTTP activity for the full timeout period + if timeSinceActivity <= t.sessionTimeout { + continue + } + + // DLNA clients buffer aggressively - they fetch most/all of the video quickly, + // then play from cache with NO further HTTP requests. + // + // Two scenarios: + // 1. User watched the whole video: timeSinceStart >= videoDuration + // -> Set LastActivity to when timeout began (they finished watching) + // 2. User stopped early: timeSinceStart < videoDuration + // -> Keep LastActivity as-is (best estimate of when they stopped) + + videoDuration := time.Duration(session.VideoDuration) * time.Second + if timeSinceStart >= videoDuration && videoDuration > 0 { + // User likely watched the whole video, then it timed out + // Estimate they watched until the timeout period started + session.LastActivity = now.Add(-t.sessionTimeout) + } + // else: User stopped early - LastActivity is already our best estimate + + expiredSessions = append(expiredSessions, session) + delete(t.sessions, key) + } + t.mutex.Unlock() + + for _, session := range expiredSessions { + t.processCompletedSession(session) + } +} + +// processCompletedSession saves activity data for a completed streaming session. +func (t *ActivityTracker) processCompletedSession(session *streamSession) { + percentWatched := session.percentWatched() + resumeTime := session.estimatedResumeTime() + + logger.Debugf("[DLNA Activity] Session completed: scene=%d, client=%s, videoDuration=%.1fs, percent=%.1f%%, resume=%.1fs", + session.SceneID, session.ClientIP, session.VideoDuration, percentWatched, resumeTime) + + // Only save if there was meaningful activity (at least 1% watched) + if percentWatched < 1 { + logger.Debugf("[DLNA Activity] Session too short, skipping save") + return + } + + // Skip DB operations if txnManager is nil (for testing) + if t.txnManager == nil { + logger.Debugf("[DLNA Activity] No transaction manager, skipping DB save") + return + } + + // Determine what needs to be saved + shouldSaveResume := resumeTime > 0 + shouldAddView := !session.PlayCountAdded && percentWatched >= float64(t.getMinimumPlayPercent()) + + // Nothing to save + if !shouldSaveResume && !shouldAddView { + return + } + + // Save everything in a single transaction + ctx := context.Background() + if err := txn.WithTxn(ctx, t.txnManager, func(ctx context.Context) error { + // Save resume time only. DLNA clients buffer aggressively and don't report + // playback position, so we can't accurately track play duration - saving + // guesses would corrupt analytics. Resume time is still useful as a + // "continue watching" hint even if imprecise. + if shouldSaveResume { + if _, err := t.sceneWriter.SaveActivity(ctx, session.SceneID, &resumeTime, nil); err != nil { + return fmt.Errorf("save resume time: %w", err) + } + } + + // Increment play count (also updates last_played_at via view date) + if shouldAddView { + if _, err := t.sceneWriter.AddViews(ctx, session.SceneID, []time.Time{time.Now()}); err != nil { + return fmt.Errorf("add view: %w", err) + } + session.PlayCountAdded = true + logger.Debugf("[DLNA Activity] Incremented play count for scene %d (%.1f%% watched)", + session.SceneID, percentWatched) + } + + return nil + }); err != nil { + logger.Warnf("[DLNA Activity] Failed to save activity for scene %d: %v", session.SceneID, err) + } +} + +// isEnabled returns true if activity tracking is enabled. +func (t *ActivityTracker) isEnabled() bool { + if t.config == nil { + return true // Default to enabled + } + return t.config.GetDLNAActivityTrackingEnabled() +} + +// getMinimumPlayPercent returns the minimum play percentage for incrementing play count. +func (t *ActivityTracker) getMinimumPlayPercent() int { + if t.config == nil { + return 0 // Default: any play increments count (matches frontend default) + } + return t.config.GetMinimumPlayPercent() +} diff --git a/internal/dlna/activity_test.go b/internal/dlna/activity_test.go new file mode 100644 index 000000000..19ae7ebb8 --- /dev/null +++ b/internal/dlna/activity_test.go @@ -0,0 +1,420 @@ +package dlna + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// mockSceneWriter is a mock implementation of SceneActivityWriter +type mockSceneWriter struct { + mu sync.Mutex + saveActivityCalls []saveActivityCall + addViewsCalls []addViewsCall +} + +type saveActivityCall struct { + sceneID int + resumeTime *float64 + playDuration *float64 +} + +type addViewsCall struct { + sceneID int + dates []time.Time +} + +func (m *mockSceneWriter) SaveActivity(_ context.Context, sceneID int, resumeTime *float64, playDuration *float64) (bool, error) { + m.mu.Lock() + m.saveActivityCalls = append(m.saveActivityCalls, saveActivityCall{ + sceneID: sceneID, + resumeTime: resumeTime, + playDuration: playDuration, + }) + m.mu.Unlock() + return true, nil +} + +func (m *mockSceneWriter) AddViews(_ context.Context, sceneID int, dates []time.Time) ([]time.Time, error) { + m.mu.Lock() + m.addViewsCalls = append(m.addViewsCalls, addViewsCall{ + sceneID: sceneID, + dates: dates, + }) + m.mu.Unlock() + return dates, nil +} + +// mockConfig is a mock implementation of ActivityConfig +type mockConfig struct { + enabled bool + minPlayPercent int +} + +func (c *mockConfig) GetDLNAActivityTrackingEnabled() bool { + return c.enabled +} + +func (c *mockConfig) GetMinimumPlayPercent() int { + return c.minPlayPercent +} + +func TestStreamSession_PercentWatched(t *testing.T) { + now := time.Now() + + tests := []struct { + name string + startTime time.Time + lastActivity time.Time + videoDuration float64 + expected float64 + }{ + { + name: "no video duration", + startTime: now.Add(-60 * time.Second), + lastActivity: now, + videoDuration: 0, + expected: 0, + }, + { + name: "half watched", + startTime: now.Add(-60 * time.Second), + lastActivity: now, + videoDuration: 120.0, // 2 minutes, watched for 1 minute = 50% + expected: 50.0, + }, + { + name: "fully watched", + startTime: now.Add(-120 * time.Second), + lastActivity: now, + videoDuration: 120.0, // 2 minutes, watched for 2 minutes = 100% + expected: 100.0, + }, + { + name: "quarter watched", + startTime: now.Add(-30 * time.Second), + lastActivity: now, + videoDuration: 120.0, // 2 minutes, watched for 30 seconds = 25% + expected: 25.0, + }, + { + name: "elapsed exceeds duration - capped at 100%", + startTime: now.Add(-180 * time.Second), + lastActivity: now, + videoDuration: 120.0, // 2 minutes, but 3 minutes elapsed = capped at 100% + expected: 100.0, + }, + { + name: "no elapsed time", + startTime: now, + lastActivity: now, + videoDuration: 120.0, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := &streamSession{ + StartTime: tt.startTime, + LastActivity: tt.lastActivity, + VideoDuration: tt.videoDuration, + } + result := session.percentWatched() + assert.InDelta(t, tt.expected, result, 0.01) + }) + } +} + +func TestStreamSession_EstimatedResumeTime(t *testing.T) { + now := time.Now() + + tests := []struct { + name string + startTime time.Time + lastActivity time.Time + videoDuration float64 + expected float64 + }{ + { + name: "no elapsed time", + startTime: now, + lastActivity: now, + videoDuration: 120.0, + expected: 0, + }, + { + name: "half way through", + startTime: now.Add(-60 * time.Second), + lastActivity: now, + videoDuration: 120.0, // 2 minutes, watched for 1 minute = resume at 60s + expected: 60.0, + }, + { + name: "quarter way through", + startTime: now.Add(-30 * time.Second), + lastActivity: now, + videoDuration: 120.0, // 2 minutes, watched for 30 seconds = resume at 30s + expected: 30.0, + }, + { + name: "98% complete - should reset to 0", + startTime: now.Add(-118 * time.Second), + lastActivity: now, + videoDuration: 120.0, // 98.3% elapsed, should reset + expected: 0, + }, + { + name: "100% complete - should reset to 0", + startTime: now.Add(-120 * time.Second), + lastActivity: now, + videoDuration: 120.0, + expected: 0, + }, + { + name: "elapsed exceeds duration - capped and reset to 0", + startTime: now.Add(-180 * time.Second), + lastActivity: now, + videoDuration: 120.0, // 150% elapsed, capped at 100%, reset to 0 + expected: 0, + }, + { + name: "no video duration", + startTime: now.Add(-60 * time.Second), + lastActivity: now, + videoDuration: 0, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := &streamSession{ + StartTime: tt.startTime, + LastActivity: tt.lastActivity, + VideoDuration: tt.videoDuration, + } + result := session.estimatedResumeTime() + assert.InDelta(t, tt.expected, result, 1.0) // Allow 1 second tolerance + }) + } +} + +func TestSessionKey(t *testing.T) { + key := sessionKey("192.168.1.100", 42) + assert.Equal(t, "192.168.1.100:42", key) +} + +func TestActivityTracker_RecordRequest(t *testing.T) { + config := &mockConfig{enabled: true, minPlayPercent: 50} + + // Create tracker without starting the goroutine (for unit testing) + tracker := &ActivityTracker{ + txnManager: nil, // Don't need DB for this test + sceneWriter: nil, + config: config, + sessionTimeout: DefaultSessionTimeout, + sessions: make(map[string]*streamSession), + } + + // Record first request - should create new session + tracker.RecordRequest(42, "192.168.1.100", 120.0) + + tracker.mutex.RLock() + session := tracker.sessions["192.168.1.100:42"] + tracker.mutex.RUnlock() + + assert.NotNil(t, session) + assert.Equal(t, 42, session.SceneID) + assert.Equal(t, "192.168.1.100", session.ClientIP) + assert.Equal(t, 120.0, session.VideoDuration) + assert.False(t, session.StartTime.IsZero()) + assert.False(t, session.LastActivity.IsZero()) + + // Record second request - should update LastActivity + firstActivity := session.LastActivity + time.Sleep(10 * time.Millisecond) + tracker.RecordRequest(42, "192.168.1.100", 120.0) + + tracker.mutex.RLock() + session = tracker.sessions["192.168.1.100:42"] + tracker.mutex.RUnlock() + + assert.True(t, session.LastActivity.After(firstActivity)) +} + +func TestActivityTracker_DisabledTracking(t *testing.T) { + config := &mockConfig{enabled: false, minPlayPercent: 50} + + // Create tracker without starting the goroutine (for unit testing) + tracker := &ActivityTracker{ + txnManager: nil, + sceneWriter: nil, + config: config, + sessionTimeout: DefaultSessionTimeout, + sessions: make(map[string]*streamSession), + } + + // Record request - should be ignored when tracking is disabled + tracker.RecordRequest(42, "192.168.1.100", 120.0) + + tracker.mutex.RLock() + sessionCount := len(tracker.sessions) + tracker.mutex.RUnlock() + + assert.Equal(t, 0, sessionCount) +} + +func TestActivityTracker_SessionExpiration(t *testing.T) { + // For this test, we'll test the session expiration logic directly + // without the full transaction manager integration + + sceneWriter := &mockSceneWriter{} + config := &mockConfig{enabled: true, minPlayPercent: 10} + + // Create a tracker with nil txnManager - we'll test processCompletedSession separately + // Here we just verify the session management logic + tracker := &ActivityTracker{ + txnManager: nil, // Skip DB calls for this test + sceneWriter: sceneWriter, + config: config, + sessionTimeout: 100 * time.Millisecond, + sessions: make(map[string]*streamSession), + } + + // Manually add a session + // Use a short video duration (1 second) so the test can verify expiration quickly. + now := time.Now() + tracker.sessions["192.168.1.100:42"] = &streamSession{ + SceneID: 42, + ClientIP: "192.168.1.100", + StartTime: now.Add(-5 * time.Second), // Started 5 seconds ago + LastActivity: now.Add(-200 * time.Millisecond), // Last activity 200ms ago (> 100ms timeout) + VideoDuration: 1.0, // Short video so timeSinceStart > videoDuration + } + + // Verify session exists + assert.Len(t, tracker.sessions, 1) + + // Process expired sessions - this will try to save activity but txnManager is nil + // so it will skip the DB calls but still remove the session + tracker.processExpiredSessions() + + // Verify session was removed (even though DB calls were skipped) + assert.Len(t, tracker.sessions, 0) +} + +func TestActivityTracker_SessionExpiration_StoppedEarly(t *testing.T) { + // Test that sessions expire when user stops watching early (before video ends) + // This was a bug where sessions wouldn't expire until video duration passed + + config := &mockConfig{enabled: true, minPlayPercent: 10} + tracker := &ActivityTracker{ + txnManager: nil, + sceneWriter: nil, + config: config, + sessionTimeout: 100 * time.Millisecond, + sessions: make(map[string]*streamSession), + } + + // User started watching a 30-minute video but stopped after 5 seconds + now := time.Now() + tracker.sessions["192.168.1.100:42"] = &streamSession{ + SceneID: 42, + ClientIP: "192.168.1.100", + StartTime: now.Add(-5 * time.Second), // Started 5 seconds ago + LastActivity: now.Add(-200 * time.Millisecond), // Last activity 200ms ago (> 100ms timeout) + VideoDuration: 1800.0, // 30 minute video - much longer than elapsed time + } + + assert.Len(t, tracker.sessions, 1) + + // Session should expire because timeSinceActivity > timeout + // Even though the video is 30 minutes and only 5 seconds have passed + tracker.processExpiredSessions() + + // Verify session was expired + assert.Len(t, tracker.sessions, 0, "Session should expire when user stops early, not wait for video duration") +} + +func TestActivityTracker_MinimumPlayPercentThreshold(t *testing.T) { + // Test the threshold logic without full transaction integration + config := &mockConfig{enabled: true, minPlayPercent: 75} // High threshold + + tracker := &ActivityTracker{ + txnManager: nil, + sceneWriter: nil, + config: config, + sessionTimeout: 50 * time.Millisecond, + sessions: make(map[string]*streamSession), + } + + // Test that getMinimumPlayPercent returns the configured value + assert.Equal(t, 75, tracker.getMinimumPlayPercent()) + + // Create a session with 30% watched (36 seconds of a 120 second video) + now := time.Now() + session := &streamSession{ + SceneID: 42, + StartTime: now.Add(-36 * time.Second), + LastActivity: now, + VideoDuration: 120.0, + } + + // 30% is below 75% threshold + percentWatched := session.percentWatched() + assert.InDelta(t, 30.0, percentWatched, 0.1) + assert.False(t, percentWatched >= float64(tracker.getMinimumPlayPercent())) +} + +func TestActivityTracker_MultipleSessions(t *testing.T) { + config := &mockConfig{enabled: true, minPlayPercent: 50} + + // Create tracker without starting the goroutine (for unit testing) + tracker := &ActivityTracker{ + txnManager: nil, + sceneWriter: nil, + config: config, + sessionTimeout: DefaultSessionTimeout, + sessions: make(map[string]*streamSession), + } + + // Different clients watching same scene + tracker.RecordRequest(42, "192.168.1.100", 120.0) + tracker.RecordRequest(42, "192.168.1.101", 120.0) + + // Same client watching different scenes + tracker.RecordRequest(43, "192.168.1.100", 180.0) + + tracker.mutex.RLock() + assert.Len(t, tracker.sessions, 3) + tracker.mutex.RUnlock() +} + +func TestActivityTracker_ShortSessionIgnored(t *testing.T) { + // Test that short sessions are ignored + // Create a session with only ~0.8% watched (1 second of a 120 second video) + now := time.Now() + session := &streamSession{ + SceneID: 42, + ClientIP: "192.168.1.100", + StartTime: now.Add(-1 * time.Second), // Only 1 second + LastActivity: now, + VideoDuration: 120.0, // 2 minutes + } + + // Verify percent watched is below threshold (1s / 120s = 0.83%) + assert.InDelta(t, 0.83, session.percentWatched(), 0.1) + + // Verify elapsed time is short + elapsed := session.LastActivity.Sub(session.StartTime).Seconds() + assert.InDelta(t, 1.0, elapsed, 0.5) + + // Both are below the minimum thresholds (1% and 5 seconds) + percentWatched := session.percentWatched() + shouldSkip := percentWatched < 1 && elapsed < 5 + assert.True(t, shouldSkip, "Short session should be skipped") +} diff --git a/internal/dlna/cms.go b/internal/dlna/cms.go index e4a560462..daf43b382 100644 --- a/internal/dlna/cms.go +++ b/internal/dlna/cms.go @@ -27,7 +27,7 @@ import ( // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -const defaultProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*" +const defaultProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*,http-get:*:image/avif:*" type connectionManagerService struct { *Server diff --git a/internal/dlna/dms.go b/internal/dlna/dms.go index 3b27d607b..d68705f74 100644 --- a/internal/dlna/dms.go +++ b/internal/dlna/dms.go @@ -278,6 +278,7 @@ type Server struct { repository Repository sceneServer sceneServer ipWhitelistManager *ipWhitelistManager + activityTracker *ActivityTracker VideoSortOrder string subscribeLock sync.Mutex @@ -596,6 +597,7 @@ func (me *Server) initMux(mux *http.ServeMux) { mux.HandleFunc(resPath, func(w http.ResponseWriter, r *http.Request) { sceneId := r.URL.Query().Get("scene") var scene *models.Scene + var videoDuration float64 repo := me.repository err := repo.WithReadTxn(r.Context(), func(ctx context.Context) error { sceneIdInt, err := strconv.Atoi(sceneId) @@ -603,6 +605,15 @@ func (me *Server) initMux(mux *http.ServeMux) { return nil } scene, _ = repo.SceneFinder.Find(ctx, sceneIdInt) + if scene != nil { + // Load primary file to get duration for activity tracking + if err := scene.LoadPrimaryFile(ctx, repo.FileGetter); err != nil { + logger.Debugf("failed to load primary file for scene %d: %v", sceneIdInt, err) + } + if f := scene.Files.Primary(); f != nil { + videoDuration = f.Duration + } + } return nil }) if err != nil { @@ -615,6 +626,14 @@ func (me *Server) initMux(mux *http.ServeMux) { w.Header().Set("transferMode.dlna.org", "Streaming") w.Header().Set("contentFeatures.dlna.org", "DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01500000000000000000000000000000") + + // Track activity - uses time-based tracking, updated on each request + if me.activityTracker != nil { + sceneIdInt, _ := strconv.Atoi(sceneId) + clientIP, _, _ := net.SplitHostPort(r.RemoteAddr) + me.activityTracker.RecordRequest(sceneIdInt, clientIP, videoDuration) + } + me.sceneServer.StreamSceneDirect(scene, w, r) }) mux.HandleFunc(rootDescPath, func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/dlna/service.go b/internal/dlna/service.go index 6ef825bac..98715b1e6 100644 --- a/internal/dlna/service.go +++ b/internal/dlna/service.go @@ -77,13 +77,29 @@ type Config interface { GetDLNADefaultIPWhitelist() []string GetVideoSortOrder() string GetDLNAPortAsString() string + GetDLNAActivityTrackingEnabled() bool +} + +// activityConfig wraps Config to implement ActivityConfig. +type activityConfig struct { + config Config + minPlayPercent int // cached from UI config +} + +func (c *activityConfig) GetDLNAActivityTrackingEnabled() bool { + return c.config.GetDLNAActivityTrackingEnabled() +} + +func (c *activityConfig) GetMinimumPlayPercent() int { + return c.minPlayPercent } type Service struct { - repository Repository - config Config - sceneServer sceneServer - ipWhitelistMgr *ipWhitelistManager + repository Repository + config Config + sceneServer sceneServer + ipWhitelistMgr *ipWhitelistManager + activityTracker *ActivityTracker server *Server running bool @@ -155,6 +171,7 @@ func (s *Service) init() error { repository: s.repository, sceneServer: s.sceneServer, ipWhitelistManager: s.ipWhitelistMgr, + activityTracker: s.activityTracker, Interfaces: interfaces, HTTPConn: func() net.Listener { conn, err := net.Listen("tcp", dmsConfig.Http) @@ -215,7 +232,14 @@ func (s *Service) init() error { // } // NewService initialises and returns a new DLNA service. -func NewService(repo Repository, cfg Config, sceneServer sceneServer) *Service { +// The sceneWriter parameter should implement SceneActivityWriter (typically models.SceneReaderWriter). +// The minPlayPercent parameter is the minimum percentage of video that must be played to increment play count. +func NewService(repo Repository, cfg Config, sceneServer sceneServer, sceneWriter SceneActivityWriter, minPlayPercent int) *Service { + activityCfg := &activityConfig{ + config: cfg, + minPlayPercent: minPlayPercent, + } + ret := &Service{ repository: repo, sceneServer: sceneServer, @@ -223,7 +247,8 @@ func NewService(repo Repository, cfg Config, sceneServer sceneServer) *Service { ipWhitelistMgr: &ipWhitelistManager{ config: cfg, }, - mutex: sync.Mutex{}, + activityTracker: NewActivityTracker(repo.TxnManager, sceneWriter, activityCfg), + mutex: sync.Mutex{}, } return ret @@ -283,6 +308,12 @@ func (s *Service) Stop(duration *time.Duration) { if s.running { logger.Info("Stopping DLNA") + + // Stop activity tracker first to process any pending sessions + if s.activityTracker != nil { + s.activityTracker.Stop() + } + err := s.server.Close() if err != nil { logger.Error(err) diff --git a/internal/identify/scene.go b/internal/identify/scene.go index 847a140c5..789674693 100644 --- a/internal/identify/scene.go +++ b/internal/identify/scene.go @@ -153,6 +153,8 @@ func (g sceneRelationships) tags(ctx context.Context) ([]int, error) { tagIDs = originalTagIDs } + endpoint := g.result.source.RemoteSite + for _, t := range scraped { if t.StoredID != nil { // existing tag, just add it @@ -163,10 +165,9 @@ func (g sceneRelationships) tags(ctx context.Context) ([]int, error) { tagIDs = sliceutil.AppendUnique(tagIDs, int(tagID)) } else if createMissing { - newTag := models.NewTag() - newTag.Name = t.Name + newTag := t.ToTag(endpoint, nil) - err := g.tagCreator.Create(ctx, &newTag) + err := g.tagCreator.Create(ctx, newTag) if err != nil { return nil, fmt.Errorf("error creating tag: %w", err) } diff --git a/internal/identify/scene_test.go b/internal/identify/scene_test.go index a76aef516..0eec61c4e 100644 --- a/internal/identify/scene_test.go +++ b/internal/identify/scene_test.go @@ -27,7 +27,7 @@ func Test_sceneRelationships_studio(t *testing.T) { db := mocks.NewDatabase() db.Studio.On("Create", testCtx, mock.Anything).Run(func(args mock.Arguments) { - s := args.Get(1).(*models.Studio) + s := args.Get(1).(*models.CreateStudioInput) s.ID = validStoredIDInt }).Return(nil) diff --git a/internal/identify/studio_test.go b/internal/identify/studio_test.go index 5424a6a93..083675650 100644 --- a/internal/identify/studio_test.go +++ b/internal/identify/studio_test.go @@ -21,13 +21,13 @@ func Test_createMissingStudio(t *testing.T) { db := mocks.NewDatabase() - db.Studio.On("Create", testCtx, mock.MatchedBy(func(p *models.Studio) bool { + db.Studio.On("Create", testCtx, mock.MatchedBy(func(p *models.CreateStudioInput) bool { return p.Name == validName })).Run(func(args mock.Arguments) { - s := args.Get(1).(*models.Studio) + s := args.Get(1).(*models.CreateStudioInput) s.ID = createdID }).Return(nil) - db.Studio.On("Create", testCtx, mock.MatchedBy(func(p *models.Studio) bool { + db.Studio.On("Create", testCtx, mock.MatchedBy(func(p *models.CreateStudioInput) bool { return p.Name == invalidName })).Return(errors.New("error creating studio")) diff --git a/internal/log/logger.go b/internal/log/logger.go index 5f686d32d..cb07121a5 100644 --- a/internal/log/logger.go +++ b/internal/log/logger.go @@ -3,12 +3,14 @@ package log import ( "fmt" + "io" "os" "strings" "sync" "time" "github.com/sirupsen/logrus" + lumberjack "gopkg.in/natefinch/lumberjack.v2" ) type LogItem struct { @@ -41,8 +43,8 @@ func NewLogger() *Logger { } // Init initialises the logger based on a logging configuration -func (log *Logger) Init(logFile string, logOut bool, logLevel string) { - var file *os.File +func (log *Logger) Init(logFile string, logOut bool, logLevel string, logFileMaxSize int) { + var logger io.WriteCloser customFormatter := new(logrus.TextFormatter) customFormatter.TimestampFormat = "2006-01-02 15:04:05" customFormatter.ForceColors = true @@ -57,30 +59,38 @@ func (log *Logger) Init(logFile string, logOut bool, logLevel string) { // the access log colouring not being applied _, _ = customFormatter.Format(logrus.NewEntry(log.logger)) + // if size is 0, disable rotation if logFile != "" { - var err error - file, err = os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - - if err != nil { - fmt.Printf("Could not open '%s' for log output due to error: %s\n", logFile, err.Error()) + if logFileMaxSize == 0 { + var err error + logger, err = os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to open log file %s: %v\n", logFile, err) + } + } else { + logger = &lumberjack.Logger{ + Filename: logFile, + MaxSize: logFileMaxSize, // Megabytes + Compress: true, + } } } - if file != nil { + if logger != nil { if logOut { // log to file separately disabling colours fileFormatter := new(logrus.TextFormatter) fileFormatter.TimestampFormat = customFormatter.TimestampFormat fileFormatter.FullTimestamp = customFormatter.FullTimestamp log.logger.AddHook(&fileLogHook{ - Writer: file, + Writer: logger, Formatter: fileFormatter, }) } else { // logging to file only // turn off the colouring for the file customFormatter.ForceColors = false - log.logger.Out = file + log.logger.Out = logger } } diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index 1b627cbdd..bb99bdcfc 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -16,9 +16,9 @@ import ( "golang.org/x/crypto/bcrypt" - "github.com/knadh/koanf" "github.com/knadh/koanf/parsers/yaml" "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" "github.com/stashapp/stash/internal/identify" "github.com/stashapp/stash/pkg/fsutil" @@ -43,6 +43,9 @@ const ( Password = "password" MaxSessionAge = "max_session_age" + // SFWContentMode mode config key + SFWContentMode = "sfw_content_mode" + FFMpegPath = "ffmpeg_path" FFProbePath = "ffprobe_path" @@ -191,6 +194,7 @@ const ( CSSEnabled = "cssenabled" JavascriptEnabled = "javascriptenabled" CustomLocalesEnabled = "customlocalesenabled" + DisableCustomizations = "disable_customizations" ShowScrubber = "show_scrubber" showScrubberDefault = true @@ -206,6 +210,7 @@ const ( ImageLightboxResetZoomOnNav = "image_lightbox.reset_zoom_on_nav" ImageLightboxScrollModeKey = "image_lightbox.scroll_mode" ImageLightboxScrollAttemptsBeforeChange = "image_lightbox.scroll_attempts_before_change" + ImageLightboxDisableAnimation = "image_lightbox.disable_animation" UI = "ui" @@ -215,6 +220,7 @@ const ( DisableDropdownCreateStudio = "disable_dropdown_create.studio" DisableDropdownCreateTag = "disable_dropdown_create.tag" DisableDropdownCreateMovie = "disable_dropdown_create.movie" + DisableDropdownCreateGallery = "disable_dropdown_create.gallery" HandyKey = "handy_key" FunscriptOffset = "funscript_offset" @@ -249,13 +255,15 @@ const ( DLNAPortDefault = 1338 // Logging options - LogFile = "logfile" - LogOut = "logout" - defaultLogOut = true - LogLevel = "loglevel" - defaultLogLevel = "Info" - LogAccess = "logaccess" - defaultLogAccess = true + LogFile = "logfile" + LogOut = "logout" + defaultLogOut = true + LogLevel = "loglevel" + defaultLogLevel = "Info" + LogAccess = "logaccess" + defaultLogAccess = true + LogFileMaxSize = "logfile_max_size" + defaultLogFileMaxSize = 0 // megabytes, default disabled // Default settings DefaultScanSettings = "defaults.scan_task" @@ -267,6 +275,9 @@ const ( DeleteGeneratedDefault = "defaults.delete_generated" deleteGeneratedDefaultDefault = true + // Trash/Recycle Bin options + DeleteTrashPath = "delete_trash_path" + // Desktop Integration Options NoBrowser = "nobrowser" NoBrowserDefault = false @@ -285,9 +296,9 @@ const ( // slice default values var ( defaultVideoExtensions = []string{"m4v", "mp4", "mov", "wmv", "avi", "mpg", "mpeg", "rmvb", "rm", "flv", "asf", "mkv", "webm", "f4v"} - defaultImageExtensions = []string{"png", "jpg", "jpeg", "gif", "webp"} + defaultImageExtensions = []string{"png", "jpg", "jpeg", "gif", "webp", "avif"} defaultGalleryExtensions = []string{"zip", "cbz"} - defaultMenuItems = []string{"scenes", "images", "movies", "markers", "galleries", "performers", "studios", "tags"} + defaultMenuItems = []string{"scenes", "images", "groups", "markers", "galleries", "performers", "studios", "tags"} ) type MissingConfigError struct { @@ -628,7 +639,15 @@ func (i *Config) getStringMapString(key string) map[string]string { return ret } -// GetStathPaths returns the configured stash library paths. +// GetSFW returns true if SFW mode is enabled. +// Default performer images are changed to more agnostic images when enabled. +func (i *Config) GetSFWContentMode() bool { + i.RLock() + defer i.RUnlock() + return i.getBool(SFWContentMode) +} + +// GetStashPaths returns the configured stash library paths. // Works opposite to the usual case - it will return the override // value only if the main value is not set. func (i *Config) GetStashPaths() StashConfigs { @@ -1280,6 +1299,10 @@ func (i *Config) GetImageLightboxOptions() ConfigImageLightboxResult { if v := i.with(ImageLightboxScrollAttemptsBeforeChange); v != nil { ret.ScrollAttemptsBeforeChange = v.Int(ImageLightboxScrollAttemptsBeforeChange) } + if v := i.with(ImageLightboxDisableAnimation); v != nil { + value := v.Bool(ImageLightboxDisableAnimation) + ret.DisableAnimation = &value + } return ret } @@ -1290,6 +1313,7 @@ func (i *Config) GetDisableDropdownCreate() *ConfigDisableDropdownCreate { Studio: i.getBool(DisableDropdownCreateStudio), Tag: i.getBool(DisableDropdownCreateTag), Movie: i.getBool(DisableDropdownCreateMovie), + Gallery: i.getBool(DisableDropdownCreateGallery), } } @@ -1300,6 +1324,26 @@ func (i *Config) GetUIConfiguration() map[string]interface{} { return i.forKey(UI).Cut(UI).Raw() } +// GetMinimumPlayPercent returns the minimum percentage of a video that must be +// watched before incrementing the play count. Returns 0 if not configured. +func (i *Config) GetMinimumPlayPercent() int { + uiConfig := i.GetUIConfiguration() + if uiConfig == nil { + return 0 + } + if val, ok := uiConfig["minimumPlayPercent"]; ok { + switch v := val.(type) { + case int: + return v + case float64: + return int(v) + case int64: + return int(v) + } + } + return 0 +} + func (i *Config) SetUIConfiguration(v map[string]interface{}) { i.Lock() defer i.Unlock() @@ -1436,6 +1480,13 @@ func (i *Config) GetCustomLocalesEnabled() bool { return i.getBool(CustomLocalesEnabled) } +// GetDisableCustomizations returns true if all customizations (plugins, custom CSS, +// custom JavaScript, and custom locales) should be disabled. This is useful for +// troubleshooting issues without permanently disabling individual customizations. +func (i *Config) GetDisableCustomizations() bool { + return i.getBool(DisableCustomizations) +} + func (i *Config) GetHandyKey() string { return i.getString(HandyKey) } @@ -1456,6 +1507,14 @@ func (i *Config) GetDeleteGeneratedDefault() bool { return i.getBoolDefault(DeleteGeneratedDefault, deleteGeneratedDefaultDefault) } +func (i *Config) GetDeleteTrashPath() string { + return i.getString(DeleteTrashPath) +} + +func (i *Config) SetDeleteTrashPath(value string) { + i.SetString(DeleteTrashPath, value) +} + // GetDefaultIdentifySettings returns the default Identify task settings. // Returns nil if the settings could not be unmarshalled, or if it // has not been set. @@ -1584,6 +1643,22 @@ func (i *Config) GetDLNAPortAsString() string { return ":" + strconv.Itoa(i.GetDLNAPort()) } +// GetDLNAActivityTrackingEnabled returns true if DLNA activity tracking is enabled. +// This uses the same "trackActivity" UI setting that controls frontend play history tracking. +// When enabled, scenes played via DLNA will have their play count and duration tracked. +func (i *Config) GetDLNAActivityTrackingEnabled() bool { + uiConfig := i.GetUIConfiguration() + if uiConfig == nil { + return true // Default to enabled + } + if val, ok := uiConfig["trackActivity"]; ok { + if v, ok := val.(bool); ok { + return v + } + } + return true // Default to enabled +} + // GetVideoSortOrder returns the sort order to display videos. If // empty, videos will be sorted by titles. func (i *Config) GetVideoSortOrder() string { @@ -1625,6 +1700,16 @@ func (i *Config) GetLogAccess() bool { return i.getBoolDefault(LogAccess, defaultLogAccess) } +// GetLogFileMaxSize returns the maximum size of the log file in megabytes for lumberjack to rotate +func (i *Config) GetLogFileMaxSize() int { + value := i.getInt(LogFileMaxSize) + if value < 0 { + value = defaultLogFileMaxSize + } + + return value +} + // Max allowed graphql upload size in megabytes func (i *Config) GetMaxUploadSize() int64 { i.RLock() diff --git a/internal/manager/config/init.go b/internal/manager/config/init.go index 09f1c18bc..840b50b70 100644 --- a/internal/manager/config/init.go +++ b/internal/manager/config/init.go @@ -8,9 +8,9 @@ import ( "path/filepath" "strings" - "github.com/knadh/koanf" "github.com/knadh/koanf/providers/env" "github.com/knadh/koanf/providers/posflag" + "github.com/knadh/koanf/v2" "github.com/spf13/pflag" "github.com/stashapp/stash/pkg/fsutil" diff --git a/internal/manager/config/tasks.go b/internal/manager/config/tasks.go index 0cfabef30..af7d5f674 100644 --- a/internal/manager/config/tasks.go +++ b/internal/manager/config/tasks.go @@ -11,8 +11,10 @@ type ScanMetadataOptions struct { ScanGenerateImagePreviews bool `json:"scanGenerateImagePreviews"` // Generate sprites during scan ScanGenerateSprites bool `json:"scanGenerateSprites"` - // Generate phashes during scan + // Generate video phashes during scan ScanGeneratePhashes bool `json:"scanGeneratePhashes"` + // Generate image phashes during scan + ScanGenerateImagePhashes bool `json:"scanGenerateImagePhashes"` // Generate image thumbnails during scan ScanGenerateThumbnails bool `json:"scanGenerateThumbnails"` // Generate image thumbnails during scan diff --git a/internal/manager/config/ui.go b/internal/manager/config/ui.go index 699091154..de769304f 100644 --- a/internal/manager/config/ui.go +++ b/internal/manager/config/ui.go @@ -13,6 +13,7 @@ type ConfigImageLightboxResult struct { ResetZoomOnNav *bool `json:"resetZoomOnNav"` ScrollMode *ImageLightboxScrollMode `json:"scrollMode"` ScrollAttemptsBeforeChange int `json:"scrollAttemptsBeforeChange"` + DisableAnimation *bool `json:"disableAnimation"` } type ImageLightboxDisplayMode string @@ -104,4 +105,5 @@ type ConfigDisableDropdownCreate struct { Tag bool `json:"tag"` Studio bool `json:"studio"` Movie bool `json:"movie"` + Gallery bool `json:"gallery"` } diff --git a/internal/manager/init.go b/internal/manager/init.go index dd1640ed3..b4af5eab7 100644 --- a/internal/manager/init.go +++ b/internal/manager/init.go @@ -78,7 +78,7 @@ func Initialize(cfg *config.Config, l *log.Logger) (*Manager, error) { } dlnaRepository := dlna.NewRepository(repo) - dlnaService := dlna.NewService(dlnaRepository, cfg, sceneServer) + dlnaService := dlna.NewService(dlnaRepository, cfg, sceneServer, repo.Scene, cfg.GetMinimumPlayPercent()) mgr := &Manager{ Config: cfg, @@ -313,6 +313,7 @@ func (s *Manager) RefreshFFMpeg(ctx context.Context) { s.FFMpeg = ffmpeg.NewEncoder(ffmpegPath) s.FFProbe = ffmpeg.NewFFProbe(ffprobePath) - s.FFMpeg.InitHWSupport(ctx) + // initialise hardware support with background context + s.FFMpeg.InitHWSupport(context.Background()) } } diff --git a/internal/manager/manager.go b/internal/manager/manager.go index 4827a3e3d..f4f3fa636 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -219,8 +219,11 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error { // paths since they must not be relative. The config file property is // resolved to an absolute path when stash is run normally, so convert // relative paths to absolute paths during setup. - configFile, _ := filepath.Abs(input.ConfigLocation) - + // #6287 - this should no longer be necessary since the ffmpeg code + // converts to absolute paths. Converting the config location to + // absolute means that scraper and plugin paths default to absolute + // which we don't want. + configFile := input.ConfigLocation configDir := filepath.Dir(configFile) if exists, _ := fsutil.DirExists(configDir); !exists { @@ -262,6 +265,10 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error { cfg.SetString(config.Cache, input.CacheLocation) } + if input.SFWContentMode { + cfg.SetBool(config.SFWContentMode, true) + } + if input.StoreBlobsInDatabase { cfg.SetInterface(config.BlobsStorage, config.BlobStorageTypeDatabase) } else { @@ -322,6 +329,11 @@ func (s *Manager) BackupDatabase(download bool) (string, string, error) { backupPath = f.Name() backupName = s.Database.DatabaseBackupPath("") f.Close() + + // delete the temp file so that the backup operation can create it + if err := os.Remove(backupPath); err != nil { + return "", "", fmt.Errorf("could not remove temporary backup file %v: %w", backupPath, err) + } } else { backupDir := s.Config.GetBackupDirectoryPathOrDefault() if backupDir != "" { diff --git a/internal/manager/manager_tasks.go b/internal/manager/manager_tasks.go index b85a4c2cf..bac726c1b 100644 --- a/internal/manager/manager_tasks.go +++ b/internal/manager/manager_tasks.go @@ -100,6 +100,8 @@ func (s *Manager) Scan(ctx context.Context, input ScanMetadataInput) (int, error return 0, err } + cfg := config.GetInstance() + scanner := &file.Scanner{ Repository: file.NewRepository(s.Repository), FileDecorators: []file.Decorator{ @@ -118,6 +120,10 @@ func (s *Manager) Scan(ctx context.Context, input ScanMetadataInput) (int, error }, FingerprintCalculator: &fingerprintCalculator{s.Config}, FS: &file.OsFS{}, + ZipFileExtensions: cfg.GetGalleryExtensions(), + // ScanFilters is set in ScanJob.Execute + // HandlerRequiredFilters is set in ScanJob.Execute + Rescan: input.Rescan, } scanJob := ScanJob{ @@ -294,6 +300,7 @@ func (s *Manager) Clean(ctx context.Context, input CleanMetadataInput) int { Handlers: []file.CleanHandler{ &cleanHandler{}, }, + TrashPath: s.Config.GetDeleteTrashPath(), } j := cleanJob{ @@ -364,9 +371,37 @@ func (s *Manager) MigrateHash(ctx context.Context) int { return s.JobManager.Add(ctx, "Migrating scene hashes...", j) } -// If neither ids nor names are set, tag all items +// batchTagType indicates which batch tagging mode to use +type batchTagType int + +const ( + batchTagByIds batchTagType = iota + batchTagByNamesOrStashIds + batchTagAll +) + +// getBatchTagType determines the batch tag mode based on the input +func (input StashBoxBatchTagInput) getBatchTagType(hasPerformerFields bool) batchTagType { + switch { + case len(input.Ids) > 0: + return batchTagByIds + case hasPerformerFields && len(input.PerformerIds) > 0: + return batchTagByIds + case len(input.StashIDs) > 0 || len(input.Names) > 0: + return batchTagByNamesOrStashIds + case hasPerformerFields && len(input.PerformerNames) > 0: + return batchTagByNamesOrStashIds + default: + return batchTagAll + } +} + +// Accepts either ids, or a combination of names and stash_ids. +// If none are set, then all existing items will be tagged. type StashBoxBatchTagInput struct { - // Stash endpoint to use for the tagging - deprecated - use StashBoxEndpoint + // Stash endpoint to use for the tagging + // + // Deprecated: use StashBoxEndpoint Endpoint *int `json:"endpoint"` StashBoxEndpoint *string `json:"stash_box_endpoint"` // Fields to exclude when executing the tagging @@ -375,128 +410,143 @@ type StashBoxBatchTagInput struct { Refresh bool `json:"refresh"` // If batch adding studios, should their parent studios also be created? CreateParent bool `json:"createParent"` - // If set, only tag these ids + // IDs in stash of the items to update. + // If set, names and stash_ids fields will be ignored. Ids []string `json:"ids"` - // If set, only tag these names + // Names of the items in the stash-box instance to search for and create Names []string `json:"names"` - // If set, only tag these performer ids + // Stash IDs of the items in the stash-box instance to search for and create + StashIDs []string `json:"stash_ids"` + // IDs in stash of the performers to update // - // Deprecated: please use Ids + // Deprecated: use Ids PerformerIds []string `json:"performer_ids"` - // If set, only tag these performer names + // Names of the performers in the stash-box instance to search for and create // - // Deprecated: please use Names + // Deprecated: use Names PerformerNames []string `json:"performer_names"` } +func (s *Manager) batchTagPerformersByIds(ctx context.Context, input StashBoxBatchTagInput, box *models.StashBox) ([]Task, error) { + var tasks []Task + + err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { + performerQuery := s.Repository.Performer + + ids := input.Ids + if len(ids) == 0 { + ids = input.PerformerIds //nolint:staticcheck + } + + for _, performerID := range ids { + if id, err := strconv.Atoi(performerID); err == nil { + performer, err := performerQuery.Find(ctx, id) + if err != nil { + return err + } + + if err := performer.LoadStashIDs(ctx, performerQuery); err != nil { + return fmt.Errorf("loading performer stash ids: %w", err) + } + + hasStashID := performer.StashIDs.ForEndpoint(box.Endpoint) != nil + if (input.Refresh && hasStashID) || (!input.Refresh && !hasStashID) { + tasks = append(tasks, &stashBoxBatchPerformerTagTask{ + performer: performer, + box: box, + excludedFields: input.ExcludeFields, + }) + } + } + } + return nil + }) + + return tasks, err +} + +func (s *Manager) batchTagPerformersByNamesOrStashIds(input StashBoxBatchTagInput, box *models.StashBox) []Task { + var tasks []Task + + for i := range input.StashIDs { + stashID := input.StashIDs[i] + if len(stashID) > 0 { + tasks = append(tasks, &stashBoxBatchPerformerTagTask{ + stashID: &stashID, + box: box, + excludedFields: input.ExcludeFields, + }) + } + } + + names := input.Names + if len(names) == 0 { + names = input.PerformerNames //nolint:staticcheck + } + + for i := range names { + name := names[i] + if len(name) > 0 { + tasks = append(tasks, &stashBoxBatchPerformerTagTask{ + name: &name, + box: box, + excludedFields: input.ExcludeFields, + }) + } + } + + return tasks +} + +func (s *Manager) batchTagAllPerformers(ctx context.Context, input StashBoxBatchTagInput, box *models.StashBox) ([]Task, error) { + var tasks []Task + + err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { + performerQuery := s.Repository.Performer + var performers []*models.Performer + var err error + + performers, err = performerQuery.FindByStashIDStatus(ctx, input.Refresh, box.Endpoint) + + if err != nil { + return fmt.Errorf("error querying performers: %v", err) + } + + for _, performer := range performers { + if err := performer.LoadStashIDs(ctx, performerQuery); err != nil { + return fmt.Errorf("error loading stash ids for performer %s: %v", performer.Name, err) + } + + tasks = append(tasks, &stashBoxBatchPerformerTagTask{ + performer: performer, + box: box, + excludedFields: input.ExcludeFields, + }) + } + return nil + }) + + return tasks, err +} + func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, box *models.StashBox, input StashBoxBatchTagInput) int { j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error { logger.Infof("Initiating stash-box batch performer tag") - var tasks []StashBoxBatchTagTask + var tasks []Task + var err error - // The gocritic linter wants to turn this ifElseChain into a switch. - // however, such a switch would contain quite large blocks for each section - // and would arguably be hard to read. - // - // This is why we mark this section nolint. In principle, we should look to - // rewrite the section at some point, to avoid the linter warning. - if len(input.Ids) > 0 || len(input.PerformerIds) > 0 { //nolint:gocritic - // The user has chosen only to tag the items on the current page - if err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { - performerQuery := s.Repository.Performer + switch input.getBatchTagType(true) { + case batchTagByIds: + tasks, err = s.batchTagPerformersByIds(ctx, input, box) + case batchTagByNamesOrStashIds: + tasks = s.batchTagPerformersByNamesOrStashIds(input, box) + case batchTagAll: + tasks, err = s.batchTagAllPerformers(ctx, input, box) + } - idsToUse := input.PerformerIds - if len(input.Ids) > 0 { - idsToUse = input.Ids - } - - for _, performerID := range idsToUse { - if id, err := strconv.Atoi(performerID); err == nil { - performer, err := performerQuery.Find(ctx, id) - if err == nil { - if err := performer.LoadStashIDs(ctx, performerQuery); err != nil { - return fmt.Errorf("loading performer stash ids: %w", err) - } - - // Check if the user wants to refresh existing or new items - hasStashID := performer.StashIDs.ForEndpoint(box.Endpoint) != nil - if (input.Refresh && hasStashID) || (!input.Refresh && !hasStashID) { - tasks = append(tasks, StashBoxBatchTagTask{ - performer: performer, - refresh: input.Refresh, - box: box, - excludedFields: input.ExcludeFields, - taskType: Performer, - }) - } - } else { - return err - } - } - } - return nil - }); err != nil { - return err - } - } else if len(input.Names) > 0 || len(input.PerformerNames) > 0 { - // The user is batch adding performers - namesToUse := input.PerformerNames - if len(input.Names) > 0 { - namesToUse = input.Names - } - - for i := range namesToUse { - name := namesToUse[i] - if len(name) > 0 { - tasks = append(tasks, StashBoxBatchTagTask{ - name: &name, - refresh: false, - box: box, - excludedFields: input.ExcludeFields, - taskType: Performer, - }) - } - } - } else { //nolint:gocritic - // The gocritic linter wants to fold this if-block into the else on the line above. - // However, this doesn't really help with readability of the current section. Mark it - // as nolint for now. In the future we'd like to rewrite this code by factoring some of - // this into separate functions. - - // The user has chosen to tag every item in their database - if err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { - performerQuery := s.Repository.Performer - var performers []*models.Performer - var err error - - if input.Refresh { - performers, err = performerQuery.FindByStashIDStatus(ctx, true, box.Endpoint) - } else { - performers, err = performerQuery.FindByStashIDStatus(ctx, false, box.Endpoint) - } - - if err != nil { - return fmt.Errorf("error querying performers: %v", err) - } - - for _, performer := range performers { - if err := performer.LoadStashIDs(ctx, performerQuery); err != nil { - return fmt.Errorf("error loading stash ids for performer %s: %v", performer.Name, err) - } - - tasks = append(tasks, StashBoxBatchTagTask{ - performer: performer, - refresh: input.Refresh, - box: box, - excludedFields: input.ExcludeFields, - taskType: Performer, - }) - } - return nil - }); err != nil { - return err - } + if err != nil { + return err } if len(tasks) == 0 { @@ -508,7 +558,7 @@ func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, box *models.Sta logger.Infof("Starting stash-box batch operation for %d performers", len(tasks)) for _, task := range tasks { - progress.ExecuteTask(task.Description(), func() { + progress.ExecuteTask(task.GetDescription(), func() { task.Start(ctx) }) @@ -521,103 +571,116 @@ func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, box *models.Sta return s.JobManager.Add(ctx, "Batch stash-box performer tag...", j) } +func (s *Manager) batchTagStudiosByIds(ctx context.Context, input StashBoxBatchTagInput, box *models.StashBox) ([]Task, error) { + var tasks []Task + + err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { + studioQuery := s.Repository.Studio + + for _, studioID := range input.Ids { + if id, err := strconv.Atoi(studioID); err == nil { + studio, err := studioQuery.Find(ctx, id) + if err != nil { + return err + } + + if err := studio.LoadStashIDs(ctx, studioQuery); err != nil { + return fmt.Errorf("loading studio stash ids: %w", err) + } + + hasStashID := studio.StashIDs.ForEndpoint(box.Endpoint) != nil + if (input.Refresh && hasStashID) || (!input.Refresh && !hasStashID) { + tasks = append(tasks, &stashBoxBatchStudioTagTask{ + studio: studio, + createParent: input.CreateParent, + box: box, + excludedFields: input.ExcludeFields, + }) + } + } + } + return nil + }) + + return tasks, err +} + +func (s *Manager) batchTagStudiosByNamesOrStashIds(input StashBoxBatchTagInput, box *models.StashBox) []Task { + var tasks []Task + + for i := range input.StashIDs { + stashID := input.StashIDs[i] + if len(stashID) > 0 { + tasks = append(tasks, &stashBoxBatchStudioTagTask{ + stashID: &stashID, + createParent: input.CreateParent, + box: box, + excludedFields: input.ExcludeFields, + }) + } + } + + for i := range input.Names { + name := input.Names[i] + if len(name) > 0 { + tasks = append(tasks, &stashBoxBatchStudioTagTask{ + name: &name, + createParent: input.CreateParent, + box: box, + excludedFields: input.ExcludeFields, + }) + } + } + + return tasks +} + +func (s *Manager) batchTagAllStudios(ctx context.Context, input StashBoxBatchTagInput, box *models.StashBox) ([]Task, error) { + var tasks []Task + + err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { + studioQuery := s.Repository.Studio + var studios []*models.Studio + var err error + + studios, err = studioQuery.FindByStashIDStatus(ctx, input.Refresh, box.Endpoint) + + if err != nil { + return fmt.Errorf("error querying studios: %v", err) + } + + for _, studio := range studios { + tasks = append(tasks, &stashBoxBatchStudioTagTask{ + studio: studio, + createParent: input.CreateParent, + box: box, + excludedFields: input.ExcludeFields, + }) + } + return nil + }) + + return tasks, err +} + func (s *Manager) StashBoxBatchStudioTag(ctx context.Context, box *models.StashBox, input StashBoxBatchTagInput) int { j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error { logger.Infof("Initiating stash-box batch studio tag") - var tasks []StashBoxBatchTagTask + var tasks []Task + var err error - // The gocritic linter wants to turn this ifElseChain into a switch. - // however, such a switch would contain quite large blocks for each section - // and would arguably be hard to read. - // - // This is why we mark this section nolint. In principle, we should look to - // rewrite the section at some point, to avoid the linter warning. - if len(input.Ids) > 0 { //nolint:gocritic - // The user has chosen only to tag the items on the current page - if err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { - studioQuery := s.Repository.Studio + switch input.getBatchTagType(false) { + case batchTagByIds: + tasks, err = s.batchTagStudiosByIds(ctx, input, box) + case batchTagByNamesOrStashIds: + tasks = s.batchTagStudiosByNamesOrStashIds(input, box) + case batchTagAll: + tasks, err = s.batchTagAllStudios(ctx, input, box) + } - for _, studioID := range input.Ids { - if id, err := strconv.Atoi(studioID); err == nil { - studio, err := studioQuery.Find(ctx, id) - if err == nil { - if err := studio.LoadStashIDs(ctx, studioQuery); err != nil { - return fmt.Errorf("loading studio stash ids: %w", err) - } - - // Check if the user wants to refresh existing or new items - hasStashID := studio.StashIDs.ForEndpoint(box.Endpoint) != nil - if (input.Refresh && hasStashID) || (!input.Refresh && !hasStashID) { - tasks = append(tasks, StashBoxBatchTagTask{ - studio: studio, - refresh: input.Refresh, - createParent: input.CreateParent, - box: box, - excludedFields: input.ExcludeFields, - taskType: Studio, - }) - } - } else { - return err - } - } - } - return nil - }); err != nil { - logger.Error(err.Error()) - } - } else if len(input.Names) > 0 { - // The user is batch adding studios - for i := range input.Names { - name := input.Names[i] - if len(name) > 0 { - tasks = append(tasks, StashBoxBatchTagTask{ - name: &name, - refresh: false, - createParent: input.CreateParent, - box: box, - excludedFields: input.ExcludeFields, - taskType: Studio, - }) - } - } - } else { //nolint:gocritic - // The gocritic linter wants to fold this if-block into the else on the line above. - // However, this doesn't really help with readability of the current section. Mark it - // as nolint for now. In the future we'd like to rewrite this code by factoring some of - // this into separate functions. - - // The user has chosen to tag every item in their database - if err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { - studioQuery := s.Repository.Studio - var studios []*models.Studio - var err error - - if input.Refresh { - studios, err = studioQuery.FindByStashIDStatus(ctx, true, box.Endpoint) - } else { - studios, err = studioQuery.FindByStashIDStatus(ctx, false, box.Endpoint) - } - - if err != nil { - return fmt.Errorf("error querying studios: %v", err) - } - - for _, studio := range studios { - tasks = append(tasks, StashBoxBatchTagTask{ - studio: studio, - refresh: input.Refresh, - createParent: input.CreateParent, - box: box, - excludedFields: input.ExcludeFields, - taskType: Studio, - }) - } - return nil - }); err != nil { - return err - } + if err != nil { + return err } if len(tasks) == 0 { @@ -629,7 +692,7 @@ func (s *Manager) StashBoxBatchStudioTag(ctx context.Context, box *models.StashB logger.Infof("Starting stash-box batch operation for %d studios", len(tasks)) for _, task := range tasks { - progress.ExecuteTask(task.Description(), func() { + progress.ExecuteTask(task.GetDescription(), func() { task.Start(ctx) }) diff --git a/internal/manager/models.go b/internal/manager/models.go index 3e96e6182..b7c7232c5 100644 --- a/internal/manager/models.go +++ b/internal/manager/models.go @@ -21,6 +21,7 @@ type SetupInput struct { // Empty to indicate $HOME/.stash/config.yml default ConfigLocation string `json:"configLocation"` Stashes []*config.StashConfigInput `json:"stashes"` + SFWContentMode bool `json:"sfwContentMode"` // Empty to indicate default DatabaseFile string `json:"databaseFile"` // Empty to indicate default diff --git a/internal/manager/repository.go b/internal/manager/repository.go index 8d4ef1137..e51e737ee 100644 --- a/internal/manager/repository.go +++ b/internal/manager/repository.go @@ -13,14 +13,14 @@ type SceneService interface { Create(ctx context.Context, input *models.Scene, fileIDs []models.FileID, coverImage []byte) (*models.Scene, error) AssignFile(ctx context.Context, sceneID int, fileID models.FileID) error Merge(ctx context.Context, sourceIDs []int, destinationID int, fileDeleter *scene.FileDeleter, options scene.MergeOptions) error - Destroy(ctx context.Context, scene *models.Scene, fileDeleter *scene.FileDeleter, deleteGenerated, deleteFile bool) error + Destroy(ctx context.Context, scene *models.Scene, fileDeleter *scene.FileDeleter, deleteGenerated, deleteFile, destroyFileEntry bool) error FindByIDs(ctx context.Context, ids []int, load ...scene.LoadRelationshipOption) ([]*models.Scene, error) sceneFingerprintGetter } type ImageService interface { - Destroy(ctx context.Context, image *models.Image, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) error + Destroy(ctx context.Context, image *models.Image, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile, destroyFileEntry bool) error DestroyZipImages(ctx context.Context, zipFile models.File, fileDeleter *image.FileDeleter, deleteGenerated bool) ([]*models.Image, error) } @@ -31,7 +31,7 @@ type GalleryService interface { SetCover(ctx context.Context, g *models.Gallery, coverImageId int) error ResetCover(ctx context.Context, g *models.Gallery) error - Destroy(ctx context.Context, i *models.Gallery, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) ([]*models.Image, error) + Destroy(ctx context.Context, i *models.Gallery, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile, destroyFileEntry bool) ([]*models.Image, error) ValidateImageGalleryChange(ctx context.Context, i *models.Image, updateIDs models.UpdateIDs) error diff --git a/internal/manager/running_streams.go b/internal/manager/running_streams.go index c6b0c4665..18ac3b042 100644 --- a/internal/manager/running_streams.go +++ b/internal/manager/running_streams.go @@ -3,7 +3,9 @@ package manager import ( "context" "errors" + "mime" "net/http" + "path/filepath" "github.com/stashapp/stash/internal/manager/config" "github.com/stashapp/stash/internal/static" @@ -46,14 +48,17 @@ func (s *SceneServer) StreamSceneDirect(scene *models.Scene, w http.ResponseWrit sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()) - filepath := GetInstance().Paths.Scene.GetStreamPath(scene.Path, sceneHash) + fp := GetInstance().Paths.Scene.GetStreamPath(scene.Path, sceneHash) streamRequestCtx := ffmpeg.NewStreamRequestContext(w, r) // #2579 - hijacking and closing the connection here causes video playback to fail in Safari // We trust that the request context will be closed, so we don't need to call Cancel on the // returned context here. - _ = GetInstance().ReadLockManager.ReadLock(streamRequestCtx, filepath) - http.ServeFile(w, r, filepath) + _ = GetInstance().ReadLockManager.ReadLock(streamRequestCtx, fp) + _, filename := filepath.Split(fp) + contentDisposition := mime.FormatMediaType("inline", map[string]string{"filename": filename}) + w.Header().Set("Content-Disposition", contentDisposition) + http.ServeFile(w, r, fp) } func (s *SceneServer) ServeScreenshot(scene *models.Scene, w http.ResponseWriter, r *http.Request) { diff --git a/internal/manager/task_clean.go b/internal/manager/task_clean.go index 9690cf4c8..ddd86e2f2 100644 --- a/internal/manager/task_clean.go +++ b/internal/manager/task_clean.go @@ -300,7 +300,10 @@ func (h *cleanHandler) handleRelatedScenes(ctx context.Context, fileDeleter *fil // only delete if the scene has no other files if len(scene.Files.List()) <= 1 { logger.Infof("Deleting scene %q since it has no other related files", scene.DisplayName()) - if err := mgr.SceneService.Destroy(ctx, scene, sceneFileDeleter, true, false); err != nil { + const deleteGenerated = true + const deleteFile = false + const destroyFileEntry = false + if err := mgr.SceneService.Destroy(ctx, scene, sceneFileDeleter, deleteGenerated, deleteFile, destroyFileEntry); err != nil { return err } @@ -421,7 +424,10 @@ func (h *cleanHandler) handleRelatedImages(ctx context.Context, fileDeleter *fil if len(i.Files.List()) <= 1 { logger.Infof("Deleting image %q since it has no other related files", i.DisplayName()) - if err := mgr.ImageService.Destroy(ctx, i, imageFileDeleter, true, false); err != nil { + const deleteGenerated = true + const deleteFile = false + const destroyFileEntry = false + if err := mgr.ImageService.Destroy(ctx, i, imageFileDeleter, deleteGenerated, deleteFile, destroyFileEntry); err != nil { return err } diff --git a/internal/manager/task_generate.go b/internal/manager/task_generate.go index c28ffe55b..2b330bcf3 100644 --- a/internal/manager/task_generate.go +++ b/internal/manager/task_generate.go @@ -29,6 +29,7 @@ type GenerateMetadataInput struct { // Generate transcodes even if not required ForceTranscodes bool `json:"forceTranscodes"` Phashes bool `json:"phashes"` + ImagePhashes bool `json:"imagePhashes"` InteractiveHeatmapsSpeeds bool `json:"interactiveHeatmapsSpeeds"` ClipPreviews bool `json:"clipPreviews"` ImageThumbnails bool `json:"imageThumbnails"` @@ -36,6 +37,10 @@ type GenerateMetadataInput struct { SceneIDs []string `json:"sceneIDs"` // marker ids to generate for MarkerIDs []string `json:"markerIDs"` + // image ids to generate for + ImageIDs []string `json:"imageIDs"` + // gallery ids to generate for + GalleryIDs []string `json:"galleryIDs"` // overwrite existing media Overwrite bool `json:"overwrite"` } @@ -73,6 +78,7 @@ type totalsGenerate struct { markers int64 transcodes int64 phashes int64 + imagePhashes int64 interactiveHeatmapSpeeds int64 clipPreviews int64 imageThumbnails int64 @@ -82,8 +88,9 @@ type totalsGenerate struct { func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) error { var scenes []*models.Scene - var err error var markers []*models.SceneMarker + var images []*models.Image + var err error j.overwrite = j.input.Overwrite j.fileNamingAlgo = config.GetInstance().GetVideoFileNamingAlgorithm() @@ -105,6 +112,14 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) error if err != nil { logger.Error(err.Error()) } + imageIDs, err := stringslice.StringSliceToIntSlice(j.input.ImageIDs) + if err != nil { + logger.Error(err.Error()) + } + galleryIDs, err := stringslice.StringSliceToIntSlice(j.input.GalleryIDs) + if err != nil { + logger.Error(err.Error()) + } g := &generate.Generator{ Encoder: instance.FFMpeg, @@ -118,7 +133,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) error r := j.repository if err := r.WithReadTxn(ctx, func(ctx context.Context) error { qb := r.Scene - if len(j.input.SceneIDs) == 0 && len(j.input.MarkerIDs) == 0 { + if len(j.input.SceneIDs) == 0 && len(j.input.MarkerIDs) == 0 && len(j.input.ImageIDs) == 0 && len(j.input.GalleryIDs) == 0 { j.queueTasks(ctx, g, queue) } else { if len(j.input.SceneIDs) > 0 { @@ -141,6 +156,33 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) error j.queueMarkerJob(g, m, queue) } } + + if len(j.input.ImageIDs) > 0 { + images, err = r.Image.FindMany(ctx, imageIDs) + for _, i := range images { + if err := i.LoadFiles(ctx, r.Image); err != nil { + return err + } + + j.queueImageJob(g, i, queue) + } + } + + if len(j.input.GalleryIDs) > 0 { + for _, galleryID := range galleryIDs { + imgs, err := r.Image.FindByGalleryID(ctx, galleryID) + if err != nil { + return err + } + for _, img := range imgs { + if err := img.LoadFiles(ctx, r.Image); err != nil { + return err + } + + j.queueImageJob(g, img, queue) + } + } + } } return nil @@ -172,6 +214,9 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) error if j.input.Phashes { logMsg += fmt.Sprintf(" %d phashes", totals.phashes) } + if j.input.ImagePhashes { + logMsg += fmt.Sprintf(" %d image phashes", totals.imagePhashes) + } if j.input.InteractiveHeatmapsSpeeds { logMsg += fmt.Sprintf(" %d heatmaps & speeds", totals.interactiveHeatmapSpeeds) } @@ -284,7 +329,7 @@ func (j *GenerateJob) queueImagesTasks(ctx context.Context, g *generate.Generato r := j.repository - for more := j.input.ClipPreviews || j.input.ImageThumbnails; more; { + for more := j.input.ClipPreviews || j.input.ImageThumbnails || j.input.ImagePhashes; more; { if job.IsCancelled(ctx) { return } @@ -411,12 +456,13 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator, } } - if j.input.Markers { + if j.input.Markers || j.input.MarkerImagePreviews || j.input.MarkerScreenshots { task := &GenerateMarkersTask{ repository: r, Scene: scene, Overwrite: j.overwrite, fileNamingAlgorithm: j.fileNamingAlgo, + VideoPreview: j.input.Markers, ImagePreview: j.input.MarkerImagePreviews, Screenshot: j.input.MarkerScreenshots, @@ -488,6 +534,9 @@ func (j *GenerateJob) queueMarkerJob(g *generate.Generator, marker *models.Scene Marker: marker, Overwrite: j.overwrite, fileNamingAlgorithm: j.fileNamingAlgo, + VideoPreview: j.input.Markers, + ImagePreview: j.input.MarkerImagePreviews, + Screenshot: j.input.MarkerScreenshots, generator: g, } j.totals.markers++ @@ -521,4 +570,23 @@ func (j *GenerateJob) queueImageJob(g *generate.Generator, image *models.Image, queue <- task } } + + if j.input.ImagePhashes { + // generate for all files in image + for _, f := range image.Files.List() { + if imageFile, ok := f.(*models.ImageFile); ok { + task := &GenerateImagePhashTask{ + repository: j.repository, + File: imageFile, + Overwrite: j.overwrite, + } + + if task.required() { + j.totals.imagePhashes++ + j.totals.tasks++ + queue <- task + } + } + } + } } diff --git a/internal/manager/task_generate_image_phash.go b/internal/manager/task_generate_image_phash.go new file mode 100644 index 000000000..4c07ffadf --- /dev/null +++ b/internal/manager/task_generate_image_phash.go @@ -0,0 +1,103 @@ +package manager + +import ( + "context" + "fmt" + + "github.com/stashapp/stash/pkg/hash/imagephash" + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/models" +) + +type GenerateImagePhashTask struct { + repository models.Repository + File *models.ImageFile + Overwrite bool +} + +func (t *GenerateImagePhashTask) GetDescription() string { + return fmt.Sprintf("Generating phash for %s", t.File.Path) +} + +func (t *GenerateImagePhashTask) Start(ctx context.Context) { + if !t.required() { + return + } + + var hash int64 + set := false + + // #4393 - if there is a file with the same md5, we can use the same phash + // only use this if we're not overwriting + if !t.Overwrite { + existing, err := t.findExistingPhash(ctx) + if err != nil { + logger.Warnf("Error finding existing phash: %v", err) + } else if existing != nil { + logger.Infof("Using existing phash for %s", t.File.Path) + hash = existing.(int64) + set = true + } + } + + if !set { + generated, err := imagephash.Generate(t.File) + if err != nil { + logger.Errorf("Error generating phash for %q: %v", t.File.Path, err) + logErrorOutput(err) + return + } + + hash = int64(*generated) + } + + r := t.repository + if err := r.WithTxn(ctx, func(ctx context.Context) error { + t.File.Fingerprints = t.File.Fingerprints.AppendUnique(models.Fingerprint{ + Type: models.FingerprintTypePhash, + Fingerprint: hash, + }) + + return r.File.Update(ctx, t.File) + }); err != nil && ctx.Err() == nil { + logger.Errorf("Error setting phash: %v", err) + } +} + +func (t *GenerateImagePhashTask) findExistingPhash(ctx context.Context) (interface{}, error) { + r := t.repository + var ret interface{} + if err := r.WithReadTxn(ctx, func(ctx context.Context) error { + md5 := t.File.Fingerprints.Get(models.FingerprintTypeMD5) + + // find other files with the same md5 + files, err := r.File.FindByFingerprint(ctx, models.Fingerprint{ + Type: models.FingerprintTypeMD5, + Fingerprint: md5, + }) + if err != nil { + return fmt.Errorf("finding files by md5: %w", err) + } + + // find the first file with a phash + for _, file := range files { + if phash := file.Base().Fingerprints.Get(models.FingerprintTypePhash); phash != nil { + ret = phash + return nil + } + } + return nil + }); err != nil { + return nil, err + } + + return ret, nil +} + +func (t *GenerateImagePhashTask) required() bool { + if t.Overwrite { + return true + } + + return t.File.Fingerprints.Get(models.FingerprintTypePhash) == nil +} diff --git a/internal/manager/task_generate_image_thumbnail.go b/internal/manager/task_generate_image_thumbnail.go index 2d32e2d60..14518d2bb 100644 --- a/internal/manager/task_generate_image_thumbnail.go +++ b/internal/manager/task_generate_image_thumbnail.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "os/exec" "github.com/stashapp/stash/pkg/fsutil" "github.com/stashapp/stash/pkg/image" @@ -20,6 +21,13 @@ func (t *GenerateImageThumbnailTask) GetDescription() string { return fmt.Sprintf("Generating Thumbnail for image %s", t.Image.Path) } +func (t *GenerateImageThumbnailTask) logStderr(err error) { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + logger.Debugf("[generator] error output: %s", exitErr.Stderr) + } +} + func (t *GenerateImageThumbnailTask) Start(ctx context.Context) { if !t.required() { return @@ -46,14 +54,15 @@ func (t *GenerateImageThumbnailTask) Start(ctx context.Context) { if err != nil { // don't log for animated images if !errors.Is(err, image.ErrNotSupportedForThumbnail) { - logger.Errorf("[generator] getting thumbnail for image %s: %w", path, err) + logger.Errorf("[generator] getting thumbnail for image %s: %s", path, err.Error()) + t.logStderr(err) } return } err = fsutil.WriteFile(thumbPath, data) if err != nil { - logger.Errorf("[generator] writing thumbnail for image %s: %w", path, err) + logger.Errorf("[generator] writing thumbnail for image %s: %s", path, err.Error()) return } } diff --git a/internal/manager/task_generate_markers.go b/internal/manager/task_generate_markers.go index f37c7aed1..1da458ba8 100644 --- a/internal/manager/task_generate_markers.go +++ b/internal/manager/task_generate_markers.go @@ -18,6 +18,7 @@ type GenerateMarkersTask struct { Overwrite bool fileNamingAlgorithm models.HashAlgorithm + VideoPreview bool ImagePreview bool Screenshot bool @@ -107,11 +108,19 @@ func (t *GenerateMarkersTask) generateMarker(videoFile *models.VideoFile, scene sceneHash := scene.GetHash(t.fileNamingAlgorithm) seconds := float64(sceneMarker.Seconds) + // check if marker past duration + if seconds > float64(videoFile.Duration) { + logger.Warnf("[generator] scene marker at %.2f seconds exceeds video duration of %.2f seconds, skipping", seconds, float64(videoFile.Duration)) + return + } + g := t.generator - if err := g.MarkerPreviewVideo(context.TODO(), videoFile.Path, sceneHash, seconds, sceneMarker.EndSeconds, instance.Config.GetPreviewAudio()); err != nil { - logger.Errorf("[generator] failed to generate marker video: %v", err) - logErrorOutput(err) + if t.VideoPreview { + if err := g.MarkerPreviewVideo(context.TODO(), videoFile.Path, sceneHash, seconds, sceneMarker.EndSeconds, instance.Config.GetPreviewAudio()); err != nil { + logger.Errorf("[generator] failed to generate marker video: %v", err) + logErrorOutput(err) + } } if t.ImagePreview { @@ -158,7 +167,7 @@ func (t *GenerateMarkersTask) markerExists(sceneChecksum string, seconds int) bo return false } - videoExists := t.videoExists(sceneChecksum, seconds) + videoExists := !t.VideoPreview || t.videoExists(sceneChecksum, seconds) imageExists := !t.ImagePreview || t.imageExists(sceneChecksum, seconds) screenshotExists := !t.Screenshot || t.screenshotExists(sceneChecksum, seconds) diff --git a/internal/manager/task_generate_phash.go b/internal/manager/task_generate_phash.go index 54dc1a10b..5d35a8738 100644 --- a/internal/manager/task_generate_phash.go +++ b/internal/manager/task_generate_phash.go @@ -44,7 +44,7 @@ func (t *GeneratePhashTask) Start(ctx context.Context) { if !set { generated, err := videophash.Generate(instance.FFMpeg, t.File) if err != nil { - logger.Errorf("Error generating phash: %v", err) + logger.Errorf("Error generating phash for %q: %v", t.File.Path, err) logErrorOutput(err) return } diff --git a/internal/manager/task_generate_screenshot.go b/internal/manager/task_generate_screenshot.go index 77ad2be34..2f4031586 100644 --- a/internal/manager/task_generate_screenshot.go +++ b/internal/manager/task_generate_screenshot.go @@ -32,6 +32,7 @@ func (t *GenerateCoverTask) Start(ctx context.Context) { return t.Scene.LoadPrimaryFile(ctx, r.File) }); err != nil { logger.Error(err) + return } if !required { diff --git a/internal/manager/task_scan.go b/internal/manager/task_scan.go index 6f7f34b3c..d09765577 100644 --- a/internal/manager/task_scan.go +++ b/internal/manager/task_scan.go @@ -2,13 +2,17 @@ package manager import ( "context" + "errors" "fmt" "io/fs" "path/filepath" "regexp" + "runtime/debug" + "sync" "time" "github.com/99designs/gqlgen/graphql/handler/lru" + "github.com/remeh/sizedwaitgroup" "github.com/stashapp/stash/internal/manager/config" "github.com/stashapp/stash/pkg/file" "github.com/stashapp/stash/pkg/file/video" @@ -24,14 +28,13 @@ import ( "github.com/stashapp/stash/pkg/txn" ) -type scanner interface { - Scan(ctx context.Context, handlers []file.Handler, options file.ScanOptions, progressReporter file.ProgressReporter) -} - type ScanJob struct { - scanner scanner + scanner *file.Scanner input ScanMetadataInput subscriptions *subscriptionManager + + fileQueue chan file.ScannedFile + count int } func (j *ScanJob) Execute(ctx context.Context, progress *job.Progress) error { @@ -55,22 +58,22 @@ func (j *ScanJob) Execute(ctx context.Context, progress *job.Progress) error { start := time.Now() + nTasks := cfg.GetParallelTasksWithAutoDetection() + const taskQueueSize = 200000 - taskQueue := job.NewTaskQueue(ctx, progress, taskQueueSize, cfg.GetParallelTasksWithAutoDetection()) + taskQueue := job.NewTaskQueue(ctx, progress, taskQueueSize, nTasks) var minModTime time.Time if j.input.Filter != nil && j.input.Filter.MinModTime != nil { minModTime = *j.input.Filter.MinModTime } - j.scanner.Scan(ctx, getScanHandlers(j.input, taskQueue, progress), file.ScanOptions{ - Paths: paths, - ScanFilters: []file.PathFilter{newScanFilter(c, repo, minModTime)}, - ZipFileExtensions: cfg.GetGalleryExtensions(), - ParallelTasks: cfg.GetParallelTasksWithAutoDetection(), - HandlerRequiredFilters: []file.Filter{newHandlerRequiredFilter(cfg, repo)}, - Rescan: j.input.Rescan, - }, progress) + // HACK - these should really be set in the scanner initialization + j.scanner.FileHandlers = getScanHandlers(j.input, taskQueue, progress) + j.scanner.ScanFilters = []file.PathFilter{newScanFilter(c, repo, minModTime)} + j.scanner.HandlerRequiredFilters = []file.Filter{newHandlerRequiredFilter(cfg, repo)} + + j.runJob(ctx, paths, nTasks, progress) taskQueue.Close() @@ -86,6 +89,264 @@ func (j *ScanJob) Execute(ctx context.Context, progress *job.Progress) error { return nil } +func (j *ScanJob) runJob(ctx context.Context, paths []string, nTasks int, progress *job.Progress) { + var wg sync.WaitGroup + wg.Add(1) + + j.fileQueue = make(chan file.ScannedFile, scanQueueSize) + + go func() { + defer func() { + wg.Done() + + // handle panics in goroutine + if p := recover(); p != nil { + logger.Errorf("panic while queuing files for scan: %v", p) + logger.Errorf(string(debug.Stack())) + } + }() + + if err := j.queueFiles(ctx, paths, progress); err != nil { + if errors.Is(err, context.Canceled) { + return + } + + logger.Errorf("error queuing files for scan: %v", err) + return + } + + logger.Infof("Finished adding files to queue. %d files queued", j.count) + }() + + defer wg.Wait() + + j.processQueue(ctx, nTasks, progress) +} + +const scanQueueSize = 200000 + +func (j *ScanJob) queueFiles(ctx context.Context, paths []string, progress *job.Progress) error { + fs := &file.OsFS{} + + defer func() { + close(j.fileQueue) + + progress.AddTotal(j.count) + progress.Definite() + }() + + var err error + progress.ExecuteTask("Walking directory tree", func() { + for _, p := range paths { + err = file.SymWalk(fs, p, j.queueFileFunc(ctx, fs, nil, progress)) + if err != nil { + return + } + } + }) + + return err +} + +func (j *ScanJob) queueFileFunc(ctx context.Context, f models.FS, zipFile *file.ScannedFile, progress *job.Progress) fs.WalkDirFunc { + return func(path string, d fs.DirEntry, err error) error { + if err != nil { + // don't let errors prevent scanning + logger.Errorf("error scanning %s: %v", path, err) + return nil + } + + if err = ctx.Err(); err != nil { + return err + } + + info, err := d.Info() + if err != nil { + logger.Errorf("reading info for %q: %v", path, err) + return nil + } + + if !j.scanner.AcceptEntry(ctx, path, info) { + if info.IsDir() { + logger.Debugf("Skipping directory %s", path) + return fs.SkipDir + } + + logger.Debugf("Skipping file %s", path) + return nil + } + + size, err := file.GetFileSize(f, path, info) + if err != nil { + return err + } + + ff := file.ScannedFile{ + BaseFile: &models.BaseFile{ + DirEntry: models.DirEntry{ + ModTime: file.ModTime(info), + }, + Path: path, + Basename: filepath.Base(path), + Size: size, + }, + FS: f, + Info: info, + } + + if zipFile != nil { + ff.ZipFileID = &zipFile.ID + ff.ZipFile = zipFile + } + + if info.IsDir() { + // handle folders immediately + if err := j.handleFolder(ctx, ff, progress); err != nil { + if !errors.Is(err, context.Canceled) { + logger.Errorf("error processing %q: %v", path, err) + } + + // skip the directory since we won't be able to process the files anyway + return fs.SkipDir + } + + return nil + } + + // if zip file is present, we handle immediately + if zipFile != nil { + progress.ExecuteTask("Scanning "+path, func() { + // don't increment progress in zip files + if err := j.handleFile(ctx, ff, nil); err != nil { + if !errors.Is(err, context.Canceled) { + logger.Errorf("error processing %q: %v", path, err) + } + // don't return an error, just skip the file + } + }) + + return nil + } + + logger.Tracef("Queueing file %s for scanning", path) + j.fileQueue <- ff + + j.count++ + + return nil + } +} + +func (j *ScanJob) processQueue(ctx context.Context, parallelTasks int, progress *job.Progress) { + if parallelTasks < 1 { + parallelTasks = 1 + } + + wg := sizedwaitgroup.New(parallelTasks) + + func() { + defer func() { + wg.Wait() + + // handle panics in goroutine + if p := recover(); p != nil { + logger.Errorf("panic while scanning files: %v", p) + logger.Errorf(string(debug.Stack())) + } + }() + + for f := range j.fileQueue { + logger.Tracef("Processing queued file %s", f.Path) + if err := ctx.Err(); err != nil { + return + } + + wg.Add() + ff := f + go func() { + defer wg.Done() + j.processQueueItem(ctx, ff, progress) + }() + } + }() +} + +func (j *ScanJob) processQueueItem(ctx context.Context, f file.ScannedFile, progress *job.Progress) { + progress.ExecuteTask("Scanning "+f.Path, func() { + var err error + if f.Info.IsDir() { + err = j.handleFolder(ctx, f, progress) + } else { + err = j.handleFile(ctx, f, progress) + } + + if err != nil && !errors.Is(err, context.Canceled) { + logger.Errorf("error processing %q: %v", f.Path, err) + } + }) +} + +func (j *ScanJob) handleFolder(ctx context.Context, f file.ScannedFile, progress *job.Progress) error { + if progress != nil { + defer progress.Increment() + } + + _, err := j.scanner.ScanFolder(ctx, f) + if err != nil { + return err + } + + return nil +} + +func (j *ScanJob) handleFile(ctx context.Context, f file.ScannedFile, progress *job.Progress) error { + if progress != nil { + defer progress.Increment() + } + + r, err := j.scanner.ScanFile(ctx, f) + if err != nil { + return err + } + + // handle rename should have already handled the contents of the zip file + // so shouldn't need to scan it again + + if (r.New || r.Updated) && j.scanner.IsZipFile(f.Info.Name()) { + ff := r.File + f.BaseFile = ff.Base() + + // scan zip files with a different context that is not cancellable + // cancelling while scanning zip file contents results in the scan + // contents being partially completed + zipCtx := context.WithoutCancel(ctx) + + if err := j.scanZipFile(zipCtx, f, progress); err != nil { + logger.Errorf("Error scanning zip file %q: %v", f.Path, err) + } + } + + return nil +} + +func (j *ScanJob) scanZipFile(ctx context.Context, f file.ScannedFile, progress *job.Progress) error { + zipFS, err := f.FS.OpenZip(f.Path, f.Size) + if err != nil { + if errors.Is(err, file.ErrNotReaderAt) { + // can't walk the zip file + // just return + logger.Debugf("Skipping zip file %q as it cannot be opened for walking", f.Path) + return nil + } + + return err + } + + defer zipFS.Close() + + return file.SymWalk(zipFS, f.Path, j.queueFileFunc(ctx, zipFS, &f, progress)) +} + type extensionConfig struct { vidExt []string imgExt []string @@ -463,6 +724,29 @@ func (g *imageGenerators) Generate(ctx context.Context, i *models.Image, f model } } + if t.ScanGenerateImagePhashes { + progress.AddTotal(1) + phashFn := func(ctx context.Context) { + mgr := GetInstance() + // Only generate phash for image files, not video files + if imageFile, ok := f.(*models.ImageFile); ok { + taskPhash := GenerateImagePhashTask{ + repository: mgr.Repository, + File: imageFile, + Overwrite: overwrite, + } + taskPhash.Start(ctx) + } + progress.Increment() + } + + if g.sequentialScanning { + phashFn(ctx) + } else { + g.taskQueue.Add(fmt.Sprintf("Generating phash for %s", path), phashFn) + } + } + return nil } diff --git a/internal/manager/task_stash_box_tag.go b/internal/manager/task_stash_box_tag.go index d20b71f06..37859ba61 100644 --- a/internal/manager/task_stash_box_tag.go +++ b/internal/manager/task_stash_box_tag.go @@ -14,57 +14,33 @@ import ( "github.com/stashapp/stash/pkg/studio" ) -type StashBoxTagTaskType int - -const ( - Performer StashBoxTagTaskType = iota - Studio -) - -type StashBoxBatchTagTask struct { +// stashBoxBatchPerformerTagTask is used to tag or create performers from stash-box. +// +// Two modes of operation: +// - Update existing performer: set performer to update from stash-box data +// - Create new performer: set name or stashID to search stash-box and create locally +type stashBoxBatchPerformerTagTask struct { box *models.StashBox name *string + stashID *string performer *models.Performer - studio *models.Studio - refresh bool - createParent bool excludedFields []string - taskType StashBoxTagTaskType } -func (t *StashBoxBatchTagTask) Start(ctx context.Context) { - switch t.taskType { - case Performer: - t.stashBoxPerformerTag(ctx) - case Studio: - t.stashBoxStudioTag(ctx) +func (t *stashBoxBatchPerformerTagTask) getName() string { + switch { + case t.name != nil: + return *t.name + case t.stashID != nil: + return *t.stashID + case t.performer != nil: + return t.performer.Name default: - logger.Errorf("Error starting batch task, unknown task_type %d", t.taskType) + return "" } } -func (t *StashBoxBatchTagTask) Description() string { - if t.taskType == Performer { - var name string - if t.name != nil { - name = *t.name - } else { - name = t.performer.Name - } - return fmt.Sprintf("Tagging performer %s from stash-box", name) - } else if t.taskType == Studio { - var name string - if t.name != nil { - name = *t.name - } else { - name = t.studio.Name - } - return fmt.Sprintf("Tagging studio %s from stash-box", name) - } - return fmt.Sprintf("Unknown tagging task type %d from stash-box", t.taskType) -} - -func (t *StashBoxBatchTagTask) stashBoxPerformerTag(ctx context.Context) { +func (t *stashBoxBatchPerformerTagTask) Start(ctx context.Context) { performer, err := t.findStashBoxPerformer(ctx) if err != nil { logger.Errorf("Error fetching performer data from stash-box: %v", err) @@ -76,21 +52,18 @@ func (t *StashBoxBatchTagTask) stashBoxPerformerTag(ctx context.Context) { excluded[field] = true } - // performer will have a value if pulling from Stash-box by Stash ID or name was successful if performer != nil { t.processMatchedPerformer(ctx, performer, excluded) } else { - var name string - if t.name != nil { - name = *t.name - } else if t.performer != nil { - name = t.performer.Name - } - logger.Infof("No match found for %s", name) + logger.Infof("No match found for %s", t.getName()) } } -func (t *StashBoxBatchTagTask) findStashBoxPerformer(ctx context.Context) (*models.ScrapedPerformer, error) { +func (t *stashBoxBatchPerformerTagTask) GetDescription() string { + return fmt.Sprintf("Tagging performer %s from stash-box", t.getName()) +} + +func (t *stashBoxBatchPerformerTagTask) findStashBoxPerformer(ctx context.Context) (*models.ScrapedPerformer, error) { var performer *models.ScrapedPerformer var err error @@ -98,7 +71,24 @@ func (t *StashBoxBatchTagTask) findStashBoxPerformer(ctx context.Context) (*mode client := stashbox.NewClient(*t.box, stashbox.ExcludeTagPatterns(instance.Config.GetScraperExcludeTagPatterns())) - if t.refresh { + switch { + case t.name != nil: + performer, err = client.FindPerformerByName(ctx, *t.name) + case t.stashID != nil: + performer, err = client.FindPerformerByID(ctx, *t.stashID) + + if performer != nil && performer.RemoteMergedIntoId != nil { + mergedPerformer, err := t.handleMergedPerformer(ctx, performer, client) + if err != nil { + return nil, err + } + + if mergedPerformer != nil { + logger.Infof("Performer id %s merged into %s, updating local performer", *t.stashID, *performer.RemoteMergedIntoId) + performer = mergedPerformer + } + } + case t.performer != nil: // tagging or updating existing performer var remoteID string if err := r.WithReadTxn(ctx, func(ctx context.Context) error { qb := r.Performer @@ -118,6 +108,7 @@ func (t *StashBoxBatchTagTask) findStashBoxPerformer(ctx context.Context) (*mode }); err != nil { return nil, err } + if remoteID != "" { performer, err = client.FindPerformerByID(ctx, remoteID) @@ -132,15 +123,10 @@ func (t *StashBoxBatchTagTask) findStashBoxPerformer(ctx context.Context) (*mode performer = mergedPerformer } } - } - } else { - var name string - if t.name != nil { - name = *t.name } else { - name = t.performer.Name + // find by performer name instead + performer, err = client.FindPerformerByName(ctx, t.performer.Name) } - performer, err = client.FindPerformerByName(ctx, name) } if performer != nil { @@ -154,7 +140,7 @@ func (t *StashBoxBatchTagTask) findStashBoxPerformer(ctx context.Context) (*mode return performer, err } -func (t *StashBoxBatchTagTask) handleMergedPerformer(ctx context.Context, performer *models.ScrapedPerformer, client *stashbox.Client) (mergedPerformer *models.ScrapedPerformer, err error) { +func (t *stashBoxBatchPerformerTagTask) handleMergedPerformer(ctx context.Context, performer *models.ScrapedPerformer, client *stashbox.Client) (mergedPerformer *models.ScrapedPerformer, err error) { mergedPerformer, err = client.FindPerformerByID(ctx, *performer.RemoteMergedIntoId) if err != nil { return nil, fmt.Errorf("loading merged performer %s from stashbox", *performer.RemoteMergedIntoId) @@ -169,8 +155,7 @@ func (t *StashBoxBatchTagTask) handleMergedPerformer(ctx context.Context, perfor return mergedPerformer, nil } -func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *models.ScrapedPerformer, excluded map[string]bool) { - // Refreshing an existing performer +func (t *stashBoxBatchPerformerTagTask) processMatchedPerformer(ctx context.Context, p *models.ScrapedPerformer, excluded map[string]bool) { if t.performer != nil { storedID, _ := strconv.Atoi(*p.StoredID) @@ -180,7 +165,6 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m return } - // Start the transaction and update the performer r := instance.Repository err = r.WithTxn(ctx, func(ctx context.Context) error { qb := r.Performer @@ -226,8 +210,8 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m } else { logger.Infof("Updated performer %s", *p.Name) } - } else if t.name != nil && p.Name != nil { - // Creating a new performer + } else { + // no existing performer, create a new one newPerformer := p.ToPerformer(t.box.Endpoint, excluded) image, err := p.GetImage(ctx, excluded) if err != nil { @@ -263,7 +247,34 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m } } -func (t *StashBoxBatchTagTask) stashBoxStudioTag(ctx context.Context) { +// stashBoxBatchStudioTagTask is used to tag or create studios from stash-box. +// +// Two modes of operation: +// - Update existing studio: set studio to update from stash-box data +// - Create new studio: set name or stashID to search stash-box and create locally +type stashBoxBatchStudioTagTask struct { + box *models.StashBox + name *string + stashID *string + studio *models.Studio + createParent bool + excludedFields []string +} + +func (t *stashBoxBatchStudioTagTask) getName() string { + switch { + case t.name != nil: + return *t.name + case t.stashID != nil: + return *t.stashID + case t.studio != nil: + return t.studio.Name + default: + return "" + } +} + +func (t *stashBoxBatchStudioTagTask) Start(ctx context.Context) { studio, err := t.findStashBoxStudio(ctx) if err != nil { logger.Errorf("Error fetching studio data from stash-box: %v", err) @@ -275,21 +286,18 @@ func (t *StashBoxBatchTagTask) stashBoxStudioTag(ctx context.Context) { excluded[field] = true } - // studio will have a value if pulling from Stash-box by Stash ID or name was successful if studio != nil { t.processMatchedStudio(ctx, studio, excluded) } else { - var name string - if t.name != nil { - name = *t.name - } else if t.studio != nil { - name = t.studio.Name - } - logger.Infof("No match found for %s", name) + logger.Infof("No match found for %s", t.getName()) } } -func (t *StashBoxBatchTagTask) findStashBoxStudio(ctx context.Context) (*models.ScrapedStudio, error) { +func (t *stashBoxBatchStudioTagTask) GetDescription() string { + return fmt.Sprintf("Tagging studio %s from stash-box", t.getName()) +} + +func (t *stashBoxBatchStudioTagTask) findStashBoxStudio(ctx context.Context) (*models.ScrapedStudio, error) { var studio *models.ScrapedStudio var err error @@ -297,7 +305,12 @@ func (t *StashBoxBatchTagTask) findStashBoxStudio(ctx context.Context) (*models. client := stashbox.NewClient(*t.box, stashbox.ExcludeTagPatterns(instance.Config.GetScraperExcludeTagPatterns())) - if t.refresh { + switch { + case t.name != nil: + studio, err = client.FindStudio(ctx, *t.name) + case t.stashID != nil: + studio, err = client.FindStudio(ctx, *t.stashID) + case t.studio != nil: var remoteID string if err := r.WithReadTxn(ctx, func(ctx context.Context) error { if !t.studio.StashIDs.Loaded() { @@ -315,17 +328,13 @@ func (t *StashBoxBatchTagTask) findStashBoxStudio(ctx context.Context) (*models. }); err != nil { return nil, err } + if remoteID != "" { studio, err = client.FindStudio(ctx, remoteID) - } - } else { - var name string - if t.name != nil { - name = *t.name } else { - name = t.studio.Name + // find by studio name instead + studio, err = client.FindStudio(ctx, t.studio.Name) } - studio, err = client.FindStudio(ctx, name) } if err := r.WithReadTxn(ctx, func(ctx context.Context) error { @@ -343,8 +352,7 @@ func (t *StashBoxBatchTagTask) findStashBoxStudio(ctx context.Context) (*models. return studio, err } -func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *models.ScrapedStudio, excluded map[string]bool) { - // Refreshing an existing studio +func (t *stashBoxBatchStudioTagTask) processMatchedStudio(ctx context.Context, s *models.ScrapedStudio, excluded map[string]bool) { if t.studio != nil { storedID, _ := strconv.Atoi(*s.StoredID) @@ -361,7 +369,6 @@ func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *mode return } - // Start the transaction and update the studio r := instance.Repository err = r.WithTxn(ctx, func(ctx context.Context) error { qb := r.Studio @@ -394,8 +401,8 @@ func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *mode } else { logger.Infof("Updated studio %s", s.Name) } - } else if t.name != nil && s.Name != "" { - // Creating a new studio + } else if s.Name != "" { + // no existing studio, create a new one if s.Parent != nil && t.createParent { err := t.processParentStudio(ctx, s.Parent, excluded) if err != nil { @@ -410,7 +417,6 @@ func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *mode return } - // Start the transaction and save the studio r := instance.Repository err = r.WithTxn(ctx, func(ctx context.Context) error { qb := r.Studio @@ -439,9 +445,8 @@ func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *mode } } -func (t *StashBoxBatchTagTask) processParentStudio(ctx context.Context, parent *models.ScrapedStudio, excluded map[string]bool) error { +func (t *stashBoxBatchStudioTagTask) processParentStudio(ctx context.Context, parent *models.ScrapedStudio, excluded map[string]bool) error { if parent.StoredID == nil { - // The parent needs to be created newParentStudio := parent.ToStudio(t.box.Endpoint, excluded) image, err := parent.GetImage(ctx, excluded) @@ -450,7 +455,6 @@ func (t *StashBoxBatchTagTask) processParentStudio(ctx context.Context, parent * return err } - // Start the transaction and save the studio r := instance.Repository err = r.WithTxn(ctx, func(ctx context.Context) error { qb := r.Studio @@ -476,7 +480,6 @@ func (t *StashBoxBatchTagTask) processParentStudio(ctx context.Context, parent * } return err } else { - // The parent studio matched an existing one and the user has chosen in the UI to link and/or update it storedID, _ := strconv.Atoi(*parent.StoredID) image, err := parent.GetImage(ctx, excluded) @@ -485,7 +488,6 @@ func (t *StashBoxBatchTagTask) processParentStudio(ctx context.Context, parent * return err } - // Start the transaction and update the studio r := instance.Repository err = r.WithTxn(ctx, func(ctx context.Context) error { qb := r.Studio diff --git a/internal/static/embed.go b/internal/static/embed.go index 91437a81f..665c5a892 100644 --- a/internal/static/embed.go +++ b/internal/static/embed.go @@ -8,12 +8,13 @@ import ( "io/fs" ) -//go:embed performer performer_male scene image gallery tag studio group +//go:embed performer performer_male performer_sfw scene image gallery tag studio group var data embed.FS const ( - Performer = "performer" - PerformerMale = "performer_male" + Performer = "performer" + PerformerMale = "performer_male" + DefaultSFWPerformerImage = "performer_sfw/performer.svg" Scene = "scene" DefaultSceneImage = "scene/scene.svg" diff --git a/internal/static/performer_sfw/performer.svg b/internal/static/performer_sfw/performer.svg new file mode 100644 index 000000000..24b444171 --- /dev/null +++ b/internal/static/performer_sfw/performer.svg @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/pkg/ffmpeg/codec_hardware.go b/pkg/ffmpeg/codec_hardware.go index 5151e7efe..aa8c75dcc 100644 --- a/pkg/ffmpeg/codec_hardware.go +++ b/pkg/ffmpeg/codec_hardware.go @@ -5,9 +5,11 @@ import ( "context" "fmt" "math" + "os" "regexp" "strconv" "strings" + "time" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" @@ -27,12 +29,39 @@ var ( VideoCodecIVP9 = makeVideoCodec("VP9 Intel Quick Sync Video (QSV)", "vp9_qsv") VideoCodecVVP9 = makeVideoCodec("VP9 VAAPI", "vp9_vaapi") VideoCodecVVPX = makeVideoCodec("VP8 VAAPI", "vp8_vaapi") + VideoCodecRK264 = makeVideoCodec("H264 Rockchip MPP (rkmpp)", "h264_rkmpp") ) const minHeight int = 480 // Tests all (given) hardware codec's func (f *FFMpeg) InitHWSupport(ctx context.Context) { + // do the hardware codec tests in a separate goroutine to avoid blocking + done := make(chan struct{}) + go func() { + f.initHWSupport(ctx) + close(done) + }() + + // log if the initialization takes too long + const hwInitLogTimeoutSecondsDefault = 5 + hwInitLogTimeoutSeconds := hwInitLogTimeoutSecondsDefault * time.Second + timer := time.NewTimer(hwInitLogTimeoutSeconds) + + go func() { + select { + case <-timer.C: + logger.Warnf("[InitHWSupport] Hardware codec initialization is taking longer than %s...", hwInitLogTimeoutSeconds) + logger.Info("[InitHWSupport] Hardware encoding will not be available until initialization is complete.") + case <-done: + if !timer.Stop() { + <-timer.C + } + } + }() +} + +func (f *FFMpeg) initHWSupport(ctx context.Context) { var hwCodecSupport []VideoCodec // Note that the first compatible codec is returned, so order is important @@ -43,6 +72,7 @@ func (f *FFMpeg) InitHWSupport(ctx context.Context) { VideoCodecI264C, VideoCodecV264, VideoCodecR264, + VideoCodecRK264, VideoCodecIVP9, VideoCodecVVP9, VideoCodecM264, @@ -64,12 +94,33 @@ func (f *FFMpeg) InitHWSupport(ctx context.Context) { args = args.Format("null") args = args.Output("-") - cmd := f.Command(ctx, args) + // #6064 - add timeout to context to prevent hangs + const hwTestTimeoutSecondsDefault = 10 + hwTestTimeoutSeconds := hwTestTimeoutSecondsDefault * time.Second + + // allow timeout to be overridden with environment variable + if timeout := os.Getenv("STASH_HW_TEST_TIMEOUT"); timeout != "" { + if seconds, err := strconv.Atoi(timeout); err == nil { + hwTestTimeoutSeconds = time.Duration(seconds) * time.Second + } + } + + testCtx, cancel := context.WithTimeout(ctx, hwTestTimeoutSeconds) + defer cancel() + + cmd := f.Command(testCtx, args) + cmd.WaitDelay = time.Second + logger.Tracef("[InitHWSupport] Testing codec %s: %v", codec, cmd.Args) var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { + if testCtx.Err() != nil { + logger.Debugf("[InitHWSupport] Codec %s test timed out after %s", codec, hwTestTimeoutSeconds) + continue + } + errOutput := stderr.String() if len(errOutput) == 0 { @@ -88,6 +139,8 @@ func (f *FFMpeg) InitHWSupport(ctx context.Context) { } logger.Info(outstr) + f.hwCodecSupportMutex.Lock() + defer f.hwCodecSupportMutex.Unlock() f.hwCodecSupport = hwCodecSupport } @@ -179,6 +232,19 @@ func (f *FFMpeg) hwDeviceInit(args Args, toCodec VideoCodec, fullhw bool) Args { args = append(args, "-init_hw_device") args = append(args, "videotoolbox=vt") } + case VideoCodecRK264: + // Rockchip: always create rkmpp device and make it the filter device, so + // scale_rkrga and subsequent hwupload/hwmap operate in the right context. + args = append(args, "-init_hw_device") + args = append(args, "rkmpp=rk") + args = append(args, "-filter_hw_device") + args = append(args, "rk") + if fullhw { + args = append(args, "-hwaccel") + args = append(args, "rkmpp") + args = append(args, "-hwaccel_output_format") + args = append(args, "drm_prime") + } } return args @@ -211,6 +277,14 @@ func (f *FFMpeg) hwFilterInit(toCodec VideoCodec, fullhw bool) VideoFilter { videoFilter = videoFilter.Append("format=nv12") videoFilter = videoFilter.Append("hwupload") } + case VideoCodecRK264: + // For Rockchip full-hw, do NOT pre-map to rkrga here. scale_rkrga can + // consume DRM_PRIME frames directly when filter_hw_device is set. + // For non-fullhw, keep a sane software format. + if !fullhw { + videoFilter = videoFilter.Append("format=nv12") + videoFilter = videoFilter.Append("hwupload") + } } return videoFilter @@ -288,6 +362,12 @@ func (f *FFMpeg) hwApplyFullHWFilter(args VideoFilter, codec VideoCodec, fullhw if fullhw && f.version.Gteq(Version{major: 3, minor: 3}) { // Added in FFMpeg 3.3 args = args.Append("scale_qsv=format=nv12") } + case VideoCodecRK264: + // Full-hw decode on 10-bit sources often produces DRM_PRIME with sw_pix_fmt=nv15. + // h264_rkmpp does NOT accept nv15, so we must force a conversion to nv12 + if fullhw { + args = args.Append("scale_rkrga=w=iw:h=ih:format=nv12") + } } return args @@ -315,6 +395,14 @@ func (f *FFMpeg) hwApplyScaleTemplate(sargs string, codec VideoCodec, match []in } case VideoCodecM264: template = "scale_vt=$value" + case VideoCodecRK264: + // The original filter chain is a fallback for maximum compatibility: + // "scale_rkrga=$value:format=nv12,hwdownload,format=nv12,hwupload" + // It avoids hwmap(rkrga→rkmpp) failures (-38/-12) seen on some builds + // by downloading the scaled frame to system RAM and re-uploading it. + // The filter chain below uses a zero-copy approach, passing the hardware-scaled + // frame directly to the encoder. This is more efficient but may be less stable. + template = "scale_rkrga=$value:format=nv12" default: return VideoFilter(sargs) } @@ -323,12 +411,15 @@ func (f *FFMpeg) hwApplyScaleTemplate(sargs string, codec VideoCodec, match []in isIntel := codec == VideoCodecI264 || codec == VideoCodecI264C || codec == VideoCodecIVP9 // BUG: scale_vt doesn't call ff_scale_adjust_dimensions, thus cant accept negative size values isApple := codec == VideoCodecM264 + // Rockchip's scale_rkrga supports -1/-2; don't apply minus-one hack here. return VideoFilter(templateReplaceScale(sargs, template, match, vf, isIntel || isApple)) } // Returns the max resolution for a given codec, or a default func (f *FFMpeg) hwCodecMaxRes(codec VideoCodec) (int, int) { switch codec { + case VideoCodecRK264: + return 8192, 8192 case VideoCodecN264, VideoCodecN264H, VideoCodecI264, @@ -352,7 +443,7 @@ func (f *FFMpeg) hwMaxResFilter(toCodec VideoCodec, vf *models.VideoFile, reqHei // Return if a hardware accelerated for HLS is available func (f *FFMpeg) hwCodecHLSCompatible() *VideoCodec { - for _, element := range f.hwCodecSupport { + for _, element := range f.getHWCodecSupport() { switch element { case VideoCodecN264, VideoCodecN264H, @@ -360,7 +451,8 @@ func (f *FFMpeg) hwCodecHLSCompatible() *VideoCodec { VideoCodecI264C, VideoCodecV264, VideoCodecR264, - VideoCodecM264: // Note that the Apple encoder sucks at startup, thus HLS quality is crap + VideoCodecM264, // Note that the Apple encoder sucks at startup, thus HLS quality is crap + VideoCodecRK264: return &element } } @@ -369,13 +461,14 @@ func (f *FFMpeg) hwCodecHLSCompatible() *VideoCodec { // Return if a hardware accelerated codec for MP4 is available func (f *FFMpeg) hwCodecMP4Compatible() *VideoCodec { - for _, element := range f.hwCodecSupport { + for _, element := range f.getHWCodecSupport() { switch element { case VideoCodecN264, VideoCodecN264H, VideoCodecI264, VideoCodecI264C, - VideoCodecM264: + VideoCodecM264, + VideoCodecRK264: return &element } } @@ -384,7 +477,7 @@ func (f *FFMpeg) hwCodecMP4Compatible() *VideoCodec { // Return if a hardware accelerated codec for WebM is available func (f *FFMpeg) hwCodecWEBMCompatible() *VideoCodec { - for _, element := range f.hwCodecSupport { + for _, element := range f.getHWCodecSupport() { switch element { case VideoCodecIVP9, VideoCodecVVP9: diff --git a/pkg/ffmpeg/ffmpeg.go b/pkg/ffmpeg/ffmpeg.go index ce1232e5d..04c58f04b 100644 --- a/pkg/ffmpeg/ffmpeg.go +++ b/pkg/ffmpeg/ffmpeg.go @@ -10,6 +10,7 @@ import ( "regexp" "strconv" "strings" + "sync" stashExec "github.com/stashapp/stash/pkg/exec" "github.com/stashapp/stash/pkg/fsutil" @@ -216,9 +217,10 @@ func (v Version) String() string { // FFMpeg provides an interface to ffmpeg. type FFMpeg struct { - ffmpeg string - version Version - hwCodecSupport []VideoCodec + ffmpeg string + version Version + hwCodecSupport []VideoCodec + hwCodecSupportMutex sync.RWMutex } // Creates a new FFMpeg encoder @@ -241,3 +243,9 @@ func (f *FFMpeg) Command(ctx context.Context, args []string) *exec.Cmd { func (f *FFMpeg) Path() string { return f.ffmpeg } + +func (f *FFMpeg) getHWCodecSupport() []VideoCodec { + f.hwCodecSupportMutex.RLock() + defer f.hwCodecSupportMutex.RUnlock() + return f.hwCodecSupport +} diff --git a/pkg/file/clean.go b/pkg/file/clean.go index 8c54fd0e0..53b2e0612 100644 --- a/pkg/file/clean.go +++ b/pkg/file/clean.go @@ -18,7 +18,8 @@ type Cleaner struct { FS models.FS Repository Repository - Handlers []CleanHandler + Handlers []CleanHandler + TrashPath string } type cleanJob struct { @@ -392,7 +393,7 @@ func (j *cleanJob) shouldCleanFolder(ctx context.Context, f *models.Folder) bool func (j *cleanJob) deleteFile(ctx context.Context, fileID models.FileID, fn string) { // delete associated objects - fileDeleter := NewDeleter() + fileDeleter := NewDeleterWithTrash(j.TrashPath) r := j.Repository if err := r.WithTxn(ctx, func(ctx context.Context) error { fileDeleter.RegisterHooks(ctx) @@ -410,7 +411,7 @@ func (j *cleanJob) deleteFile(ctx context.Context, fileID models.FileID, fn stri func (j *cleanJob) deleteFolder(ctx context.Context, folderID models.FolderID, fn string) { // delete associated objects - fileDeleter := NewDeleter() + fileDeleter := NewDeleterWithTrash(j.TrashPath) r := j.Repository if err := r.WithTxn(ctx, func(ctx context.Context) error { fileDeleter.RegisterHooks(ctx) diff --git a/pkg/file/delete.go b/pkg/file/delete.go index 88eb5169e..c36068faa 100644 --- a/pkg/file/delete.go +++ b/pkg/file/delete.go @@ -58,20 +58,33 @@ func newRenamerRemoverImpl() renamerRemoverImpl { // Deleter is used to safely delete files and directories from the filesystem. // During a transaction, files and directories are marked for deletion using -// the Files and Dirs methods. This will rename the files/directories to be -// deleted. If the transaction is rolled back, then the files/directories can -// be restored to their original state with the Abort method. If the -// transaction is committed, the marked files are then deleted from the -// filesystem using the Complete method. +// the Files and Dirs methods. If TrashPath is set, files are moved to trash +// immediately. Otherwise, they are renamed with a .delete suffix. If the +// transaction is rolled back, then the files/directories can be restored to +// their original state with the Rollback method. If the transaction is +// committed, the marked files are then deleted from the filesystem using the +// Commit method. type Deleter struct { RenamerRemover RenamerRemover files []string dirs []string + TrashPath string // if set, files will be moved to this directory instead of being permanently deleted + trashedPaths map[string]string // map of original path -> trash path (only used when TrashPath is set) } func NewDeleter() *Deleter { return &Deleter{ RenamerRemover: newRenamerRemoverImpl(), + TrashPath: "", + trashedPaths: make(map[string]string), + } +} + +func NewDeleterWithTrash(trashPath string) *Deleter { + return &Deleter{ + RenamerRemover: newRenamerRemoverImpl(), + TrashPath: trashPath, + trashedPaths: make(map[string]string), } } @@ -92,6 +105,17 @@ func (d *Deleter) RegisterHooks(ctx context.Context) { // Abort should be called to restore marked files if this function returns an // error. func (d *Deleter) Files(paths []string) error { + return d.filesInternal(paths, false) +} + +// FilesWithoutTrash designates files to be deleted, bypassing the trash directory. +// Files will be permanently deleted even if TrashPath is configured. +// This is useful for deleting generated files that can be easily recreated. +func (d *Deleter) FilesWithoutTrash(paths []string) error { + return d.filesInternal(paths, true) +} + +func (d *Deleter) filesInternal(paths []string, bypassTrash bool) error { for _, p := range paths { // fail silently if the file does not exist if _, err := d.RenamerRemover.Stat(p); err != nil { @@ -103,7 +127,7 @@ func (d *Deleter) Files(paths []string) error { return fmt.Errorf("check file %q exists: %w", p, err) } - if err := d.renameForDelete(p); err != nil { + if err := d.renameForDelete(p, bypassTrash); err != nil { return fmt.Errorf("marking file %q for deletion: %w", p, err) } d.files = append(d.files, p) @@ -118,6 +142,17 @@ func (d *Deleter) Files(paths []string) error { // Abort should be called to restore marked files/directories if this function returns an // error. func (d *Deleter) Dirs(paths []string) error { + return d.dirsInternal(paths, false) +} + +// DirsWithoutTrash designates directories to be deleted, bypassing the trash directory. +// Directories will be permanently deleted even if TrashPath is configured. +// This is useful for deleting generated directories that can be easily recreated. +func (d *Deleter) DirsWithoutTrash(paths []string) error { + return d.dirsInternal(paths, true) +} + +func (d *Deleter) dirsInternal(paths []string, bypassTrash bool) error { for _, p := range paths { // fail silently if the file does not exist if _, err := d.RenamerRemover.Stat(p); err != nil { @@ -129,7 +164,7 @@ func (d *Deleter) Dirs(paths []string) error { return fmt.Errorf("check directory %q exists: %w", p, err) } - if err := d.renameForDelete(p); err != nil { + if err := d.renameForDelete(p, bypassTrash); err != nil { return fmt.Errorf("marking directory %q for deletion: %w", p, err) } d.dirs = append(d.dirs, p) @@ -150,33 +185,65 @@ func (d *Deleter) Rollback() { d.files = nil d.dirs = nil + d.trashedPaths = make(map[string]string) } // Commit deletes all files marked for deletion and clears the marked list. +// When using trash, files have already been moved during renameForDelete, so +// this just clears the tracking. Otherwise, permanently delete the .delete files. // Any errors encountered are logged. All files will be attempted, regardless // of the errors encountered. func (d *Deleter) Commit() { - for _, f := range d.files { - if err := d.RenamerRemover.Remove(f + deleteFileSuffix); err != nil { - logger.Warnf("Error deleting file %q: %v", f+deleteFileSuffix, err) + if d.TrashPath != "" { + // Files were already moved to trash during renameForDelete, just clear tracking + logger.Debugf("Commit: %d files and %d directories already in trash, clearing tracking", len(d.files), len(d.dirs)) + } else { + // Permanently delete files and directories marked with .delete suffix + for _, f := range d.files { + if err := d.RenamerRemover.Remove(f + deleteFileSuffix); err != nil { + logger.Warnf("Error deleting file %q: %v", f+deleteFileSuffix, err) + } } - } - for _, f := range d.dirs { - if err := d.RenamerRemover.RemoveAll(f + deleteFileSuffix); err != nil { - logger.Warnf("Error deleting directory %q: %v", f+deleteFileSuffix, err) + for _, f := range d.dirs { + if err := d.RenamerRemover.RemoveAll(f + deleteFileSuffix); err != nil { + logger.Warnf("Error deleting directory %q: %v", f+deleteFileSuffix, err) + } } } d.files = nil d.dirs = nil + d.trashedPaths = make(map[string]string) } -func (d *Deleter) renameForDelete(path string) error { +func (d *Deleter) renameForDelete(path string, bypassTrash bool) error { + if d.TrashPath != "" && !bypassTrash { + // Move file to trash immediately + trashDest, err := fsutil.MoveToTrash(path, d.TrashPath) + if err != nil { + return err + } + d.trashedPaths[path] = trashDest + logger.Infof("Moved %q to trash at %s", path, trashDest) + return nil + } + + // Standard behavior: rename with .delete suffix (or when bypassing trash) return d.RenamerRemover.Rename(path, path+deleteFileSuffix) } func (d *Deleter) renameForRestore(path string) error { + if d.TrashPath != "" { + // Restore file from trash + trashPath, ok := d.trashedPaths[path] + if !ok { + return fmt.Errorf("no trash path found for %q", path) + } + return d.RenamerRemover.Rename(trashPath, path) + } + + // Standard behavior: restore from .delete suffix return d.RenamerRemover.Rename(path+deleteFileSuffix, path) } diff --git a/pkg/file/file.go b/pkg/file/file.go index 407949ba1..b93083b35 100644 --- a/pkg/file/file.go +++ b/pkg/file/file.go @@ -3,6 +3,10 @@ package file import ( "context" + "fmt" + "io/fs" + "os" + "time" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/txn" @@ -35,3 +39,23 @@ func (r *Repository) WithReadTxn(ctx context.Context, fn txn.TxnFunc) error { func (r *Repository) WithDB(ctx context.Context, fn txn.TxnFunc) error { return txn.WithDatabase(ctx, r.TxnManager, fn) } + +// ModTime returns the modification time truncated to seconds. +func ModTime(info fs.FileInfo) time.Time { + // truncate to seconds, since we don't store beyond that in the database + return info.ModTime().Truncate(time.Second) +} + +// GetFileSize gets the size of the file, taking into account symlinks. +func GetFileSize(f models.FS, path string, info fs.FileInfo) (int64, error) { + // #2196/#3042 - replace size with target size if file is a symlink + if info.Mode()&os.ModeSymlink == os.ModeSymlink { + targetInfo, err := f.Stat(path) + if err != nil { + return 0, fmt.Errorf("reading info for symlink %q: %w", path, err) + } + return targetInfo.Size(), nil + } + + return info.Size(), nil +} diff --git a/pkg/file/folder.go b/pkg/file/folder.go index 451bb1d93..fe260c155 100644 --- a/pkg/file/folder.go +++ b/pkg/file/folder.go @@ -15,7 +15,9 @@ import ( // Does not create any folders in the file system func GetOrCreateFolderHierarchy(ctx context.Context, fc models.FolderFinderCreator, path string) (*models.Folder, error) { // get or create folder hierarchy - folder, err := fc.FindByPath(ctx, path) + // assume case sensitive when searching for the folder + const caseSensitive = true + folder, err := fc.FindByPath(ctx, path, caseSensitive) if err != nil { return nil, err } diff --git a/pkg/file/folder_rename_detect.go b/pkg/file/folder_rename_detect.go index 4c057461b..cfae7e4fb 100644 --- a/pkg/file/folder_rename_detect.go +++ b/pkg/file/folder_rename_detect.go @@ -75,7 +75,7 @@ func (d *folderRenameDetector) bestCandidate() *models.Folder { return best.folder } -func (s *scanJob) detectFolderMove(ctx context.Context, file scanFile) (*models.Folder, error) { +func (s *Scanner) detectFolderMove(ctx context.Context, file ScannedFile) (*models.Folder, error) { // in order for a folder to be considered moved, the existing folder must be // missing, and the majority of the old folder's files must be present, unchanged, // in the new folder. @@ -88,7 +88,7 @@ func (s *scanJob) detectFolderMove(ctx context.Context, file scanFile) (*models. r := s.Repository - if err := symWalk(file.fs, file.Path, func(path string, d fs.DirEntry, err error) error { + if err := SymWalk(file.FS, file.Path, func(path string, d fs.DirEntry, err error) error { if err != nil { // don't let errors prevent scanning logger.Errorf("error scanning %s: %v", path, err) @@ -111,11 +111,11 @@ func (s *scanJob) detectFolderMove(ctx context.Context, file scanFile) (*models. return nil } - if !s.acceptEntry(ctx, path, info) { + if !s.AcceptEntry(ctx, path, info) { return nil } - size, err := getFileSize(file.fs, path, info) + size, err := GetFileSize(file.FS, path, info) if err != nil { return fmt.Errorf("getting file size for %q: %w", path, err) } @@ -154,7 +154,7 @@ func (s *scanJob) detectFolderMove(ctx context.Context, file scanFile) (*models. } // parent folder must be missing - _, err = file.fs.Lstat(pf.Path) + _, err = file.FS.Lstat(pf.Path) if err == nil { // parent folder exists, not a candidate detector.reject(parentFolderID) diff --git a/pkg/file/image/scan.go b/pkg/file/image/scan.go index a1d63f649..7ac69480c 100644 --- a/pkg/file/image/scan.go +++ b/pkg/file/image/scan.go @@ -2,8 +2,11 @@ package image import ( "context" + "errors" "fmt" "image" + "path/filepath" + "strings" _ "image/gif" _ "image/jpeg" @@ -17,6 +20,8 @@ import ( _ "golang.org/x/image/webp" ) +var ErrUnsupportedAVIFInZip = errors.New("AVIF images in zip files is unsupported") + // Decorator adds image specific fields to a File. type Decorator struct { FFProbe *ffmpeg.FFProbe @@ -28,6 +33,10 @@ func (d *Decorator) Decorate(ctx context.Context, fs models.FS, f models.File) ( // ignore clips in non-OsFS filesystems as ffprobe cannot read them // TODO - copy to temp file if not an OsFS if _, isOs := fs.(*file.OsFS); !isOs { + // AVIF images inside zip files are not supported + if strings.ToLower(filepath.Ext(base.Path)) == ".avif" { + return nil, fmt.Errorf("%w: %s", ErrUnsupportedAVIFInZip, base.Path) + } logger.Debugf("assuming ImageFile for non-OsFS file %q", base.Path) return decorateFallback(fs, f) } @@ -50,7 +59,7 @@ func (d *Decorator) Decorate(ctx context.Context, fs models.FS, f models.File) ( isClip := true // This list is derived from ffmpegImageThumbnail in pkg/image/thumbnail. If one gets updated, the other should be as well - for _, item := range []string{"png", "mjpeg", "webp", "bmp"} { + for _, item := range []string{"png", "mjpeg", "webp", "bmp", "jpegxl"} { if item == probe.VideoCodec { isClip = false } @@ -67,6 +76,25 @@ func (d *Decorator) Decorate(ctx context.Context, fs models.FS, f models.File) ( Height: probe.Height, } + // FFprobe has a known bug where it returns 0x0 dimensions for some animated WebP files + // Fall back to image.DecodeConfig in this case. + // See: https://trac.ffmpeg.org/ticket/4907 + if ret.Width == 0 || ret.Height == 0 { + logger.Warnf("FFprobe returned invalid dimensions (%dx%d) for %q, trying fallback decoder", ret.Width, ret.Height, base.Path) + c, format, err := decodeConfig(fs, base.Path) + if err != nil { + logger.Warnf("Fallback decoder failed for %q: %s. Proceeding with original FFprobe result", base.Path, err) + } else { + ret.Width = c.Width + ret.Height = c.Height + // Update format if it differs (fallback decoder may be more accurate) + if format != "" && format != ret.Format { + logger.Debugf("Updating format from %q to %q for %q", ret.Format, format, base.Path) + ret.Format = format + } + } + } + adjustForOrientation(fs, base.Path, ret) return ret, nil diff --git a/pkg/file/import.go b/pkg/file/import.go index 7c28197b8..8ca7487cb 100644 --- a/pkg/file/import.go +++ b/pkg/file/import.go @@ -120,7 +120,7 @@ func (i *Importer) baseFileJSONToBaseFile(ctx context.Context, baseJSON *jsonsch func (i *Importer) populateZipFileID(ctx context.Context, f *models.DirEntry) error { zipFilePath := i.Input.DirEntry().ZipFile if zipFilePath != "" { - zf, err := i.ReaderWriter.FindByPath(ctx, zipFilePath) + zf, err := i.ReaderWriter.FindByPath(ctx, zipFilePath, true) if err != nil { return fmt.Errorf("error finding file by path %q: %v", zipFilePath, err) } @@ -146,7 +146,7 @@ func (i *Importer) Name() string { func (i *Importer) FindExistingID(ctx context.Context) (*int, error) { path := i.Input.DirEntry().Path - existing, err := i.ReaderWriter.FindByPath(ctx, path) + existing, err := i.ReaderWriter.FindByPath(ctx, path, true) if err != nil { return nil, err } @@ -176,7 +176,7 @@ func (i *Importer) createFolderHierarchy(ctx context.Context, p string) (*models } func (i *Importer) getOrCreateFolder(ctx context.Context, path string, parent *models.Folder) (*models.Folder, error) { - folder, err := i.FolderStore.FindByPath(ctx, path) + folder, err := i.FolderStore.FindByPath(ctx, path, true) if err != nil { return nil, err } diff --git a/pkg/file/scan.go b/pkg/file/scan.go index 8b0ec956e..d9a58ad44 100644 --- a/pkg/file/scan.go +++ b/pkg/file/scan.go @@ -2,27 +2,16 @@ package file import ( "context" - "errors" "fmt" "io/fs" - "os" "path/filepath" "strings" "sync" "time" - "github.com/remeh/sizedwaitgroup" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/txn" - "github.com/stashapp/stash/pkg/utils" -) - -const ( - scanQueueSize = 200000 - // maximum number of times to retry in the event of a locked database - // use -1 to retry forever - maxRetries = -1 ) // Scanner scans files into the database. @@ -55,8 +44,26 @@ type Scanner struct { Repository Repository FingerprintCalculator FingerprintCalculator + // ZipFileExtensions is a list of file extensions that are considered zip files. + // Extension does not include the . character. + ZipFileExtensions []string + + // ScanFilters are used to determine if a file should be scanned. + ScanFilters []PathFilter + + // HandlerRequiredFilters are used to determine if an unchanged file needs to be handled + HandlerRequiredFilters []Filter + // FileDecorators are applied to files as they are scanned. FileDecorators []Decorator + + // handlers are called after a file has been scanned. + FileHandlers []Handler + + // Rescan indicates whether files should be rescanned even if they haven't changed. + Rescan bool + + folderPathToID sync.Map } // FingerprintCalculator calculates a fingerprint for the provided file. @@ -91,245 +98,18 @@ func (d *FilteredDecorator) IsMissingMetadata(ctx context.Context, fs models.FS, return false } -// ProgressReporter is used to report progress of the scan. -type ProgressReporter interface { - AddTotal(total int) - Increment() - Definite() - ExecuteTask(description string, fn func()) -} - -type scanJob struct { - *Scanner - - // handlers are called after a file has been scanned. - handlers []Handler - - ProgressReports ProgressReporter - options ScanOptions - - startTime time.Time - fileQueue chan scanFile - retryList []scanFile - retrying bool - folderPathToID sync.Map - zipPathToID sync.Map - count int - - txnRetryer txn.Retryer -} - -// ScanOptions provides options for scanning files. -type ScanOptions struct { - Paths []string - - // ZipFileExtensions is a list of file extensions that are considered zip files. - // Extension does not include the . character. - ZipFileExtensions []string - - // ScanFilters are used to determine if a file should be scanned. - ScanFilters []PathFilter - - // HandlerRequiredFilters are used to determine if an unchanged file needs to be handled - HandlerRequiredFilters []Filter - - ParallelTasks int - - // When true files in path will be rescanned even if they haven't changed - Rescan bool -} - -// Scan starts the scanning process. -func (s *Scanner) Scan(ctx context.Context, handlers []Handler, options ScanOptions, progressReporter ProgressReporter) { - job := &scanJob{ - Scanner: s, - handlers: handlers, - ProgressReports: progressReporter, - options: options, - txnRetryer: txn.Retryer{ - Manager: s.Repository.TxnManager, - Retries: maxRetries, - }, - } - - job.execute(ctx) -} - -type scanFile struct { +// ScannedFile represents a file being scanned. +type ScannedFile struct { *models.BaseFile - fs models.FS - info fs.FileInfo + FS models.FS + Info fs.FileInfo } -func (s *scanJob) withTxn(ctx context.Context, fn func(ctx context.Context) error) error { - return s.txnRetryer.WithTxn(ctx, fn) -} - -func (s *scanJob) withDB(ctx context.Context, fn func(ctx context.Context) error) error { - return s.Repository.WithDB(ctx, fn) -} - -func (s *scanJob) execute(ctx context.Context) { - paths := s.options.Paths - logger.Infof("scanning %d paths", len(paths)) - s.startTime = time.Now() - - s.fileQueue = make(chan scanFile, scanQueueSize) - var wg sync.WaitGroup - wg.Add(1) - - go func() { - defer wg.Done() - if err := s.queueFiles(ctx, paths); err != nil { - if errors.Is(err, context.Canceled) { - return - } - - logger.Errorf("error queuing files for scan: %v", err) - return - } - - logger.Infof("Finished adding files to queue. %d files queued", s.count) - }() - - defer wg.Wait() - - if err := s.processQueue(ctx); err != nil { - if errors.Is(err, context.Canceled) { - return - } - - logger.Errorf("error scanning files: %v", err) - return - } -} - -func (s *scanJob) queueFiles(ctx context.Context, paths []string) error { - var err error - s.ProgressReports.ExecuteTask("Walking directory tree", func() { - for _, p := range paths { - err = symWalk(s.FS, p, s.queueFileFunc(ctx, s.FS, nil)) - if err != nil { - return - } - } - }) - - close(s.fileQueue) - - if s.ProgressReports != nil { - s.ProgressReports.AddTotal(s.count) - s.ProgressReports.Definite() - } - - return err -} - -func (s *scanJob) queueFileFunc(ctx context.Context, f models.FS, zipFile *scanFile) fs.WalkDirFunc { - return func(path string, d fs.DirEntry, err error) error { - if err != nil { - // don't let errors prevent scanning - logger.Errorf("error scanning %s: %v", path, err) - return nil - } - - if err = ctx.Err(); err != nil { - return err - } - - info, err := d.Info() - if err != nil { - return fmt.Errorf("reading info for %q: %w", path, err) - } - - if !s.acceptEntry(ctx, path, info) { - if info.IsDir() { - return fs.SkipDir - } - - return nil - } - - size, err := getFileSize(f, path, info) - if err != nil { - return err - } - - ff := scanFile{ - BaseFile: &models.BaseFile{ - DirEntry: models.DirEntry{ - ModTime: modTime(info), - }, - Path: path, - Basename: filepath.Base(path), - Size: size, - }, - fs: f, - info: info, - } - - if zipFile != nil { - zipFileID, err := s.getZipFileID(ctx, zipFile) - if err != nil { - return err - } - ff.ZipFileID = zipFileID - ff.ZipFile = zipFile - } - - if info.IsDir() { - // handle folders immediately - if err := s.handleFolder(ctx, ff); err != nil { - if !errors.Is(err, context.Canceled) { - logger.Errorf("error processing %q: %v", path, err) - } - - // skip the directory since we won't be able to process the files anyway - return fs.SkipDir - } - - return nil - } - - // if zip file is present, we handle immediately - if zipFile != nil { - s.ProgressReports.ExecuteTask("Scanning "+path, func() { - if err := s.handleFile(ctx, ff); err != nil { - if !errors.Is(err, context.Canceled) { - logger.Errorf("error processing %q: %v", path, err) - } - // don't return an error, just skip the file - } - }) - - return nil - } - - s.fileQueue <- ff - - s.count++ - - return nil - } -} - -func getFileSize(f models.FS, path string, info fs.FileInfo) (int64, error) { - // #2196/#3042 - replace size with target size if file is a symlink - if info.Mode()&os.ModeSymlink == os.ModeSymlink { - targetInfo, err := f.Stat(path) - if err != nil { - return 0, fmt.Errorf("reading info for symlink %q: %w", path, err) - } - return targetInfo.Size(), nil - } - - return info.Size(), nil -} - -func (s *scanJob) acceptEntry(ctx context.Context, path string, info fs.FileInfo) bool { +// AcceptEntry determines if the file entry should be accepted for scanning +func (s *Scanner) AcceptEntry(ctx context.Context, path string, info fs.FileInfo) bool { // always accept if there's no filters - accept := len(s.options.ScanFilters) == 0 - for _, filter := range s.options.ScanFilters { + accept := len(s.ScanFilters) == 0 + for _, filter := range s.ScanFilters { // accept if any filter accepts the file if filter.Accept(ctx, path, info) { accept = true @@ -340,109 +120,17 @@ func (s *scanJob) acceptEntry(ctx context.Context, path string, info fs.FileInfo return accept } -func (s *scanJob) scanZipFile(ctx context.Context, f scanFile) error { - zipFS, err := f.fs.OpenZip(f.Path, f.Size) - if err != nil { - if errors.Is(err, errNotReaderAt) { - // can't walk the zip file - // just return - return nil - } - - return err - } - - defer zipFS.Close() - - return symWalk(zipFS, f.Path, s.queueFileFunc(ctx, zipFS, &f)) -} - -func (s *scanJob) processQueue(ctx context.Context) error { - parallelTasks := s.options.ParallelTasks - if parallelTasks < 1 { - parallelTasks = 1 - } - - wg := sizedwaitgroup.New(parallelTasks) - - if err := func() error { - defer wg.Wait() - - for f := range s.fileQueue { - if err := ctx.Err(); err != nil { - return err - } - - wg.Add() - ff := f - go func() { - defer wg.Done() - s.processQueueItem(ctx, ff) - }() - } - - return nil - }(); err != nil { - return err - } - - s.retrying = true - - if err := func() error { - defer wg.Wait() - - for _, f := range s.retryList { - if err := ctx.Err(); err != nil { - return err - } - - wg.Add() - ff := f - go func() { - defer wg.Done() - s.processQueueItem(ctx, ff) - }() - } - - return nil - }(); err != nil { - return err - } - - return nil -} - -func (s *scanJob) incrementProgress(f scanFile) { - // don't increment for files inside zip files since these aren't - // counted during the initial walking - if s.ProgressReports != nil && f.ZipFile == nil { - s.ProgressReports.Increment() - } -} - -func (s *scanJob) processQueueItem(ctx context.Context, f scanFile) { - s.ProgressReports.ExecuteTask("Scanning "+f.Path, func() { - var err error - if f.info.IsDir() { - err = s.handleFolder(ctx, f) - } else { - err = s.handleFile(ctx, f) - } - - if err != nil && !errors.Is(err, context.Canceled) { - logger.Errorf("error processing %q: %v", f.Path, err) - } - }) -} - -func (s *scanJob) getFolderID(ctx context.Context, path string) (*models.FolderID, error) { +func (s *Scanner) getFolderID(ctx context.Context, path string) (*models.FolderID, error) { // check the folder cache first if f, ok := s.folderPathToID.Load(path); ok { v := f.(models.FolderID) return &v, nil } - ret, err := s.Repository.Folder.FindByPath(ctx, path) + // assume case sensitive when searching for the folder + const caseSensitive = true + + ret, err := s.Repository.Folder.FindByPath(ctx, path, caseSensitive) if err != nil { return nil, err } @@ -455,48 +143,35 @@ func (s *scanJob) getFolderID(ctx context.Context, path string) (*models.FolderI return &ret.ID, nil } -func (s *scanJob) getZipFileID(ctx context.Context, zipFile *scanFile) (*models.FileID, error) { - if zipFile == nil { - return nil, nil - } - - if zipFile.ID != 0 { - return &zipFile.ID, nil - } - - path := zipFile.Path - - // check the folder cache first - if f, ok := s.zipPathToID.Load(path); ok { - v := f.(models.FileID) - return &v, nil - } - - ret, err := s.Repository.File.FindByPath(ctx, path) - if err != nil { - return nil, fmt.Errorf("getting zip file ID for %q: %w", path, err) - } - - if ret == nil { - return nil, fmt.Errorf("zip file %q doesn't exist in database", zipFile.Path) - } - - s.zipPathToID.Store(path, ret.Base().ID) - return &ret.Base().ID, nil -} - -func (s *scanJob) handleFolder(ctx context.Context, file scanFile) error { +// ScanFolder scans the provided folder into the database, returning the folder entry. +// If the folder already exists, it is updated if necessary. +func (s *Scanner) ScanFolder(ctx context.Context, file ScannedFile) (*models.Folder, error) { + var f *models.Folder + var err error path := file.Path - return s.withTxn(ctx, func(ctx context.Context) error { - defer s.incrementProgress(file) - + err = s.Repository.WithTxn(ctx, func(ctx context.Context) error { // determine if folder already exists in data store (by path) - f, err := s.Repository.Folder.FindByPath(ctx, path) + // assume case sensitive by default + f, err = s.Repository.Folder.FindByPath(ctx, path, true) if err != nil { return fmt.Errorf("checking for existing folder %q: %w", path, err) } + // #1426 / #6326 - if folder is in a case-insensitive filesystem, then try + // case insensitive searching + // assume case sensitive if in zip + if f == nil && file.ZipFileID == nil { + caseSensitive, _ := file.FS.IsPathCaseSensitive(file.Path) + + if !caseSensitive { + f, err = s.Repository.Folder.FindByPath(ctx, path, false) + if err != nil { + return fmt.Errorf("checking for existing folder %q: %w", path, err) + } + } + } + // if folder not exists, create it if f == nil { f, err = s.onNewFolder(ctx, file) @@ -514,9 +189,11 @@ func (s *scanJob) handleFolder(ctx context.Context, file scanFile) error { return nil }) + + return f, err } -func (s *scanJob) onNewFolder(ctx context.Context, file scanFile) (*models.Folder, error) { +func (s *Scanner) onNewFolder(ctx context.Context, file ScannedFile) (*models.Folder, error) { renamed, err := s.handleFolderRename(ctx, file) if err != nil { return nil, err @@ -563,7 +240,7 @@ func (s *scanJob) onNewFolder(ctx context.Context, file scanFile) (*models.Folde return toCreate, nil } -func (s *scanJob) handleFolderRename(ctx context.Context, file scanFile) (*models.Folder, error) { +func (s *Scanner) handleFolderRename(ctx context.Context, file ScannedFile) (*models.Folder, error) { // ignore folders in zip files if file.ZipFileID != nil { return nil, nil @@ -604,16 +281,24 @@ func (s *scanJob) handleFolderRename(ctx context.Context, file scanFile) (*model return renamedFrom, nil } -func (s *scanJob) onExistingFolder(ctx context.Context, f scanFile, existing *models.Folder) (*models.Folder, error) { +func (s *Scanner) onExistingFolder(ctx context.Context, f ScannedFile, existing *models.Folder) (*models.Folder, error) { update := false // update if mod time is changed entryModTime := f.ModTime if !entryModTime.Equal(existing.ModTime) { + existing.Path = f.Path existing.ModTime = entryModTime update = true } + // #6326 - update if path has changed - should only happen if case is + // changed and filesystem is case insensitive + if existing.Path != f.Path { + existing.Path = f.Path + update = true + } + // update if zip file ID has changed fZfID := f.ZipFileID existingZfID := existing.ZipFileID @@ -637,55 +322,59 @@ func (s *scanJob) onExistingFolder(ctx context.Context, f scanFile, existing *mo return existing, nil } -func modTime(info fs.FileInfo) time.Time { - // truncate to seconds, since we don't store beyond that in the database - return info.ModTime().Truncate(time.Second) +type ScanFileResult struct { + File models.File + New bool + Renamed bool + Updated bool } -func (s *scanJob) handleFile(ctx context.Context, f scanFile) error { - defer s.incrementProgress(f) +// ScanFile scans the provided file into the database, returning the scan result. +func (s *Scanner) ScanFile(ctx context.Context, f ScannedFile) (*ScanFileResult, error) { + var r *ScanFileResult - var ff models.File // don't use a transaction to check if new or existing - if err := s.withDB(ctx, func(ctx context.Context) error { + if err := s.Repository.WithDB(ctx, func(ctx context.Context) error { // determine if file already exists in data store - var err error - ff, err = s.Repository.File.FindByPath(ctx, f.Path) + // assume case sensitive when searching for the file to begin with + ff, err := s.Repository.File.FindByPath(ctx, f.Path, true) if err != nil { return fmt.Errorf("checking for existing file %q: %w", f.Path, err) } + // #1426 / #6326 - if file is in a case-insensitive filesystem, then try + // case insensitive search + // assume case sensitive if in zip + if ff == nil && f.ZipFileID != nil { + caseSensitive, _ := f.FS.IsPathCaseSensitive(f.Path) + + if !caseSensitive { + ff, err = s.Repository.File.FindByPath(ctx, f.Path, false) + if err != nil { + return fmt.Errorf("checking for existing file %q: %w", f.Path, err) + } + } + } + if ff == nil { // returns a file only if it is actually new - ff, err = s.onNewFile(ctx, f) + r, err = s.onNewFile(ctx, f) return err } - ff, err = s.onExistingFile(ctx, f, ff) + r, err = s.onExistingFile(ctx, f, ff) return err }); err != nil { - return err + return nil, err } - if ff != nil && s.isZipFile(f.info.Name()) { - f.BaseFile = ff.Base() - - // scan zip files with a different context that is not cancellable - // cancelling while scanning zip file contents results in the scan - // contents being partially completed - zipCtx := utils.ValueOnlyContext{Context: ctx} - - if err := s.scanZipFile(zipCtx, f); err != nil { - logger.Errorf("Error scanning zip file %q: %v", f.Path, err) - } - } - - return nil + return r, nil } -func (s *scanJob) isZipFile(path string) bool { +// IsZipFile determines if the provided path is a zip file based on its extension. +func (s *Scanner) IsZipFile(path string) bool { fExt := filepath.Ext(path) - for _, ext := range s.options.ZipFileExtensions { + for _, ext := range s.ZipFileExtensions { if strings.EqualFold(fExt, "."+ext) { return true } @@ -694,7 +383,7 @@ func (s *scanJob) isZipFile(path string) bool { return false } -func (s *scanJob) onNewFile(ctx context.Context, f scanFile) (models.File, error) { +func (s *Scanner) onNewFile(ctx context.Context, f ScannedFile) (*ScanFileResult, error) { now := time.Now() baseFile := f.BaseFile @@ -710,28 +399,20 @@ func (s *scanJob) onNewFile(ctx context.Context, f scanFile) (models.File, error } if parentFolderID == nil { - // if parent folder doesn't exist, assume it's not yet created - // add this file to the queue to be created later - if s.retrying { - // if we're retrying and the folder still doesn't exist, then it's a problem - return nil, fmt.Errorf("parent folder for %q doesn't exist", path) - } - - s.retryList = append(s.retryList, f) - return nil, nil + return nil, fmt.Errorf("parent folder for %q doesn't exist", path) } baseFile.ParentFolderID = *parentFolderID const useExisting = false - fp, err := s.calculateFingerprints(f.fs, baseFile, path, useExisting) + fp, err := s.calculateFingerprints(f.FS, baseFile, path, useExisting) if err != nil { return nil, err } baseFile.SetFingerprints(fp) - file, err := s.fireDecorators(ctx, f.fs, baseFile) + file, err := s.fireDecorators(ctx, f.FS, baseFile) if err != nil { return nil, err } @@ -744,14 +425,17 @@ func (s *scanJob) onNewFile(ctx context.Context, f scanFile) (models.File, error } if renamed != nil { + return &ScanFileResult{ + File: renamed, + Renamed: true, + }, nil // handle rename should have already handled the contents of the zip file // so shouldn't need to scan it again // return nil so it doesn't - return nil, nil } // if not renamed, queue file for creation - if err := s.withTxn(ctx, func(ctx context.Context) error { + if err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { if err := s.Repository.File.Create(ctx, file); err != nil { return fmt.Errorf("creating file %q: %w", path, err) } @@ -765,10 +449,13 @@ func (s *scanJob) onNewFile(ctx context.Context, f scanFile) (models.File, error return nil, err } - return file, nil + return &ScanFileResult{ + File: file, + New: true, + }, nil } -func (s *scanJob) fireDecorators(ctx context.Context, fs models.FS, f models.File) (models.File, error) { +func (s *Scanner) fireDecorators(ctx context.Context, fs models.FS, f models.File) (models.File, error) { for _, h := range s.FileDecorators { var err error f, err = h.Decorate(ctx, fs, f) @@ -780,8 +467,8 @@ func (s *scanJob) fireDecorators(ctx context.Context, fs models.FS, f models.Fil return f, nil } -func (s *scanJob) fireHandlers(ctx context.Context, f models.File, oldFile models.File) error { - for _, h := range s.handlers { +func (s *Scanner) fireHandlers(ctx context.Context, f models.File, oldFile models.File) error { + for _, h := range s.FileHandlers { if err := h.Handle(ctx, f, oldFile); err != nil { return err } @@ -790,7 +477,7 @@ func (s *scanJob) fireHandlers(ctx context.Context, f models.File, oldFile model return nil } -func (s *scanJob) calculateFingerprints(fs models.FS, f *models.BaseFile, path string, useExisting bool) (models.Fingerprints, error) { +func (s *Scanner) calculateFingerprints(fs models.FS, f *models.BaseFile, path string, useExisting bool) (models.Fingerprints, error) { // only log if we're (re)calculating fingerprints if !useExisting { logger.Infof("Calculating fingerprints for %s ...", path) @@ -827,7 +514,7 @@ func appendFileUnique(v []models.File, toAdd []models.File) []models.File { return v } -func (s *scanJob) getFileFS(f *models.BaseFile) (models.FS, error) { +func (s *Scanner) getFileFS(f *models.BaseFile) (models.FS, error) { if f.ZipFile == nil { return s.FS, nil } @@ -838,10 +525,11 @@ func (s *scanJob) getFileFS(f *models.BaseFile) (models.FS, error) { } zipPath := f.ZipFile.Base().Path - return fs.OpenZip(zipPath, f.Size) + zipSize := f.ZipFile.Base().Size + return fs.OpenZip(zipPath, zipSize) } -func (s *scanJob) handleRename(ctx context.Context, f models.File, fp []models.Fingerprint) (models.File, error) { +func (s *Scanner) handleRename(ctx context.Context, f models.File, fp []models.Fingerprint) (models.File, error) { var others []models.File for _, tfp := range fp { @@ -878,11 +566,12 @@ func (s *scanJob) handleRename(ctx context.Context, f models.File, fp []models.F // #1426 - if file exists but is a case-insensitive match for the // original filename, and the filesystem is case-insensitive // then treat it as a move + // #6326 - this should now be handled earlier, and this shouldn't be necessary if caseSensitive, _ := fs.IsPathCaseSensitive(other.Base().Path); !caseSensitive { // treat as a move missing = append(missing, other) } - case !s.acceptEntry(ctx, other.Base().Path, info): + case !s.AcceptEntry(ctx, other.Base().Path, info): // #4393 - if the file is no longer in the configured library paths, treat it as a move logger.Debugf("File %q no longer in library paths. Treating as a move.", other.Base().Path) missing = append(missing, other) @@ -915,12 +604,12 @@ func (s *scanJob) handleRename(ctx context.Context, f models.File, fp []models.F fBaseCopy.Fingerprints = updatedBase.Fingerprints *updatedBase = fBaseCopy - if err := s.withTxn(ctx, func(ctx context.Context) error { + if err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { if err := s.Repository.File.Update(ctx, updated); err != nil { return fmt.Errorf("updating file for rename %q: %w", newPath, err) } - if s.isZipFile(updatedBase.Basename) { + if s.IsZipFile(updatedBase.Basename) { if err := transferZipHierarchy(ctx, s.Repository.Folder, s.Repository.File, updatedBase.ID, oldPath, newPath); err != nil { return fmt.Errorf("moving zip hierarchy for renamed zip file %q: %w", newPath, err) } @@ -938,9 +627,9 @@ func (s *scanJob) handleRename(ctx context.Context, f models.File, fp []models.F return updated, nil } -func (s *scanJob) isHandlerRequired(ctx context.Context, f models.File) bool { - accept := len(s.options.HandlerRequiredFilters) == 0 - for _, filter := range s.options.HandlerRequiredFilters { +func (s *Scanner) isHandlerRequired(ctx context.Context, f models.File) bool { + accept := len(s.HandlerRequiredFilters) == 0 + for _, filter := range s.HandlerRequiredFilters { // accept if any filter accepts the file if filter.Accept(ctx, f) { accept = true @@ -959,9 +648,9 @@ func (s *scanJob) isHandlerRequired(ctx context.Context, f models.File) bool { // - file size // - image format, width or height // - video codec, audio codec, format, width, height, framerate or bitrate -func (s *scanJob) isMissingMetadata(ctx context.Context, f scanFile, existing models.File) bool { +func (s *Scanner) isMissingMetadata(ctx context.Context, f ScannedFile, existing models.File) bool { for _, h := range s.FileDecorators { - if h.IsMissingMetadata(ctx, f.fs, existing) { + if h.IsMissingMetadata(ctx, f.FS, existing) { return true } } @@ -969,20 +658,20 @@ func (s *scanJob) isMissingMetadata(ctx context.Context, f scanFile, existing mo return false } -func (s *scanJob) setMissingMetadata(ctx context.Context, f scanFile, existing models.File) (models.File, error) { +func (s *Scanner) setMissingMetadata(ctx context.Context, f ScannedFile, existing models.File) (models.File, error) { path := existing.Base().Path logger.Infof("Updating metadata for %s", path) existing.Base().Size = f.Size var err error - existing, err = s.fireDecorators(ctx, f.fs, existing) + existing, err = s.fireDecorators(ctx, f.FS, existing) if err != nil { return nil, err } // queue file for update - if err := s.withTxn(ctx, func(ctx context.Context) error { + if err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { if err := s.Repository.File.Update(ctx, existing); err != nil { return fmt.Errorf("updating file %q: %w", path, err) } @@ -995,9 +684,9 @@ func (s *scanJob) setMissingMetadata(ctx context.Context, f scanFile, existing m return existing, nil } -func (s *scanJob) setMissingFingerprints(ctx context.Context, f scanFile, existing models.File) (models.File, error) { +func (s *Scanner) setMissingFingerprints(ctx context.Context, f ScannedFile, existing models.File) (models.File, error) { const useExisting = true - fp, err := s.calculateFingerprints(f.fs, existing.Base(), f.Path, useExisting) + fp, err := s.calculateFingerprints(f.FS, existing.Base(), f.Path, useExisting) if err != nil { return nil, err } @@ -1005,7 +694,7 @@ func (s *scanJob) setMissingFingerprints(ctx context.Context, f scanFile, existi if fp.ContentsChanged(existing.Base().Fingerprints) { existing.SetFingerprints(fp) - if err := s.withTxn(ctx, func(ctx context.Context) error { + if err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { if err := s.Repository.File.Update(ctx, existing); err != nil { return fmt.Errorf("updating file %q: %w", f.Path, err) } @@ -1020,13 +709,14 @@ func (s *scanJob) setMissingFingerprints(ctx context.Context, f scanFile, existi } // returns a file only if it was updated -func (s *scanJob) onExistingFile(ctx context.Context, f scanFile, existing models.File) (models.File, error) { +func (s *Scanner) onExistingFile(ctx context.Context, f ScannedFile, existing models.File) (*ScanFileResult, error) { base := existing.Base() path := base.Path fileModTime := f.ModTime - updated := !fileModTime.Equal(base.ModTime) - forceRescan := s.options.Rescan + // #6326 - also force a rescan if the basename changed + updated := !fileModTime.Equal(base.ModTime) || base.Basename != f.Basename + forceRescan := s.Rescan if !updated && !forceRescan { return s.onUnchangedFile(ctx, f, existing) @@ -1040,13 +730,15 @@ func (s *scanJob) onExistingFile(ctx context.Context, f scanFile, existing model logger.Infof("%s has been updated: rescanning", path) } + // #6326 - update basename in case it changed + base.Basename = f.Basename base.ModTime = fileModTime base.Size = f.Size base.UpdatedAt = time.Now() // calculate and update fingerprints for the file const useExisting = false - fp, err := s.calculateFingerprints(f.fs, base, path, useExisting) + fp, err := s.calculateFingerprints(f.FS, base, path, useExisting) if err != nil { return nil, err } @@ -1054,13 +746,13 @@ func (s *scanJob) onExistingFile(ctx context.Context, f scanFile, existing model s.removeOutdatedFingerprints(existing, fp) existing.SetFingerprints(fp) - existing, err = s.fireDecorators(ctx, f.fs, existing) + existing, err = s.fireDecorators(ctx, f.FS, existing) if err != nil { return nil, err } // queue file for update - if err := s.withTxn(ctx, func(ctx context.Context) error { + if err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { if err := s.Repository.File.Update(ctx, existing); err != nil { return fmt.Errorf("updating file %q: %w", path, err) } @@ -1073,11 +765,13 @@ func (s *scanJob) onExistingFile(ctx context.Context, f scanFile, existing model }); err != nil { return nil, err } - - return existing, nil + return &ScanFileResult{ + File: existing, + Updated: true, + }, nil } -func (s *scanJob) removeOutdatedFingerprints(existing models.File, fp models.Fingerprints) { +func (s *Scanner) removeOutdatedFingerprints(existing models.File, fp models.Fingerprints) { // HACK - if no MD5 fingerprint was returned, and the oshash is changed // then remove the MD5 fingerprint oshash := fp.For(models.FingerprintTypeOshash) @@ -1105,7 +799,7 @@ func (s *scanJob) removeOutdatedFingerprints(existing models.File, fp models.Fin } // returns a file only if it was updated -func (s *scanJob) onUnchangedFile(ctx context.Context, f scanFile, existing models.File) (models.File, error) { +func (s *Scanner) onUnchangedFile(ctx context.Context, f ScannedFile, existing models.File) (*ScanFileResult, error) { var err error isMissingMetdata := s.isMissingMetadata(ctx, f, existing) @@ -1124,7 +818,7 @@ func (s *scanJob) onUnchangedFile(ctx context.Context, f scanFile, existing mode } handlerRequired := false - if err := s.withDB(ctx, func(ctx context.Context) error { + if err := s.Repository.WithDB(ctx, func(ctx context.Context) error { // check if the handler needs to be run handlerRequired = s.isHandlerRequired(ctx, existing) return nil @@ -1134,15 +828,20 @@ func (s *scanJob) onUnchangedFile(ctx context.Context, f scanFile, existing mode if !handlerRequired { // if this file is a zip file, then we need to rescan the contents - // as well. We do this by returning the file, instead of nil. + // as well. We do this by indicating that the file is updated. if isMissingMetdata { - return existing, nil + return &ScanFileResult{ + File: existing, + Updated: true, + }, nil } - return nil, nil + return &ScanFileResult{ + File: existing, + }, nil } - if err := s.withTxn(ctx, func(ctx context.Context) error { + if err := s.Repository.WithTxn(ctx, func(ctx context.Context) error { if err := s.fireHandlers(ctx, existing, nil); err != nil { return err } @@ -1153,6 +852,9 @@ func (s *scanJob) onUnchangedFile(ctx context.Context, f scanFile, existing mode } // if this file is a zip file, then we need to rescan the contents - // as well. We do this by returning the file, instead of nil. - return existing, nil + // as well. We do this by indicating that the file is updated. + return &ScanFileResult{ + File: existing, + Updated: true, + }, nil } diff --git a/pkg/file/video/caption.go b/pkg/file/video/caption.go index bec3db6fd..43723864f 100644 --- a/pkg/file/video/caption.go +++ b/pkg/file/video/caption.go @@ -97,7 +97,7 @@ func AssociateCaptions(ctx context.Context, captionPath string, txnMgr txn.Manag captionPrefix := getCaptionPrefix(captionPath) if err := txn.WithTxn(ctx, txnMgr, func(ctx context.Context) error { var err error - files, er := fqb.FindAllByPath(ctx, captionPrefix+"*") + files, er := fqb.FindAllByPath(ctx, captionPrefix+"*", true) if er != nil { return fmt.Errorf("searching for scene %s: %w", captionPrefix, er) diff --git a/pkg/file/walk.go b/pkg/file/walk.go index 3c6a157b7..bd33f42c3 100644 --- a/pkg/file/walk.go +++ b/pkg/file/walk.go @@ -81,8 +81,8 @@ func walkSym(f models.FS, filename string, linkDirname string, walkFn fs.WalkDir return fsWalk(f, filename, symWalkFunc) } -// symWalk extends filepath.Walk to also follow symlinks -func symWalk(fs models.FS, path string, walkFn fs.WalkDirFunc) error { +// SymWalk extends filepath.Walk to also follow symlinks +func SymWalk(fs models.FS, path string, walkFn fs.WalkDirFunc) error { return walkSym(fs, path, path, walkFn) } diff --git a/pkg/file/zip.go b/pkg/file/zip.go index 4df2453dc..5afcd5329 100644 --- a/pkg/file/zip.go +++ b/pkg/file/zip.go @@ -18,7 +18,7 @@ import ( ) var ( - errNotReaderAt = errors.New("not a ReaderAt") + ErrNotReaderAt = errors.New("invalid reader: does not implement io.ReaderAt") errZipFSOpenZip = errors.New("cannot open zip file inside zip file") ) @@ -38,7 +38,7 @@ func newZipFS(fs models.FS, path string, size int64) (*zipFS, error) { asReaderAt, _ := reader.(io.ReaderAt) if asReaderAt == nil { reader.Close() - return nil, errNotReaderAt + return nil, ErrNotReaderAt } zipReader, err := zip.NewReader(asReaderAt, size) diff --git a/pkg/fsutil/trash.go b/pkg/fsutil/trash.go new file mode 100644 index 000000000..9a3bed835 --- /dev/null +++ b/pkg/fsutil/trash.go @@ -0,0 +1,43 @@ +package fsutil + +import ( + "fmt" + "os" + "path/filepath" + "time" +) + +// MoveToTrash moves a file or directory to a custom trash directory. +// If a file with the same name already exists in the trash, a timestamp is appended. +// Returns the destination path where the file was moved to. +func MoveToTrash(sourcePath string, trashPath string) (string, error) { + // Get absolute path for the source + absSourcePath, err := filepath.Abs(sourcePath) + if err != nil { + return "", fmt.Errorf("failed to get absolute path: %w", err) + } + + // Ensure trash directory exists + if err := os.MkdirAll(trashPath, 0755); err != nil { + return "", fmt.Errorf("failed to create trash directory: %w", err) + } + + // Get the base name of the file/directory + baseName := filepath.Base(absSourcePath) + destPath := filepath.Join(trashPath, baseName) + + // If a file with the same name already exists in trash, append timestamp + if _, err := os.Stat(destPath); err == nil { + ext := filepath.Ext(baseName) + nameWithoutExt := baseName[:len(baseName)-len(ext)] + timestamp := time.Now().Format("20060102-150405") + destPath = filepath.Join(trashPath, fmt.Sprintf("%s_%s%s", nameWithoutExt, timestamp, ext)) + } + + // Move the file to trash using SafeMove to support cross-filesystem moves + if err := SafeMove(absSourcePath, destPath); err != nil { + return "", fmt.Errorf("failed to move to trash: %w", err) + } + + return destPath, nil +} diff --git a/pkg/gallery/delete.go b/pkg/gallery/delete.go index f5186f948..4bc2e2492 100644 --- a/pkg/gallery/delete.go +++ b/pkg/gallery/delete.go @@ -8,13 +8,13 @@ import ( "github.com/stashapp/stash/pkg/models" ) -func (s *Service) Destroy(ctx context.Context, i *models.Gallery, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) ([]*models.Image, error) { +func (s *Service) Destroy(ctx context.Context, i *models.Gallery, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile, destroyFileEntry bool) ([]*models.Image, error) { var imgsDestroyed []*models.Image // chapter deletion is done via delete cascade, so we don't need to do anything here // if this is a zip-based gallery, delete the images as well first - zipImgsDestroyed, err := s.destroyZipFileImages(ctx, i, fileDeleter, deleteGenerated, deleteFile) + zipImgsDestroyed, err := s.destroyZipFileImages(ctx, i, fileDeleter, deleteGenerated, deleteFile, destroyFileEntry) if err != nil { return nil, err } @@ -45,7 +45,7 @@ func DestroyChapter(ctx context.Context, galleryChapter *models.GalleryChapter, return qb.Destroy(ctx, galleryChapter.ID) } -func (s *Service) destroyZipFileImages(ctx context.Context, i *models.Gallery, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) ([]*models.Image, error) { +func (s *Service) destroyZipFileImages(ctx context.Context, i *models.Gallery, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile, destroyFileEntry bool) ([]*models.Image, error) { if err := i.LoadFiles(ctx, s.Repository); err != nil { return nil, err } @@ -81,6 +81,12 @@ func (s *Service) destroyZipFileImages(ctx context.Context, i *models.Gallery, f if err := destroyer.DestroyZip(ctx, f, fileDeleter.Deleter, deleteFile); err != nil { return nil, err } + } else if destroyFileEntry { + // destroy file DB entry without deleting filesystem file + const deleteFileFromFS = false + if err := destroyer.DestroyZip(ctx, f, nil, deleteFileFromFS); err != nil { + return nil, err + } } } diff --git a/pkg/gallery/import.go b/pkg/gallery/import.go index 7cdf53691..543d4cf48 100644 --- a/pkg/gallery/import.go +++ b/pkg/gallery/import.go @@ -126,7 +126,7 @@ func (i *Importer) populateStudio(ctx context.Context) error { } func (i *Importer) createStudio(ctx context.Context, name string) (int, error) { - newStudio := models.NewStudio() + newStudio := models.NewCreateStudioInput() newStudio.Name = name err := i.StudioWriter.Create(ctx, &newStudio) @@ -265,7 +265,7 @@ func (i *Importer) populateFilesFolder(ctx context.Context) error { for _, ref := range i.Input.ZipFiles { path := ref - f, err := i.FileFinder.FindByPath(ctx, path) + f, err := i.FileFinder.FindByPath(ctx, path, true) if err != nil { return fmt.Errorf("error finding file: %w", err) } @@ -281,7 +281,7 @@ func (i *Importer) populateFilesFolder(ctx context.Context) error { if i.Input.FolderPath != "" { path := i.Input.FolderPath - f, err := i.FolderFinder.FindByPath(ctx, path) + f, err := i.FolderFinder.FindByPath(ctx, path, true) if err != nil { return fmt.Errorf("error finding folder: %w", err) } diff --git a/pkg/gallery/import_test.go b/pkg/gallery/import_test.go index b64f80d8f..4248f51bc 100644 --- a/pkg/gallery/import_test.go +++ b/pkg/gallery/import_test.go @@ -115,9 +115,9 @@ func TestImporterPreImportWithMissingStudio(t *testing.T) { } db.Studio.On("FindByName", testCtx, missingStudioName, false).Return(nil, nil).Times(3) - db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.Studio")).Run(func(args mock.Arguments) { - s := args.Get(1).(*models.Studio) - s.ID = existingStudioID + db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.CreateStudioInput")).Run(func(args mock.Arguments) { + s := args.Get(1).(*models.CreateStudioInput) + s.Studio.ID = existingStudioID }).Return(nil) err := i.PreImport(testCtx) @@ -147,7 +147,7 @@ func TestImporterPreImportWithMissingStudioCreateErr(t *testing.T) { } db.Studio.On("FindByName", testCtx, missingStudioName, false).Return(nil, nil).Once() - db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.Studio")).Return(errors.New("Create error")) + db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.CreateStudioInput")).Return(errors.New("Create error")) err := i.PreImport(testCtx) assert.NotNil(t, err) diff --git a/pkg/gallery/service.go b/pkg/gallery/service.go index 62604e0c5..5b2678480 100644 --- a/pkg/gallery/service.go +++ b/pkg/gallery/service.go @@ -16,7 +16,7 @@ type ImageFinder interface { } type ImageService interface { - Destroy(ctx context.Context, i *models.Image, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) error + Destroy(ctx context.Context, i *models.Image, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile, destroyFileEntry bool) error DestroyZipImages(ctx context.Context, zipFile models.File, fileDeleter *image.FileDeleter, deleteGenerated bool) ([]*models.Image, error) DestroyFolderImages(ctx context.Context, folderID models.FolderID, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) ([]*models.Image, error) } diff --git a/pkg/group/import.go b/pkg/group/import.go index 3fc7db8f1..a73c3998e 100644 --- a/pkg/group/import.go +++ b/pkg/group/import.go @@ -203,7 +203,7 @@ func (i *Importer) populateStudio(ctx context.Context) error { } func (i *Importer) createStudio(ctx context.Context, name string) (int, error) { - newStudio := models.NewStudio() + newStudio := models.NewCreateStudioInput() newStudio.Name = name err := i.StudioWriter.Create(ctx, &newStudio) diff --git a/pkg/group/import_test.go b/pkg/group/import_test.go index c4ca47442..50b8b2dd1 100644 --- a/pkg/group/import_test.go +++ b/pkg/group/import_test.go @@ -121,9 +121,9 @@ func TestImporterPreImportWithMissingStudio(t *testing.T) { } db.Studio.On("FindByName", testCtx, missingStudioName, false).Return(nil, nil).Times(3) - db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.Studio")).Run(func(args mock.Arguments) { - s := args.Get(1).(*models.Studio) - s.ID = existingStudioID + db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.CreateStudioInput")).Run(func(args mock.Arguments) { + s := args.Get(1).(*models.CreateStudioInput) + s.Studio.ID = existingStudioID }).Return(nil) err := i.PreImport(testCtx) @@ -156,7 +156,7 @@ func TestImporterPreImportWithMissingStudioCreateErr(t *testing.T) { } db.Studio.On("FindByName", testCtx, missingStudioName, false).Return(nil, nil).Once() - db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.Studio")).Return(errors.New("Create error")) + db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.CreateStudioInput")).Return(errors.New("Create error")) err := i.PreImport(testCtx) assert.NotNil(t, err) diff --git a/pkg/hash/imagephash/phash.go b/pkg/hash/imagephash/phash.go new file mode 100644 index 000000000..4cf6e9209 --- /dev/null +++ b/pkg/hash/imagephash/phash.go @@ -0,0 +1,48 @@ +package imagephash + +import ( + "bytes" + "fmt" + "image" + + "github.com/corona10/goimagehash" + "github.com/stashapp/stash/pkg/file" + "github.com/stashapp/stash/pkg/models" +) + +// Generate computes a perceptual hash for an image file. +func Generate(imageFile *models.ImageFile) (*uint64, error) { + img, err := loadImage(imageFile) + if err != nil { + return nil, fmt.Errorf("loading image: %w", err) + } + + hash, err := goimagehash.PerceptionHash(img) + if err != nil { + return nil, fmt.Errorf("computing phash from image: %w", err) + } + + hashValue := hash.GetHash() + return &hashValue, nil +} + +// loadImage loads an image from disk and decodes it. +func loadImage(imageFile *models.ImageFile) (image.Image, error) { + reader, err := imageFile.Open(&file.OsFS{}) + if err != nil { + return nil, err + } + defer reader.Close() + + buf := new(bytes.Buffer) + if _, err := buf.ReadFrom(reader); err != nil { + return nil, err + } + + img, _, err := image.Decode(buf) + if err != nil { + return nil, fmt.Errorf("decoding image: %w", err) + } + + return img, nil +} diff --git a/pkg/image/delete.go b/pkg/image/delete.go index 69fba9bd6..28bb54a59 100644 --- a/pkg/image/delete.go +++ b/pkg/image/delete.go @@ -19,6 +19,7 @@ type FileDeleter struct { } // MarkGeneratedFiles marks for deletion the generated files for the provided image. +// Generated files bypass trash and are permanently deleted since they can be regenerated. func (d *FileDeleter) MarkGeneratedFiles(image *models.Image) error { var files []string thumbPath := d.Paths.Generated.GetThumbnailPath(image.Checksum, models.DefaultGthumbWidth) @@ -32,12 +33,12 @@ func (d *FileDeleter) MarkGeneratedFiles(image *models.Image) error { files = append(files, prevPath) } - return d.Files(files) + return d.FilesWithoutTrash(files) } // Destroy destroys an image, optionally marking the file and generated files for deletion. -func (s *Service) Destroy(ctx context.Context, i *models.Image, fileDeleter *FileDeleter, deleteGenerated, deleteFile bool) error { - return s.destroyImage(ctx, i, fileDeleter, deleteGenerated, deleteFile) +func (s *Service) Destroy(ctx context.Context, i *models.Image, fileDeleter *FileDeleter, deleteGenerated, deleteFile, destroyFileEntry bool) error { + return s.destroyImage(ctx, i, fileDeleter, deleteGenerated, deleteFile, destroyFileEntry) } // DestroyZipImages destroys all images in zip, optionally marking the files and generated files for deletion. @@ -74,7 +75,8 @@ func (s *Service) DestroyZipImages(ctx context.Context, zipFile models.File, fil } const deleteFileInZip = false - if err := s.destroyImage(ctx, img, fileDeleter, deleteGenerated, deleteFileInZip); err != nil { + const destroyFileEntry = false + if err := s.destroyImage(ctx, img, fileDeleter, deleteGenerated, deleteFileInZip, destroyFileEntry); err != nil { return nil, err } @@ -134,7 +136,8 @@ func (s *Service) DestroyFolderImages(ctx context.Context, folderID models.Folde continue } - if err := s.Destroy(ctx, img, fileDeleter, deleteGenerated, deleteFile); err != nil { + const destroyFileEntry = false + if err := s.Destroy(ctx, img, fileDeleter, deleteGenerated, deleteFile, destroyFileEntry); err != nil { return nil, err } @@ -145,11 +148,15 @@ func (s *Service) DestroyFolderImages(ctx context.Context, folderID models.Folde } // Destroy destroys an image, optionally marking the file and generated files for deletion. -func (s *Service) destroyImage(ctx context.Context, i *models.Image, fileDeleter *FileDeleter, deleteGenerated, deleteFile bool) error { +func (s *Service) destroyImage(ctx context.Context, i *models.Image, fileDeleter *FileDeleter, deleteGenerated, deleteFile, destroyFileEntry bool) error { if deleteFile { if err := s.deleteFiles(ctx, i, fileDeleter); err != nil { return err } + } else if destroyFileEntry { + if err := s.destroyFileEntries(ctx, i); err != nil { + return err + } } if deleteGenerated { @@ -191,3 +198,35 @@ func (s *Service) deleteFiles(ctx context.Context, i *models.Image, fileDeleter return nil } + +// destroyFileEntries destroys file entries from the database without deleting +// the files from the filesystem +func (s *Service) destroyFileEntries(ctx context.Context, i *models.Image) error { + if err := i.LoadFiles(ctx, s.Repository); err != nil { + return err + } + + for _, f := range i.Files.List() { + // only destroy file entries where there is no other associated image + otherImages, err := s.Repository.FindByFileID(ctx, f.Base().ID) + if err != nil { + return err + } + + if len(otherImages) > 1 { + // other image associated, don't remove + continue + } + + // don't destroy files in zip archives + if f.Base().ZipFileID == nil { + const deleteFile = false + logger.Info("Destroying image file entry: ", f.Base().Path) + if err := file.Destroy(ctx, s.File, f, nil, deleteFile); err != nil { + return err + } + } + } + + return nil +} diff --git a/pkg/image/import.go b/pkg/image/import.go index ec200af04..77b6d7477 100644 --- a/pkg/image/import.go +++ b/pkg/image/import.go @@ -110,7 +110,7 @@ func (i *Importer) populateFiles(ctx context.Context) error { for _, ref := range i.Input.Files { path := ref - f, err := i.FileFinder.FindByPath(ctx, path) + f, err := i.FileFinder.FindByPath(ctx, path, true) if err != nil { return fmt.Errorf("error finding file: %w", err) } @@ -159,7 +159,7 @@ func (i *Importer) populateStudio(ctx context.Context) error { } func (i *Importer) createStudio(ctx context.Context, name string) (int, error) { - newStudio := models.NewStudio() + newStudio := models.NewCreateStudioInput() newStudio.Name = name err := i.StudioWriter.Create(ctx, &newStudio) diff --git a/pkg/image/import_test.go b/pkg/image/import_test.go index 286e51fe3..98b3972b9 100644 --- a/pkg/image/import_test.go +++ b/pkg/image/import_test.go @@ -77,9 +77,9 @@ func TestImporterPreImportWithMissingStudio(t *testing.T) { } db.Studio.On("FindByName", testCtx, missingStudioName, false).Return(nil, nil).Times(3) - db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.Studio")).Run(func(args mock.Arguments) { - s := args.Get(1).(*models.Studio) - s.ID = existingStudioID + db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.CreateStudioInput")).Run(func(args mock.Arguments) { + s := args.Get(1).(*models.CreateStudioInput) + s.Studio.ID = existingStudioID }).Return(nil) err := i.PreImport(testCtx) @@ -109,7 +109,7 @@ func TestImporterPreImportWithMissingStudioCreateErr(t *testing.T) { } db.Studio.On("FindByName", testCtx, missingStudioName, false).Return(nil, nil).Once() - db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.Studio")).Return(errors.New("Create error")) + db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.CreateStudioInput")).Return(errors.New("Create error")) err := i.PreImport(testCtx) assert.NotNil(t, err) diff --git a/pkg/image/thumbnail.go b/pkg/image/thumbnail.go index c65cfc77e..d0fba0f60 100644 --- a/pkg/image/thumbnail.go +++ b/pkg/image/thumbnail.go @@ -22,12 +22,8 @@ const ffmpegImageQuality = 5 var vipsPath string var once sync.Once -var ( - ErrUnsupportedImageFormat = errors.New("unsupported image format") - - // ErrNotSupportedForThumbnail is returned if the image format is not supported for thumbnail generation - ErrNotSupportedForThumbnail = errors.New("unsupported image format for thumbnail") -) +// ErrNotSupportedForThumbnail is returned if the image format is not supported for thumbnail generation +var ErrNotSupportedForThumbnail = errors.New("unsupported image format for thumbnail") type ThumbnailEncoder struct { FFMpeg *ffmpeg.FFMpeg @@ -83,8 +79,9 @@ func (e *ThumbnailEncoder) GetThumbnail(f models.File, maxSize int) ([]byte, err data := buf.Bytes() + format := "" if imageFile, ok := f.(*models.ImageFile); ok { - format := imageFile.Format + format = imageFile.Format animated := imageFile.Format == formatGif // #2266 - if image is webp, then determine if it is animated @@ -96,6 +93,19 @@ func (e *ThumbnailEncoder) GetThumbnail(f models.File, maxSize int) ([]byte, err if animated { return nil, fmt.Errorf("%w: %s", ErrNotSupportedForThumbnail, format) } + + // AVIF cannot be read from stdin, must use file path + // AVIF in zip files is not supported + // Note: No Windows check needed here since we use file path, not stdin + if format == "avif" { + if f.Base().ZipFileID != nil { + return nil, fmt.Errorf("%w: AVIF in zip file", ErrNotSupportedForThumbnail) + } + if e.vips != nil { + return e.vips.ImageThumbnailPath(f.Base().Path, maxSize) + } + return e.ffmpegImageThumbnailPath(f.Base().Path, maxSize) + } } // Videofiles can only be thumbnailed with ffmpeg @@ -104,11 +114,15 @@ func (e *ThumbnailEncoder) GetThumbnail(f models.File, maxSize int) ([]byte, err } // vips has issues loading files from stdin on Windows - if e.vips != nil && runtime.GOOS != "windows" { - return e.vips.ImageThumbnail(buf, maxSize) - } else { - return e.ffmpegImageThumbnail(buf, maxSize) + if e.vips != nil { + if runtime.GOOS == "windows" && f.Base().ZipFileID == nil { + return e.vips.ImageThumbnailPath(f.Base().Path, maxSize) + } + if runtime.GOOS != "windows" { + return e.vips.ImageThumbnail(buf, maxSize) + } } + return e.ffmpegImageThumbnail(buf, maxSize) } // GetPreview returns the preview clip of the provided image clip resized to @@ -130,16 +144,32 @@ func (e *ThumbnailEncoder) GetPreview(inPath string, outPath string, maxSize int } func (e *ThumbnailEncoder) ffmpegImageThumbnail(image *bytes.Buffer, maxSize int) ([]byte, error) { - args := transcoder.ImageThumbnail("-", transcoder.ImageThumbnailOptions{ + options := transcoder.ImageThumbnailOptions{ OutputFormat: ffmpeg.ImageFormatJpeg, OutputPath: "-", MaxDimensions: maxSize, Quality: ffmpegImageQuality, - }) + } + + args := transcoder.ImageThumbnail("-", options) return e.FFMpeg.GenerateOutput(context.TODO(), args, image) } +// ffmpegImageThumbnailPath generates a thumbnail from a file path (used for AVIF which can't be piped) +func (e *ThumbnailEncoder) ffmpegImageThumbnailPath(inputPath string, maxSize int) ([]byte, error) { + options := transcoder.ImageThumbnailOptions{ + OutputFormat: ffmpeg.ImageFormatJpeg, + OutputPath: "-", + MaxDimensions: maxSize, + Quality: ffmpegImageQuality, + } + + args := transcoder.ImageThumbnail(inputPath, options) + + return e.FFMpeg.GenerateOutput(context.TODO(), args, nil) +} + func (e *ThumbnailEncoder) getClipPreview(inPath string, outPath string, maxSize int, clipDuration float64, frameRate float64) error { var thumbFilter ffmpeg.VideoFilter thumbFilter = thumbFilter.ScaleMaxSize(maxSize) diff --git a/pkg/image/vips.go b/pkg/image/vips.go index 39809dc18..0a0350aa8 100644 --- a/pkg/image/vips.go +++ b/pkg/image/vips.go @@ -24,6 +24,38 @@ func (e *vipsEncoder) ImageThumbnail(image *bytes.Buffer, maxSize int) ([]byte, return []byte(data), err } +// ImageThumbnailPath generates a thumbnail from a file path instead of stdin. +// This is required for formats like AVIF that need random file access (seeking) +// which stdin cannot provide. +func (e *vipsEncoder) ImageThumbnailPath(path string, maxSize int) ([]byte, error) { + // vips thumbnail syntax: thumbnail input output width [options] + // Using .jpg[Q=70,strip] as output writes to stdout + args := []string{ + "thumbnail", + path, + ".jpg[Q=70,strip]", + fmt.Sprint(maxSize), + "--size", "down", + } + + cmd := exec.Command(string(*e), args...) + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Start(); err != nil { + return nil, err + } + + if err := cmd.Wait(); err != nil { + logger.Errorf("image encoder error when running command <%s>: %s", strings.Join(cmd.Args, " "), stderr.String()) + return nil, err + } + + return stdout.Bytes(), nil +} + func (e *vipsEncoder) run(args []string, stdin *bytes.Buffer) (string, error) { cmd := exec.Command(string(*e), args...) diff --git a/pkg/job/manager.go b/pkg/job/manager.go index 983d88cc0..3e47d842b 100644 --- a/pkg/job/manager.go +++ b/pkg/job/manager.go @@ -7,7 +7,6 @@ import ( "time" "github.com/stashapp/stash/pkg/logger" - "github.com/stashapp/stash/pkg/utils" ) const maxGraveyardSize = 10 @@ -179,7 +178,8 @@ func (m *Manager) dispatch(ctx context.Context, j *Job) (done chan struct{}) { j.StartTime = &t j.Status = StatusRunning - ctx, cancelFunc := context.WithCancel(utils.ValueOnlyContext{Context: ctx}) + // create a cancellable context for the job that is not canceled by the outer context + ctx, cancelFunc := context.WithCancel(context.WithoutCancel(ctx)) j.cancelFunc = cancelFunc done = make(chan struct{}) diff --git a/pkg/match/scraped.go b/pkg/match/scraped.go index b66f39a35..d3039f4c6 100644 --- a/pkg/match/scraped.go +++ b/pkg/match/scraped.go @@ -45,7 +45,7 @@ func (r SceneRelationships) MatchRelationships(ctx context.Context, s *models.Sc } for _, t := range s.Tags { - err := ScrapedTag(ctx, r.TagFinder, t) + err := ScrapedTag(ctx, r.TagFinder, t, endpoint) if err != nil { return err } @@ -190,11 +190,29 @@ func ScrapedGroup(ctx context.Context, qb GroupNamesFinder, storedID *string, na // ScrapedTag matches the provided tag with the tags // in the database and sets the ID field if one is found. -func ScrapedTag(ctx context.Context, qb models.TagQueryer, s *models.ScrapedTag) error { +func ScrapedTag(ctx context.Context, qb models.TagQueryer, s *models.ScrapedTag, stashBoxEndpoint string) error { if s.StoredID != nil { return nil } + // Check if a tag with the StashID already exists + if stashBoxEndpoint != "" && s.RemoteSiteID != nil { + if finder, ok := qb.(models.TagFinder); ok { + tags, err := finder.FindByStashID(ctx, models.StashID{ + StashID: *s.RemoteSiteID, + Endpoint: stashBoxEndpoint, + }) + if err != nil { + return err + } + if len(tags) > 0 { + id := strconv.Itoa(tags[0].ID) + s.StoredID = &id + return nil + } + } + } + t, err := tag.ByName(ctx, qb, s.Name) if err != nil { diff --git a/pkg/models/custom_fields.go b/pkg/models/custom_fields.go index 977c2fe89..5c3acd18b 100644 --- a/pkg/models/custom_fields.go +++ b/pkg/models/custom_fields.go @@ -9,6 +9,8 @@ type CustomFieldsInput struct { Full map[string]interface{} `json:"full"` // If populated, only the keys in this map will be updated Partial map[string]interface{} `json:"partial"` + // Remove any keys in this list + Remove []string `json:"remove"` } type CustomFieldsReader interface { diff --git a/pkg/models/date.go b/pkg/models/date.go index 151e32c1d..dbd5c4ec6 100644 --- a/pkg/models/date.go +++ b/pkg/models/date.go @@ -1,31 +1,63 @@ package models import ( + "fmt" "time" "github.com/stashapp/stash/pkg/utils" ) +type DatePrecision int + +const ( + // default precision is day + DatePrecisionDay DatePrecision = iota + DatePrecisionMonth + DatePrecisionYear +) + // Date wraps a time.Time with a format of "YYYY-MM-DD" type Date struct { time.Time + Precision DatePrecision } -const dateFormat = "2006-01-02" +var dateFormatPrecision = []string{ + "2006-01-02", + "2006-01", + "2006", +} func (d Date) String() string { - return d.Format(dateFormat) + return d.Format(dateFormatPrecision[d.Precision]) } func (d Date) After(o Date) bool { return d.Time.After(o.Time) } -// ParseDate uses utils.ParseDateStringAsTime to parse a string into a date. +// ParseDate tries to parse the input string into a date using utils.ParseDateStringAsTime. +// If that fails, it attempts to parse the string with decreasing precision (month, then year). +// It returns a Date struct with the appropriate precision set, or an error if all parsing attempts fail. func ParseDate(s string) (Date, error) { + var errs []error + + // default parse to day precision ret, err := utils.ParseDateStringAsTime(s) - if err != nil { - return Date{}, err + if err == nil { + return Date{Time: ret, Precision: DatePrecisionDay}, nil } - return Date{Time: ret}, nil + + errs = append(errs, err) + + // try month and year precision + for i, format := range dateFormatPrecision[1:] { + ret, err := time.Parse(format, s) + if err == nil { + return Date{Time: ret, Precision: DatePrecision(i + 1)}, nil + } + errs = append(errs, err) + } + + return Date{}, fmt.Errorf("failed to parse date %q: %v", s, errs) } diff --git a/pkg/models/date_test.go b/pkg/models/date_test.go new file mode 100644 index 000000000..b6cca9ee1 --- /dev/null +++ b/pkg/models/date_test.go @@ -0,0 +1,50 @@ +package models + +import ( + "testing" + "time" +) + +func TestParseDateStringAsTime(t *testing.T) { + tests := []struct { + name string + input string + output Date + expectError bool + }{ + // Full date formats (existing support) + {"RFC3339", "2014-01-02T15:04:05Z", Date{Time: time.Date(2014, 1, 2, 15, 4, 5, 0, time.UTC), Precision: DatePrecisionDay}, false}, + {"Date only", "2014-01-02", Date{Time: time.Date(2014, 1, 2, 0, 0, 0, 0, time.UTC), Precision: DatePrecisionDay}, false}, + {"Date with time", "2014-01-02 15:04:05", Date{Time: time.Date(2014, 1, 2, 15, 4, 5, 0, time.UTC), Precision: DatePrecisionDay}, false}, + + // Partial date formats (new support) + {"Year-Month", "2006-08", Date{Time: time.Date(2006, 8, 1, 0, 0, 0, 0, time.UTC), Precision: DatePrecisionMonth}, false}, + {"Year only", "2014", Date{Time: time.Date(2014, 1, 1, 0, 0, 0, 0, time.UTC), Precision: DatePrecisionYear}, false}, + + // Invalid formats + {"Invalid format", "not-a-date", Date{}, true}, + {"Empty string", "", Date{}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ParseDate(tt.input) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error for input %q, but got none", tt.input) + } + return + } + + if err != nil { + t.Errorf("Unexpected error for input %q: %v", tt.input, err) + return + } + + if !result.Time.Equal(tt.output.Time) || result.Precision != tt.output.Precision { + t.Errorf("For input %q, expected output %+v, got %+v", tt.input, tt.output, result) + } + }) + } +} diff --git a/pkg/models/gallery.go b/pkg/models/gallery.go index 73fa287d2..dfc776afe 100644 --- a/pkg/models/gallery.go +++ b/pkg/models/gallery.go @@ -59,6 +59,10 @@ type GalleryFilterType struct { StudiosFilter *StudioFilterType `json:"studios_filter"` // Filter by related tags that meet this criteria TagsFilter *TagFilterType `json:"tags_filter"` + // Filter by related files that meet this criteria + FilesFilter *FileFilterType `json:"files_filter"` + // Filter by related folders that meet this criteria + FoldersFilter *FolderFilterType `json:"folders_filter"` // Filter by created at CreatedAt *TimestampCriterionInput `json:"created_at"` // Filter by updated at @@ -91,6 +95,7 @@ type GalleryDestroyInput struct { // If true, then the zip file will be deleted if the gallery is zip-file-based. // If gallery is folder-based, then any files not associated with other // galleries will be deleted, along with the folder, if it is not empty. - DeleteFile *bool `json:"delete_file"` - DeleteGenerated *bool `json:"delete_generated"` + DeleteFile *bool `json:"delete_file"` + DeleteGenerated *bool `json:"delete_generated"` + DestroyFileEntry *bool `json:"destroy_file_entry"` } diff --git a/pkg/models/group.go b/pkg/models/group.go index 6afda3f48..6943b1055 100644 --- a/pkg/models/group.go +++ b/pkg/models/group.go @@ -23,6 +23,8 @@ type GroupFilterType struct { TagCount *IntCriterionInput `json:"tag_count"` // Filter by date Date *DateCriterionInput `json:"date"` + // Filter by O counter + OCounter *IntCriterionInput `json:"o_counter"` // Filter by containing groups ContainingGroups *HierarchicalMultiCriterionInput `json:"containing_groups"` // Filter by sub groups diff --git a/pkg/models/image.go b/pkg/models/image.go index 9d2c6f016..84be79360 100644 --- a/pkg/models/image.go +++ b/pkg/models/image.go @@ -11,6 +11,8 @@ type ImageFilterType struct { Photographer *StringCriterionInput `json:"photographer"` // Filter by file checksum Checksum *StringCriterionInput `json:"checksum"` + // Filter by phash distance + PhashDistance *PhashDistanceCriterionInput `json:"phash_distance"` // Filter by path Path *StringCriterionInput `json:"path"` // Filter by file count @@ -57,6 +59,8 @@ type ImageFilterType struct { StudiosFilter *StudioFilterType `json:"studios_filter"` // Filter by related tags that meet this criteria TagsFilter *TagFilterType `json:"tags_filter"` + // Filter by related files that meet this criteria + FilesFilter *FileFilterType `json:"files_filter"` // Filter by created at CreatedAt *TimestampCriterionInput `json:"created_at"` // Filter by updated at @@ -86,15 +90,17 @@ type ImageUpdateInput struct { } type ImageDestroyInput struct { - ID string `json:"id"` - DeleteFile *bool `json:"delete_file"` - DeleteGenerated *bool `json:"delete_generated"` + ID string `json:"id"` + DeleteFile *bool `json:"delete_file"` + DeleteGenerated *bool `json:"delete_generated"` + DestroyFileEntry *bool `json:"destroy_file_entry"` } type ImagesDestroyInput struct { - Ids []string `json:"ids"` - DeleteFile *bool `json:"delete_file"` - DeleteGenerated *bool `json:"delete_generated"` + Ids []string `json:"ids"` + DeleteFile *bool `json:"delete_file"` + DeleteGenerated *bool `json:"delete_generated"` + DestroyFileEntry *bool `json:"destroy_file_entry"` } type ImageQueryOptions struct { diff --git a/pkg/models/jsonschema/studio.go b/pkg/models/jsonschema/studio.go index 80ed97d92..a3706df66 100644 --- a/pkg/models/jsonschema/studio.go +++ b/pkg/models/jsonschema/studio.go @@ -12,7 +12,7 @@ import ( type Studio struct { Name string `json:"name,omitempty"` - URL string `json:"url,omitempty"` + URLs []string `json:"urls,omitempty"` ParentStudio string `json:"parent_studio,omitempty"` Image string `json:"image,omitempty"` CreatedAt json.JSONTime `json:"created_at,omitempty"` @@ -24,6 +24,9 @@ type Studio struct { StashIDs []models.StashID `json:"stash_ids,omitempty"` Tags []string `json:"tags,omitempty"` IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"` + + // deprecated - for import only + URL string `json:"url,omitempty"` } func (s Studio) Filename() string { diff --git a/pkg/models/jsonschema/tag.go b/pkg/models/jsonschema/tag.go index ed2bc1c9c..faab1bfb2 100644 --- a/pkg/models/jsonschema/tag.go +++ b/pkg/models/jsonschema/tag.go @@ -6,20 +6,22 @@ import ( jsoniter "github.com/json-iterator/go" "github.com/stashapp/stash/pkg/fsutil" + "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/json" ) type Tag struct { - Name string `json:"name,omitempty"` - SortName string `json:"sort_name,omitempty"` - Description string `json:"description,omitempty"` - Favorite bool `json:"favorite,omitempty"` - Aliases []string `json:"aliases,omitempty"` - Image string `json:"image,omitempty"` - Parents []string `json:"parents,omitempty"` - IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"` - CreatedAt json.JSONTime `json:"created_at,omitempty"` - UpdatedAt json.JSONTime `json:"updated_at,omitempty"` + Name string `json:"name,omitempty"` + SortName string `json:"sort_name,omitempty"` + Description string `json:"description,omitempty"` + Favorite bool `json:"favorite,omitempty"` + Aliases []string `json:"aliases,omitempty"` + Image string `json:"image,omitempty"` + Parents []string `json:"parents,omitempty"` + IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"` + StashIDs []models.StashID `json:"stash_ids,omitempty"` + CreatedAt json.JSONTime `json:"created_at,omitempty"` + UpdatedAt json.JSONTime `json:"updated_at,omitempty"` } func (s Tag) Filename() string { diff --git a/pkg/models/mocks/FileReaderWriter.go b/pkg/models/mocks/FileReaderWriter.go index 12a1b3075..97a0136e6 100644 --- a/pkg/models/mocks/FileReaderWriter.go +++ b/pkg/models/mocks/FileReaderWriter.go @@ -130,13 +130,13 @@ func (_m *FileReaderWriter) Find(ctx context.Context, id ...models.FileID) ([]mo return r0, r1 } -// FindAllByPath provides a mock function with given fields: ctx, path -func (_m *FileReaderWriter) FindAllByPath(ctx context.Context, path string) ([]models.File, error) { - ret := _m.Called(ctx, path) +// FindAllByPath provides a mock function with given fields: ctx, path, caseSensitive +func (_m *FileReaderWriter) FindAllByPath(ctx context.Context, path string, caseSensitive bool) ([]models.File, error) { + ret := _m.Called(ctx, path, caseSensitive) var r0 []models.File - if rf, ok := ret.Get(0).(func(context.Context, string) []models.File); ok { - r0 = rf(ctx, path) + if rf, ok := ret.Get(0).(func(context.Context, string, bool) []models.File); ok { + r0 = rf(ctx, path, caseSensitive) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]models.File) @@ -144,8 +144,8 @@ func (_m *FileReaderWriter) FindAllByPath(ctx context.Context, path string) ([]m } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, path) + if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok { + r1 = rf(ctx, path, caseSensitive) } else { r1 = ret.Error(1) } @@ -222,13 +222,13 @@ func (_m *FileReaderWriter) FindByFingerprint(ctx context.Context, fp models.Fin return r0, r1 } -// FindByPath provides a mock function with given fields: ctx, path -func (_m *FileReaderWriter) FindByPath(ctx context.Context, path string) (models.File, error) { - ret := _m.Called(ctx, path) +// FindByPath provides a mock function with given fields: ctx, path, caseSensitive +func (_m *FileReaderWriter) FindByPath(ctx context.Context, path string, caseSensitive bool) (models.File, error) { + ret := _m.Called(ctx, path, caseSensitive) var r0 models.File - if rf, ok := ret.Get(0).(func(context.Context, string) models.File); ok { - r0 = rf(ctx, path) + if rf, ok := ret.Get(0).(func(context.Context, string, bool) models.File); ok { + r0 = rf(ctx, path, caseSensitive) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(models.File) @@ -236,8 +236,8 @@ func (_m *FileReaderWriter) FindByPath(ctx context.Context, path string) (models } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, path) + if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok { + r1 = rf(ctx, path, caseSensitive) } else { r1 = ret.Error(1) } diff --git a/pkg/models/mocks/FolderReaderWriter.go b/pkg/models/mocks/FolderReaderWriter.go index 512925fd6..7bca013fe 100644 --- a/pkg/models/mocks/FolderReaderWriter.go +++ b/pkg/models/mocks/FolderReaderWriter.go @@ -132,13 +132,13 @@ func (_m *FolderReaderWriter) FindByParentFolderID(ctx context.Context, parentFo return r0, r1 } -// FindByPath provides a mock function with given fields: ctx, path -func (_m *FolderReaderWriter) FindByPath(ctx context.Context, path string) (*models.Folder, error) { - ret := _m.Called(ctx, path) +// FindByPath provides a mock function with given fields: ctx, path, caseSensitive +func (_m *FolderReaderWriter) FindByPath(ctx context.Context, path string, caseSensitive bool) (*models.Folder, error) { + ret := _m.Called(ctx, path, caseSensitive) var r0 *models.Folder - if rf, ok := ret.Get(0).(func(context.Context, string) *models.Folder); ok { - r0 = rf(ctx, path) + if rf, ok := ret.Get(0).(func(context.Context, string, bool) *models.Folder); ok { + r0 = rf(ctx, path, caseSensitive) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*models.Folder) @@ -146,8 +146,8 @@ func (_m *FolderReaderWriter) FindByPath(ctx context.Context, path string) (*mod } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, path) + if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok { + r1 = rf(ctx, path, caseSensitive) } else { r1 = ret.Error(1) } diff --git a/pkg/models/mocks/ImageReaderWriter.go b/pkg/models/mocks/ImageReaderWriter.go index 2bbf4ceeb..afc5efdb7 100644 --- a/pkg/models/mocks/ImageReaderWriter.go +++ b/pkg/models/mocks/ImageReaderWriter.go @@ -594,6 +594,27 @@ func (_m *ImageReaderWriter) OCountByPerformerID(ctx context.Context, performerI return r0, r1 } +// OCountByStudioID provides a mock function with given fields: ctx, studioID +func (_m *ImageReaderWriter) OCountByStudioID(ctx context.Context, studioID int) (int, error) { + ret := _m.Called(ctx, studioID) + + var r0 int + if rf, ok := ret.Get(0).(func(context.Context, int) int); ok { + r0 = rf(ctx, studioID) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, studioID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Query provides a mock function with given fields: ctx, options func (_m *ImageReaderWriter) Query(ctx context.Context, options models.ImageQueryOptions) (*models.ImageQueryResult, error) { ret := _m.Called(ctx, options) diff --git a/pkg/models/mocks/PerformerReaderWriter.go b/pkg/models/mocks/PerformerReaderWriter.go index dbf19a3cd..6487bc5a5 100644 --- a/pkg/models/mocks/PerformerReaderWriter.go +++ b/pkg/models/mocks/PerformerReaderWriter.go @@ -473,6 +473,20 @@ func (_m *PerformerReaderWriter) HasImage(ctx context.Context, performerID int) return r0, r1 } +// Merge provides a mock function with given fields: ctx, source, destination +func (_m *PerformerReaderWriter) Merge(ctx context.Context, source []int, destination int) error { + ret := _m.Called(ctx, source, destination) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []int, int) error); ok { + r0 = rf(ctx, source, destination) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Query provides a mock function with given fields: ctx, performerFilter, findFilter func (_m *PerformerReaderWriter) Query(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) { ret := _m.Called(ctx, performerFilter, findFilter) diff --git a/pkg/models/mocks/SceneReaderWriter.go b/pkg/models/mocks/SceneReaderWriter.go index 8e4e5ae5a..ef10c890d 100644 --- a/pkg/models/mocks/SceneReaderWriter.go +++ b/pkg/models/mocks/SceneReaderWriter.go @@ -1141,6 +1141,27 @@ func (_m *SceneReaderWriter) HasCover(ctx context.Context, sceneID int) (bool, e return r0, r1 } +// OCountByGroupID provides a mock function with given fields: ctx, groupID +func (_m *SceneReaderWriter) OCountByGroupID(ctx context.Context, groupID int) (int, error) { + ret := _m.Called(ctx, groupID) + + var r0 int + if rf, ok := ret.Get(0).(func(context.Context, int) int); ok { + r0 = rf(ctx, groupID) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, groupID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // OCountByPerformerID provides a mock function with given fields: ctx, performerID func (_m *SceneReaderWriter) OCountByPerformerID(ctx context.Context, performerID int) (int, error) { ret := _m.Called(ctx, performerID) @@ -1162,6 +1183,27 @@ func (_m *SceneReaderWriter) OCountByPerformerID(ctx context.Context, performerI return r0, r1 } +// OCountByStudioID provides a mock function with given fields: ctx, studioID +func (_m *SceneReaderWriter) OCountByStudioID(ctx context.Context, studioID int) (int, error) { + ret := _m.Called(ctx, studioID) + + var r0 int + if rf, ok := ret.Get(0).(func(context.Context, int) int); ok { + r0 = rf(ctx, studioID) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, studioID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // PlayDuration provides a mock function with given fields: ctx func (_m *SceneReaderWriter) PlayDuration(ctx context.Context) (float64, error) { ret := _m.Called(ctx) diff --git a/pkg/models/mocks/StudioReaderWriter.go b/pkg/models/mocks/StudioReaderWriter.go index d4932ca71..f57a73aa1 100644 --- a/pkg/models/mocks/StudioReaderWriter.go +++ b/pkg/models/mocks/StudioReaderWriter.go @@ -80,11 +80,11 @@ func (_m *StudioReaderWriter) CountByTagID(ctx context.Context, tagID int) (int, } // Create provides a mock function with given fields: ctx, newStudio -func (_m *StudioReaderWriter) Create(ctx context.Context, newStudio *models.Studio) error { +func (_m *StudioReaderWriter) Create(ctx context.Context, newStudio *models.CreateStudioInput) error { ret := _m.Called(ctx, newStudio) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.Studio) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *models.CreateStudioInput) error); ok { r0 = rf(ctx, newStudio) } else { r0 = ret.Error(0) @@ -291,6 +291,52 @@ func (_m *StudioReaderWriter) GetAliases(ctx context.Context, relatedID int) ([] return r0, r1 } +// GetCustomFields provides a mock function with given fields: ctx, id +func (_m *StudioReaderWriter) GetCustomFields(ctx context.Context, id int) (map[string]interface{}, error) { + ret := _m.Called(ctx, id) + + var r0 map[string]interface{} + if rf, ok := ret.Get(0).(func(context.Context, int) map[string]interface{}); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]interface{}) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetCustomFieldsBulk provides a mock function with given fields: ctx, ids +func (_m *StudioReaderWriter) GetCustomFieldsBulk(ctx context.Context, ids []int) ([]models.CustomFieldMap, error) { + ret := _m.Called(ctx, ids) + + var r0 []models.CustomFieldMap + if rf, ok := ret.Get(0).(func(context.Context, []int) []models.CustomFieldMap); ok { + r0 = rf(ctx, ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.CustomFieldMap) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, []int) error); ok { + r1 = rf(ctx, ids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetImage provides a mock function with given fields: ctx, studioID func (_m *StudioReaderWriter) GetImage(ctx context.Context, studioID int) ([]byte, error) { ret := _m.Called(ctx, studioID) @@ -360,6 +406,29 @@ func (_m *StudioReaderWriter) GetTagIDs(ctx context.Context, relatedID int) ([]i return r0, r1 } +// GetURLs provides a mock function with given fields: ctx, relatedID +func (_m *StudioReaderWriter) GetURLs(ctx context.Context, relatedID int) ([]string, error) { + ret := _m.Called(ctx, relatedID) + + var r0 []string + if rf, ok := ret.Get(0).(func(context.Context, int) []string); ok { + r0 = rf(ctx, relatedID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, relatedID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // HasImage provides a mock function with given fields: ctx, studioID func (_m *StudioReaderWriter) HasImage(ctx context.Context, studioID int) (bool, error) { ret := _m.Called(ctx, studioID) @@ -456,11 +525,11 @@ func (_m *StudioReaderWriter) QueryForAutoTag(ctx context.Context, words []strin } // Update provides a mock function with given fields: ctx, updatedStudio -func (_m *StudioReaderWriter) Update(ctx context.Context, updatedStudio *models.Studio) error { +func (_m *StudioReaderWriter) Update(ctx context.Context, updatedStudio *models.UpdateStudioInput) error { ret := _m.Called(ctx, updatedStudio) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.Studio) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *models.UpdateStudioInput) error); ok { r0 = rf(ctx, updatedStudio) } else { r0 = ret.Error(0) diff --git a/pkg/models/mocks/TagReaderWriter.go b/pkg/models/mocks/TagReaderWriter.go index a285b97bf..ac6b10584 100644 --- a/pkg/models/mocks/TagReaderWriter.go +++ b/pkg/models/mocks/TagReaderWriter.go @@ -427,6 +427,29 @@ func (_m *TagReaderWriter) FindBySceneMarkerID(ctx context.Context, sceneMarkerI return r0, r1 } +// FindByStashID provides a mock function with given fields: ctx, stashID +func (_m *TagReaderWriter) FindByStashID(ctx context.Context, stashID models.StashID) ([]*models.Tag, error) { + ret := _m.Called(ctx, stashID) + + var r0 []*models.Tag + if rf, ok := ret.Get(0).(func(context.Context, models.StashID) []*models.Tag); ok { + r0 = rf(ctx, stashID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.Tag) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, models.StashID) error); ok { + r1 = rf(ctx, stashID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // FindByStudioID provides a mock function with given fields: ctx, studioID func (_m *TagReaderWriter) FindByStudioID(ctx context.Context, studioID int) ([]*models.Tag, error) { ret := _m.Called(ctx, studioID) @@ -565,6 +588,29 @@ func (_m *TagReaderWriter) GetParentIDs(ctx context.Context, relatedID int) ([]i return r0, r1 } +// GetStashIDs provides a mock function with given fields: ctx, relatedID +func (_m *TagReaderWriter) GetStashIDs(ctx context.Context, relatedID int) ([]models.StashID, error) { + ret := _m.Called(ctx, relatedID) + + var r0 []models.StashID + if rf, ok := ret.Get(0).(func(context.Context, int) []models.StashID); ok { + r0 = rf(ctx, relatedID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.StashID) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, relatedID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // HasImage provides a mock function with given fields: ctx, tagID func (_m *TagReaderWriter) HasImage(ctx context.Context, tagID int) (bool, error) { ret := _m.Called(ctx, tagID) diff --git a/pkg/models/model_scene_marker.go b/pkg/models/model_scene_marker.go index 778603315..8d723b391 100644 --- a/pkg/models/model_scene_marker.go +++ b/pkg/models/model_scene_marker.go @@ -30,6 +30,7 @@ type SceneMarkerPartial struct { Seconds OptionalFloat64 EndSeconds OptionalFloat64 PrimaryTagID OptionalInt + TagIDs *UpdateIDs SceneID OptionalInt CreatedAt OptionalTime UpdatedAt OptionalTime diff --git a/pkg/models/model_scraped_item.go b/pkg/models/model_scraped_item.go index 008a05c3d..bd6db10c8 100644 --- a/pkg/models/model_scraped_item.go +++ b/pkg/models/model_scraped_item.go @@ -14,21 +14,25 @@ type ScrapedStudio struct { // Set if studio matched StoredID *string `json:"stored_id"` Name string `json:"name"` - URL *string `json:"url"` + URL *string `json:"url"` // deprecated + URLs []string `json:"urls"` Parent *ScrapedStudio `json:"parent"` Image *string `json:"image"` Images []string `json:"images"` + Details *string `json:"details"` + Aliases *string `json:"aliases"` + Tags []*ScrapedTag `json:"tags"` RemoteSiteID *string `json:"remote_site_id"` } func (ScrapedStudio) IsScrapedContent() {} -func (s *ScrapedStudio) ToStudio(endpoint string, excluded map[string]bool) *Studio { +func (s *ScrapedStudio) ToStudio(endpoint string, excluded map[string]bool) *CreateStudioInput { // Populate a new studio from the input - ret := NewStudio() - ret.Name = s.Name + ret := NewCreateStudioInput() + ret.Name = strings.TrimSpace(s.Name) - if s.RemoteSiteID != nil && endpoint != "" { + if s.RemoteSiteID != nil && endpoint != "" && *s.RemoteSiteID != "" { ret.StashIDs = NewRelatedStashIDs([]StashID{ { Endpoint: endpoint, @@ -38,8 +42,28 @@ func (s *ScrapedStudio) ToStudio(endpoint string, excluded map[string]bool) *Stu }) } - if s.URL != nil && !excluded["url"] { - ret.URL = *s.URL + // if URLs are provided, only use those + if len(s.URLs) > 0 { + if !excluded["urls"] { + ret.URLs = NewRelatedStrings(s.URLs) + } + } else { + urls := []string{} + if s.URL != nil && !excluded["url"] { + urls = append(urls, *s.URL) + } + + if len(urls) > 0 { + ret.URLs = NewRelatedStrings(urls) + } + } + + if s.Details != nil && !excluded["details"] { + ret.Details = *s.Details + } + + if s.Aliases != nil && *s.Aliases != "" && !excluded["aliases"] { + ret.Aliases = NewRelatedStrings(stringslice.FromString(*s.Aliases, ",")) } if s.Parent != nil && s.Parent.StoredID != nil && !excluded["parent"] && !excluded["parent_studio"] { @@ -71,11 +95,40 @@ func (s *ScrapedStudio) ToPartial(id string, endpoint string, excluded map[strin currentTime := time.Now() if s.Name != "" && !excluded["name"] { - ret.Name = NewOptionalString(s.Name) + ret.Name = NewOptionalString(strings.TrimSpace(s.Name)) } - if s.URL != nil && !excluded["url"] { - ret.URL = NewOptionalString(*s.URL) + if len(s.URLs) > 0 { + if !excluded["urls"] { + + ret.URLs = &UpdateStrings{ + Values: stringslice.TrimSpace(s.URLs), + Mode: RelationshipUpdateModeSet, + } + } + } else { + urls := []string{} + if s.URL != nil && !excluded["url"] { + urls = append(urls, strings.TrimSpace(*s.URL)) + } + + if len(urls) > 0 { + ret.URLs = &UpdateStrings{ + Values: stringslice.TrimSpace(urls), + Mode: RelationshipUpdateModeSet, + } + } + } + + if s.Details != nil && !excluded["details"] { + ret.Details = NewOptionalString(strings.TrimSpace(*s.Details)) + } + + if s.Aliases != nil && *s.Aliases != "" && !excluded["aliases"] { + ret.Aliases = &UpdateStrings{ + Values: stringslice.TrimSpace(stringslice.FromString(*s.Aliases, ",")), + Mode: RelationshipUpdateModeSet, + } } if s.Parent != nil && !excluded["parent"] { @@ -88,7 +141,7 @@ func (s *ScrapedStudio) ToPartial(id string, endpoint string, excluded map[strin } } - if s.RemoteSiteID != nil && endpoint != "" { + if s.RemoteSiteID != nil && endpoint != "" && *s.RemoteSiteID != "" { ret.StashIDs = &UpdateStashIDs{ StashIDs: existingStashIDs, Mode: RelationshipUpdateModeSet, @@ -145,10 +198,14 @@ func (ScrapedPerformer) IsScrapedContent() {} func (p *ScrapedPerformer) ToPerformer(endpoint string, excluded map[string]bool) *Performer { ret := NewPerformer() currentTime := time.Now() - ret.Name = *p.Name + ret.Name = strings.TrimSpace(*p.Name) if p.Aliases != nil && !excluded["aliases"] { - ret.Aliases = NewRelatedStrings(stringslice.FromString(*p.Aliases, ",")) + aliases := stringslice.FromString(*p.Aliases, ",") + for i, alias := range aliases { + aliases[i] = strings.TrimSpace(alias) + } + ret.Aliases = NewRelatedStrings(aliases) } if p.Birthdate != nil && !excluded["birthdate"] { date, err := ParseDate(*p.Birthdate) @@ -249,7 +306,7 @@ func (p *ScrapedPerformer) ToPerformer(endpoint string, excluded map[string]bool } } - if p.RemoteSiteID != nil && endpoint != "" { + if p.RemoteSiteID != nil && endpoint != "" && *p.RemoteSiteID != "" { ret.StashIDs = NewRelatedStashIDs([]StashID{ { Endpoint: endpoint, @@ -378,7 +435,7 @@ func (p *ScrapedPerformer) ToPartial(endpoint string, excluded map[string]bool, } } - if p.RemoteSiteID != nil && endpoint != "" { + if p.RemoteSiteID != nil && endpoint != "" && *p.RemoteSiteID != "" { ret.StashIDs = &UpdateStashIDs{ StashIDs: existingStashIDs, Mode: RelationshipUpdateModeSet, @@ -395,12 +452,31 @@ func (p *ScrapedPerformer) ToPartial(endpoint string, excluded map[string]bool, type ScrapedTag struct { // Set if tag matched - StoredID *string `json:"stored_id"` - Name string `json:"name"` + StoredID *string `json:"stored_id"` + Name string `json:"name"` + RemoteSiteID *string `json:"remote_site_id"` } func (ScrapedTag) IsScrapedContent() {} +func (t *ScrapedTag) ToTag(endpoint string, excluded map[string]bool) *Tag { + currentTime := time.Now() + ret := NewTag() + ret.Name = t.Name + + if t.RemoteSiteID != nil && endpoint != "" && *t.RemoteSiteID != "" { + ret.StashIDs = NewRelatedStashIDs([]StashID{ + { + Endpoint: endpoint, + StashID: *t.RemoteSiteID, + UpdatedAt: currentTime, + }, + }) + } + + return &ret +} + func ScrapedTagSortFunction(a, b *ScrapedTag) int { return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)) } @@ -462,6 +538,7 @@ type ScrapedGroup struct { Date *string `json:"date"` Rating *string `json:"rating"` Director *string `json:"director"` + URL *string `json:"url"` // included for backward compatibility URLs []string `json:"urls"` Synopsis *string `json:"synopsis"` Studio *ScrapedStudio `json:"studio"` diff --git a/pkg/models/model_scraped_item_test.go b/pkg/models/model_scraped_item_test.go index 1e8edccb4..545543652 100644 --- a/pkg/models/model_scraped_item_test.go +++ b/pkg/models/model_scraped_item_test.go @@ -11,6 +11,7 @@ import ( func Test_scrapedToStudioInput(t *testing.T) { const name = "name" url := "url" + url2 := "url2" emptyEndpoint := "" endpoint := "endpoint" remoteSiteID := "remoteSiteID" @@ -25,13 +26,33 @@ func Test_scrapedToStudioInput(t *testing.T) { "set all", &ScrapedStudio{ Name: name, + URLs: []string{url, url2}, URL: &url, RemoteSiteID: &remoteSiteID, }, endpoint, &Studio{ Name: name, - URL: url, + URLs: NewRelatedStrings([]string{url, url2}), + StashIDs: NewRelatedStashIDs([]StashID{ + { + Endpoint: endpoint, + StashID: remoteSiteID, + }, + }), + }, + }, + { + "set url instead of urls", + &ScrapedStudio{ + Name: name, + URL: &url, + RemoteSiteID: &remoteSiteID, + }, + endpoint, + &Studio{ + Name: name, + URLs: NewRelatedStrings([]string{url}), StashIDs: NewRelatedStashIDs([]StashID{ { Endpoint: endpoint, @@ -92,7 +113,7 @@ func Test_scrapedToStudioInput(t *testing.T) { got.StashIDs.List()[stid].UpdatedAt = time.Time{} } } - assert.Equal(t, tt.want, got) + assert.Equal(t, tt.want, got.Studio) }) } } @@ -321,9 +342,12 @@ func TestScrapedStudio_ToPartial(t *testing.T) { fullStudio, stdArgs, StudioPartial{ - ID: id, - Name: NewOptionalString(name), - URL: NewOptionalString(url), + ID: id, + Name: NewOptionalString(name), + URLs: &UpdateStrings{ + Values: []string{url}, + Mode: RelationshipUpdateModeSet, + }, ParentID: NewOptionalInt(parentStoredID), StashIDs: &UpdateStashIDs{ StashIDs: append(existingStashIDs, StashID{ diff --git a/pkg/models/model_studio.go b/pkg/models/model_studio.go index 0f4a09bc2..ee6fae2d2 100644 --- a/pkg/models/model_studio.go +++ b/pkg/models/model_studio.go @@ -8,7 +8,6 @@ import ( type Studio struct { ID int `json:"id"` Name string `json:"name"` - URL string `json:"url"` ParentID *int `json:"parent_id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -19,10 +18,23 @@ type Studio struct { IgnoreAutoTag bool `json:"ignore_auto_tag"` Aliases RelatedStrings `json:"aliases"` + URLs RelatedStrings `json:"urls"` TagIDs RelatedIDs `json:"tag_ids"` StashIDs RelatedStashIDs `json:"stash_ids"` } +type CreateStudioInput struct { + *Studio + + CustomFields map[string]interface{} `json:"custom_fields"` +} + +type UpdateStudioInput struct { + *Studio + + CustomFields CustomFieldsInput `json:"custom_fields"` +} + func NewStudio() Studio { currentTime := time.Now() return Studio{ @@ -31,11 +43,17 @@ func NewStudio() Studio { } } +func NewCreateStudioInput() CreateStudioInput { + s := NewStudio() + return CreateStudioInput{ + Studio: &s, + } +} + // StudioPartial represents part of a Studio object. It is used to update the database entry. type StudioPartial struct { ID int Name OptionalString - URL OptionalString ParentID OptionalInt // Rating expressed in 1-100 scale Rating OptionalInt @@ -46,8 +64,11 @@ type StudioPartial struct { IgnoreAutoTag OptionalBool Aliases *UpdateStrings + URLs *UpdateStrings TagIDs *UpdateIDs StashIDs *UpdateStashIDs + + CustomFields CustomFieldsInput } func NewStudioPartial() StudioPartial { @@ -63,6 +84,12 @@ func (s *Studio) LoadAliases(ctx context.Context, l AliasLoader) error { }) } +func (s *Studio) LoadURLs(ctx context.Context, l URLLoader) error { + return s.URLs.load(func() ([]string, error) { + return l.GetURLs(ctx, s.ID) + }) +} + func (s *Studio) LoadTagIDs(ctx context.Context, l TagIDLoader) error { return s.TagIDs.load(func() ([]int, error) { return l.GetTagIDs(ctx, s.ID) diff --git a/pkg/models/model_tag.go b/pkg/models/model_tag.go index 0d845750f..4cd038f7e 100644 --- a/pkg/models/model_tag.go +++ b/pkg/models/model_tag.go @@ -15,9 +15,10 @@ type Tag struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - Aliases RelatedStrings `json:"aliases"` - ParentIDs RelatedIDs `json:"parent_ids"` - ChildIDs RelatedIDs `json:"tag_ids"` + Aliases RelatedStrings `json:"aliases"` + ParentIDs RelatedIDs `json:"parent_ids"` + ChildIDs RelatedIDs `json:"tag_ids"` + StashIDs RelatedStashIDs `json:"stash_ids"` } func NewTag() Tag { @@ -46,6 +47,12 @@ func (s *Tag) LoadChildIDs(ctx context.Context, l TagRelationLoader) error { }) } +func (s *Tag) LoadStashIDs(ctx context.Context, l StashIDLoader) error { + return s.StashIDs.load(func() ([]StashID, error) { + return l.GetStashIDs(ctx, s.ID) + }) +} + type TagPartial struct { Name OptionalString SortName OptionalString @@ -58,6 +65,7 @@ type TagPartial struct { Aliases *UpdateStrings ParentIDs *UpdateIDs ChildIDs *UpdateIDs + StashIDs *UpdateStashIDs } func NewTagPartial() TagPartial { diff --git a/pkg/models/performer.go b/pkg/models/performer.go index 239d8347f..63a08b30c 100644 --- a/pkg/models/performer.go +++ b/pkg/models/performer.go @@ -166,6 +166,8 @@ type PerformerFilterType struct { StashID *StringCriterionInput `json:"stash_id"` // Filter by StashID Endpoint StashIDEndpoint *StashIDCriterionInput `json:"stash_id_endpoint"` + // Filter by StashIDs Endpoint + StashIDsEndpoint *StashIDsCriterionInput `json:"stash_ids_endpoint"` // Filter by rating expressed as 1-100 Rating100 *IntCriterionInput `json:"rating100"` // Filter by url diff --git a/pkg/models/repository_file.go b/pkg/models/repository_file.go index 0819b25a5..c851ce08c 100644 --- a/pkg/models/repository_file.go +++ b/pkg/models/repository_file.go @@ -13,9 +13,9 @@ type FileGetter interface { // FileFinder provides methods to find files. type FileFinder interface { FileGetter - FindAllByPath(ctx context.Context, path string) ([]File, error) + FindAllByPath(ctx context.Context, path string, caseSensitive bool) ([]File, error) FindAllInPaths(ctx context.Context, p []string, limit, offset int) ([]File, error) - FindByPath(ctx context.Context, path string) (File, error) + FindByPath(ctx context.Context, path string, caseSensitive bool) (File, error) FindByFingerprint(ctx context.Context, fp Fingerprint) ([]File, error) FindByZipFileID(ctx context.Context, zipFileID FileID) ([]File, error) FindByFileInfo(ctx context.Context, info fs.FileInfo, size int64) ([]File, error) diff --git a/pkg/models/repository_folder.go b/pkg/models/repository_folder.go index 671e8780d..3d0fdb822 100644 --- a/pkg/models/repository_folder.go +++ b/pkg/models/repository_folder.go @@ -12,7 +12,7 @@ type FolderGetter interface { type FolderFinder interface { FolderGetter FindAllInPaths(ctx context.Context, p []string, limit, offset int) ([]*Folder, error) - FindByPath(ctx context.Context, path string) (*Folder, error) + FindByPath(ctx context.Context, path string, caseSensitive bool) (*Folder, error) FindByZipFileID(ctx context.Context, zipFileID FileID) ([]*Folder, error) FindByParentFolderID(ctx context.Context, parentFolderID FolderID) ([]*Folder, error) } diff --git a/pkg/models/repository_image.go b/pkg/models/repository_image.go index 1455d7762..672ecd063 100644 --- a/pkg/models/repository_image.go +++ b/pkg/models/repository_image.go @@ -38,6 +38,7 @@ type ImageCounter interface { CountByGalleryID(ctx context.Context, galleryID int) (int, error) OCount(ctx context.Context) (int, error) OCountByPerformerID(ctx context.Context, performerID int) (int, error) + OCountByStudioID(ctx context.Context, studioID int) (int, error) } // ImageCreator provides methods to create images. diff --git a/pkg/models/repository_performer.go b/pkg/models/repository_performer.go index ad0b61da0..175208c9d 100644 --- a/pkg/models/repository_performer.go +++ b/pkg/models/repository_performer.go @@ -92,6 +92,8 @@ type PerformerWriter interface { PerformerCreator PerformerUpdater PerformerDestroyer + + Merge(ctx context.Context, source []int, destination int) error } // PerformerReaderWriter provides all performer methods. diff --git a/pkg/models/repository_scene.go b/pkg/models/repository_scene.go index f0fff4ac7..8c2833470 100644 --- a/pkg/models/repository_scene.go +++ b/pkg/models/repository_scene.go @@ -44,6 +44,8 @@ type SceneCounter interface { CountMissingChecksum(ctx context.Context) (int, error) CountMissingOSHash(ctx context.Context) (int, error) OCountByPerformerID(ctx context.Context, performerID int) (int, error) + OCountByGroupID(ctx context.Context, groupID int) (int, error) + OCountByStudioID(ctx context.Context, studioID int) (int, error) } // SceneCreator provides methods to create scenes. diff --git a/pkg/models/repository_studio.go b/pkg/models/repository_studio.go index a2b9202f3..54fb6ed47 100644 --- a/pkg/models/repository_studio.go +++ b/pkg/models/repository_studio.go @@ -42,12 +42,12 @@ type StudioCounter interface { // StudioCreator provides methods to create studios. type StudioCreator interface { - Create(ctx context.Context, newStudio *Studio) error + Create(ctx context.Context, newStudio *CreateStudioInput) error } // StudioUpdater provides methods to update studios. type StudioUpdater interface { - Update(ctx context.Context, updatedStudio *Studio) error + Update(ctx context.Context, updatedStudio *UpdateStudioInput) error UpdatePartial(ctx context.Context, updatedStudio StudioPartial) (*Studio, error) UpdateImage(ctx context.Context, studioID int, image []byte) error } @@ -77,6 +77,9 @@ type StudioReader interface { AliasLoader StashIDLoader TagIDLoader + URLLoader + + CustomFieldsReader All(ctx context.Context) ([]*Studio, error) GetImage(ctx context.Context, studioID int) ([]byte, error) diff --git a/pkg/models/repository_tag.go b/pkg/models/repository_tag.go index 2b073cae0..a7f828f0b 100644 --- a/pkg/models/repository_tag.go +++ b/pkg/models/repository_tag.go @@ -25,6 +25,7 @@ type TagFinder interface { FindByStudioID(ctx context.Context, studioID int) ([]*Tag, error) FindByName(ctx context.Context, name string, nocase bool) (*Tag, error) FindByNames(ctx context.Context, names []string, nocase bool) ([]*Tag, error) + FindByStashID(ctx context.Context, stashID StashID) ([]*Tag, error) } // TagQueryer provides methods to query tags. @@ -87,6 +88,7 @@ type TagReader interface { AliasLoader TagRelationLoader + StashIDLoader All(ctx context.Context) ([]*Tag, error) GetImage(ctx context.Context, tagID int) ([]byte, error) diff --git a/pkg/models/scene.go b/pkg/models/scene.go index 9f28d40ba..434659cbe 100644 --- a/pkg/models/scene.go +++ b/pkg/models/scene.go @@ -79,6 +79,10 @@ type SceneFilterType struct { StashID *StringCriterionInput `json:"stash_id"` // Filter by StashID Endpoint StashIDEndpoint *StashIDCriterionInput `json:"stash_id_endpoint"` + // Filter by StashIDs Endpoint + StashIDsEndpoint *StashIDsCriterionInput `json:"stash_ids_endpoint"` + // Filter by StashID count + StashIDCount *IntCriterionInput `json:"stash_id_count"` // Filter by url URL *StringCriterionInput `json:"url"` // Filter by interactive @@ -111,6 +115,8 @@ type SceneFilterType struct { MoviesFilter *GroupFilterType `json:"movies_filter"` // Filter by related markers that meet this criteria MarkersFilter *SceneMarkerFilterType `json:"markers_filter"` + // Filter by related files that meet this criteria + FilesFilter *FileFilterType `json:"files_filter"` // Filter by created at CreatedAt *TimestampCriterionInput `json:"created_at"` // Filter by updated at @@ -200,15 +206,17 @@ type SceneUpdateInput struct { } type SceneDestroyInput struct { - ID string `json:"id"` - DeleteFile *bool `json:"delete_file"` - DeleteGenerated *bool `json:"delete_generated"` + ID string `json:"id"` + DeleteFile *bool `json:"delete_file"` + DeleteGenerated *bool `json:"delete_generated"` + DestroyFileEntry *bool `json:"destroy_file_entry"` } type ScenesDestroyInput struct { - Ids []string `json:"ids"` - DeleteFile *bool `json:"delete_file"` - DeleteGenerated *bool `json:"delete_generated"` + Ids []string `json:"ids"` + DeleteFile *bool `json:"delete_file"` + DeleteGenerated *bool `json:"delete_generated"` + DestroyFileEntry *bool `json:"destroy_file_entry"` } func NewSceneQueryResult(getter SceneGetter) *SceneQueryResult { diff --git a/pkg/models/stash_ids.go b/pkg/models/stash_ids.go index 7751c2ef0..d73bfd880 100644 --- a/pkg/models/stash_ids.go +++ b/pkg/models/stash_ids.go @@ -79,10 +79,23 @@ func (s StashIDInputs) ToStashIDs() StashIDs { return nil } - ret := make(StashIDs, len(s)) - for i, v := range s { - ret[i] = v.ToStashID() + // #2800 - deduplicate StashIDs based on endpoint and stash_id + ret := make(StashIDs, 0, len(s)) + seen := make(map[string]map[string]bool) + + for _, v := range s { + stashID := v.ToStashID() + + if seen[stashID.Endpoint] == nil { + seen[stashID.Endpoint] = make(map[string]bool) + } + + if !seen[stashID.Endpoint][stashID.StashID] { + seen[stashID.Endpoint][stashID.StashID] = true + ret = append(ret, stashID) + } } + return ret } @@ -116,8 +129,16 @@ func (u *UpdateStashIDs) Set(v StashID) { type StashIDCriterionInput struct { // If present, this value is treated as a predicate. - // That is, it will filter based on stash_ids with the matching endpoint + // That is, it will filter based on stash_id with the matching endpoint Endpoint *string `json:"endpoint"` StashID *string `json:"stash_id"` Modifier CriterionModifier `json:"modifier"` } + +type StashIDsCriterionInput struct { + // If present, this value is treated as a predicate. + // That is, it will filter based on stash_ids with the matching endpoint + Endpoint *string `json:"endpoint"` + StashIDs []*string `json:"stash_ids"` + Modifier CriterionModifier `json:"modifier"` +} diff --git a/pkg/models/studio.go b/pkg/models/studio.go index 03ea8a84d..be5d54445 100644 --- a/pkg/models/studio.go +++ b/pkg/models/studio.go @@ -10,6 +10,8 @@ type StudioFilterType struct { StashID *StringCriterionInput `json:"stash_id"` // Filter by StashID Endpoint StashIDEndpoint *StashIDCriterionInput `json:"stash_id_endpoint"` + // Filter by StashIDs Endpoint + StashIDsEndpoint *StashIDsCriterionInput `json:"stash_ids_endpoint"` // Filter to only include studios missing this property IsMissing *string `json:"is_missing"` // Filter by rating expressed as 1-100 @@ -44,12 +46,16 @@ type StudioFilterType struct { CreatedAt *TimestampCriterionInput `json:"created_at"` // Filter by updated at UpdatedAt *TimestampCriterionInput `json:"updated_at"` + + // Filter by custom fields + CustomFields []CustomFieldCriterionInput `json:"custom_fields"` } type StudioCreateInput struct { - Name string `json:"name"` - URL *string `json:"url"` - ParentID *string `json:"parent_id"` + Name string `json:"name"` + URL *string `json:"url"` // deprecated + Urls []string `json:"urls"` + ParentID *string `json:"parent_id"` // This should be a URL or a base64 encoded data URL Image *string `json:"image"` StashIds []StashIDInput `json:"stash_ids"` @@ -59,13 +65,16 @@ type StudioCreateInput struct { Aliases []string `json:"aliases"` TagIds []string `json:"tag_ids"` IgnoreAutoTag *bool `json:"ignore_auto_tag"` + + CustomFields map[string]interface{} `json:"custom_fields"` } type StudioUpdateInput struct { - ID string `json:"id"` - Name *string `json:"name"` - URL *string `json:"url"` - ParentID *string `json:"parent_id"` + ID string `json:"id"` + Name *string `json:"name"` + URL *string `json:"url"` // deprecated + Urls []string `json:"urls"` + ParentID *string `json:"parent_id"` // This should be a URL or a base64 encoded data URL Image *string `json:"image"` StashIds []StashIDInput `json:"stash_ids"` @@ -75,4 +84,6 @@ type StudioUpdateInput struct { Aliases []string `json:"aliases"` TagIds []string `json:"tag_ids"` IgnoreAutoTag *bool `json:"ignore_auto_tag"` + + CustomFields CustomFieldsInput `json:"custom_fields"` } diff --git a/pkg/models/tag.go b/pkg/models/tag.go index d53863b37..5ff2df6ad 100644 --- a/pkg/models/tag.go +++ b/pkg/models/tag.go @@ -40,6 +40,10 @@ type TagFilterType struct { ChildCount *IntCriterionInput `json:"child_count"` // Filter by autotag ignore value IgnoreAutoTag *bool `json:"ignore_auto_tag"` + // Filter by StashID Endpoint + StashIDEndpoint *StashIDCriterionInput `json:"stash_id_endpoint"` + // Filter by StashIDs Endpoint + StashIDsEndpoint *StashIDsCriterionInput `json:"stash_ids_endpoint"` // Filter by related scenes that meet this criteria ScenesFilter *SceneFilterType `json:"scenes_filter"` // Filter by related images that meet this criteria diff --git a/pkg/performer/import.go b/pkg/performer/import.go index 3aaacdb8b..622af2b1a 100644 --- a/pkg/performer/import.go +++ b/pkg/performer/import.go @@ -233,7 +233,7 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform } if len(urls) > 0 { - newPerformer.URLs = models.NewRelatedStrings([]string{performerJSON.URL}) + newPerformer.URLs = models.NewRelatedStrings(urls) } } diff --git a/pkg/performer/validate.go b/pkg/performer/validate.go index 68f7a8ef5..3baaa182b 100644 --- a/pkg/performer/validate.go +++ b/pkg/performer/validate.go @@ -225,6 +225,11 @@ func ValidateUpdateAliases(existing models.Performer, name models.OptionalString newName = name.Value } + // If aliases is nil, we're only changing the name - check existing aliases against new name + if aliases == nil { + return ValidateAliases(newName, existing.Aliases) + } + newAliases := aliases.Apply(existing.Aliases.List()) return ValidateAliases(newName, models.NewRelatedStrings(newAliases)) diff --git a/pkg/performer/validate_test.go b/pkg/performer/validate_test.go index 33f4b1cec..afd9c01c5 100644 --- a/pkg/performer/validate_test.go +++ b/pkg/performer/validate_test.go @@ -213,12 +213,12 @@ func TestValidateUpdateAliases(t *testing.T) { want error }{ {"both unset", osUnset, nil, nil}, - {"invalid name set", os2, nil, &DuplicateAliasError{name2}}, + {"name conflicts with alias", os2, nil, &DuplicateAliasError{name2}}, {"valid name set", os3, nil, nil}, {"valid aliases empty", os1, []string{}, nil}, - {"invalid aliases set", osUnset, []string{name1U}, &DuplicateAliasError{name1U}}, + {"alias matches name", osUnset, []string{name1U}, &DuplicateAliasError{name1U}}, {"valid aliases set", osUnset, []string{name3, name2}, nil}, - {"invalid both set", os4, []string{name4}, &DuplicateAliasError{name4}}, + {"alias matches new name", os4, []string{name4}, &DuplicateAliasError{name4}}, {"valid both set", os2, []string{name1}, nil}, } diff --git a/pkg/plugin/examples/react-component/README.md b/pkg/plugin/examples/react-component/README.md index 5a42a3749..383b47235 100644 --- a/pkg/plugin/examples/react-component/README.md +++ b/pkg/plugin/examples/react-component/README.md @@ -1,7 +1,7 @@ This is a reference React component plugin. It replaces the `details` part of scene cards with a list of performers and tags. To build: -- run `yarn install --frozen-lockfile` -- run `yarn run build` +- run `pnpm install --frozen-lockfile` +- run `npm run build` This will copy the plugin files into the `dist` directory. These files can be copied to a `plugins` directory. diff --git a/pkg/plugin/examples/react-component/package.json b/pkg/plugin/examples/react-component/package.json index b37205d9c..1c07e5774 100644 --- a/pkg/plugin/examples/react-component/package.json +++ b/pkg/plugin/examples/react-component/package.json @@ -5,11 +5,11 @@ "author": "WithoutPants", "license": "AGPL-3.0", "scripts": { - "compile:ts": "yarn tsc", - "compile:sass": "yarn sass src/testReact.scss dist/testReact.css", + "compile:ts": "npm run tsc", + "compile:sass": "npm run sass src/testReact.scss dist/testReact.css", "copy:yml": "cpx \"src/testReact.yml\" \"dist\"", - "compile": "yarn run compile:ts && yarn run compile:sass", - "build": "yarn run compile && yarn run copy:yml" + "compile": "npm run compile:ts && npm run compile:sass", + "build": "npm run compile && npm run copy:yml" }, "devDependencies": { "@types/react": "^18.2.31", diff --git a/pkg/plugin/examples/react-component/pnpm-lock.yaml b/pkg/plugin/examples/react-component/pnpm-lock.yaml new file mode 100644 index 000000000..d19c17347 --- /dev/null +++ b/pkg/plugin/examples/react-component/pnpm-lock.yaml @@ -0,0 +1,1578 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/react': + specifier: ^18.2.31 + version: 18.3.26 + '@types/react-dom': + specifier: ^18.2.14 + version: 18.3.7(@types/react@18.3.26) + cpx: + specifier: ^1.5.0 + version: 1.5.0 + sass: + specifier: ^1.69.4 + version: 1.93.2 + typescript: + specifier: ^5.2.2 + version: 5.9.3 + +packages: + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.26': + resolution: {integrity: sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==} + + anymatch@1.3.2: + resolution: {integrity: sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==} + + arr-diff@2.0.0: + resolution: {integrity: sha512-dtXTVMkh6VkEEA7OhXnN1Ecb8aAGFdZ1LFxtOCoqj4qkyOJMt7+qs6Ahdy6p/NQCPYsRSXXivhSB/J5E9jmYKA==} + engines: {node: '>=0.10.0'} + + arr-diff@4.0.0: + resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} + engines: {node: '>=0.10.0'} + + arr-flatten@1.1.0: + resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==} + engines: {node: '>=0.10.0'} + + arr-union@3.1.0: + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} + + array-unique@0.2.1: + resolution: {integrity: sha512-G2n5bG5fSUCpnsXz4+8FUkYsGPkNfLn9YvS66U5qbTIXI2Ynnlo4Bi42bWv+omKUCqz+ejzfClwne0alJWJPhg==} + engines: {node: '>=0.10.0'} + + array-unique@0.3.2: + resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==} + engines: {node: '>=0.10.0'} + + assign-symbols@1.0.0: + resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} + engines: {node: '>=0.10.0'} + + async-each@1.0.6: + resolution: {integrity: sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==} + + atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + + babel-runtime@6.26.0: + resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base@0.11.2: + resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} + engines: {node: '>=0.10.0'} + + binary-extensions@1.13.1: + resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==} + engines: {node: '>=0.10.0'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + braces@1.8.5: + resolution: {integrity: sha512-xU7bpz2ytJl1bH9cgIurjpg/n8Gohy9GTw81heDYLJQ4RU60dlyJsa+atVF2pI0yMMvKxI9HkKwjePCj5XI1hw==} + engines: {node: '>=0.10.0'} + + braces@2.3.2: + resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} + engines: {node: '>=0.10.0'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + cache-base@1.0.1: + resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} + engines: {node: '>=0.10.0'} + + chokidar@1.7.0: + resolution: {integrity: sha512-mk8fAWcRUOxY7btlLtitj3A45jOwSAxH4tOFOoEGbVsl6cL6pPMWUy7dwZ/canfj3QEdP6FHSnf/l1c6/WkzVg==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + class-utils@0.3.6: + resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} + engines: {node: '>=0.10.0'} + + collection-visit@1.0.0: + resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} + engines: {node: '>=0.10.0'} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + copy-descriptor@0.1.1: + resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} + engines: {node: '>=0.10.0'} + + core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cpx@1.5.0: + resolution: {integrity: sha512-jHTjZhsbg9xWgsP2vuNW2jnnzBX+p4T+vNI9Lbjzs1n4KhOfa22bQppiFYLsWQKd8TzmL5aSP/Me3yfsCwXbDA==} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + define-property@0.2.5: + resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==} + engines: {node: '>=0.10.0'} + + define-property@1.0.0: + resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} + engines: {node: '>=0.10.0'} + + define-property@2.0.2: + resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==} + engines: {node: '>=0.10.0'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + expand-brackets@0.1.5: + resolution: {integrity: sha512-hxx03P2dJxss6ceIeri9cmYOT4SRs3Zk3afZwWpOsRqLqprhTR8u++SlC+sFGsQr7WGFPdMF7Gjc1njDLDK6UA==} + engines: {node: '>=0.10.0'} + + expand-brackets@2.1.4: + resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} + engines: {node: '>=0.10.0'} + + expand-range@1.8.2: + resolution: {integrity: sha512-AFASGfIlnIbkKPQwX1yHaDjFvh/1gyKJODme52V6IORh69uEYgZp0o9C+qsIGNVEiuuhQU0CSSl++Rlegg1qvA==} + engines: {node: '>=0.10.0'} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend-shallow@3.0.2: + resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} + engines: {node: '>=0.10.0'} + + extglob@0.3.2: + resolution: {integrity: sha512-1FOj1LOwn42TMrruOHGt18HemVnbwAmAak7krWk+wa93KXxGbK+2jpezm+ytJYDaBX0/SPLZFHKM7m+tKobWGg==} + engines: {node: '>=0.10.0'} + + extglob@2.0.4: + resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} + engines: {node: '>=0.10.0'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + filename-regex@2.0.1: + resolution: {integrity: sha512-BTCqyBaWBTsauvnHiE8i562+EdJj+oUpkqWp2R1iCoR8f6oo8STRu3of7WJJ0TqWtxN50a5YFpzYK4Jj9esYfQ==} + engines: {node: '>=0.10.0'} + + fill-range@2.2.4: + resolution: {integrity: sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==} + engines: {node: '>=0.10.0'} + + fill-range@4.0.0: + resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} + engines: {node: '>=0.10.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-index@0.1.1: + resolution: {integrity: sha512-uJ5vWrfBKMcE6y2Z8834dwEZj9mNGxYa3t3I53OwFeuZ8D9oc2E5zcsrkuhX6h4iYrjhiv0T3szQmxlAV9uxDg==} + + for-in@1.0.2: + resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} + engines: {node: '>=0.10.0'} + + for-own@0.1.5: + resolution: {integrity: sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==} + engines: {node: '>=0.10.0'} + + fragment-cache@0.2.1: + resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} + engines: {node: '>=0.10.0'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@1.2.13: + resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==} + engines: {node: '>= 4.0'} + os: [darwin] + deprecated: Upgrade to fsevents v2 to mitigate potential security issues + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-value@2.0.6: + resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} + engines: {node: '>=0.10.0'} + + glob-base@0.3.0: + resolution: {integrity: sha512-ab1S1g1EbO7YzauaJLkgLp7DZVAqj9M/dvKlTt8DkXA2tiOIcSMrlVI2J1RZyB5iJVccEscjGn+kpOG9788MHA==} + engines: {node: '>=0.10.0'} + + glob-parent@2.0.0: + resolution: {integrity: sha512-JDYOvfxio/t42HKdxkAYaCiBN7oYiuxykOxKxdaUW5Qn0zaYN3gRQWolrwdnf0shM9/EP0ebuuTmyoXNr1cC5w==} + + glob2base@0.0.12: + resolution: {integrity: sha512-ZyqlgowMbfj2NPjxaZZ/EtsXlOch28FRXgMd64vqZWk1bT9+wvSRLYD1om9M7QfQru51zJPAT17qXm4/zd+9QA==} + engines: {node: '>= 0.10'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-value@0.3.1: + resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} + engines: {node: '>=0.10.0'} + + has-value@1.0.0: + resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==} + engines: {node: '>=0.10.0'} + + has-values@0.1.4: + resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==} + engines: {node: '>=0.10.0'} + + has-values@1.0.0: + resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==} + engines: {node: '>=0.10.0'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-accessor-descriptor@1.0.1: + resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==} + engines: {node: '>= 0.10'} + + is-binary-path@1.0.1: + resolution: {integrity: sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==} + engines: {node: '>=0.10.0'} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-descriptor@1.0.1: + resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} + engines: {node: '>= 0.4'} + + is-descriptor@0.1.7: + resolution: {integrity: sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==} + engines: {node: '>= 0.4'} + + is-descriptor@1.0.3: + resolution: {integrity: sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==} + engines: {node: '>= 0.4'} + + is-dotfile@1.0.3: + resolution: {integrity: sha512-9YclgOGtN/f8zx0Pr4FQYMdibBiTaH3sn52vjYip4ZSf6C4/6RfTEZ+MR4GvKhCxdPh21Bg42/WL55f6KSnKpg==} + engines: {node: '>=0.10.0'} + + is-equal-shallow@0.1.3: + resolution: {integrity: sha512-0EygVC5qPvIyb+gSz7zdD5/AAoS6Qrx1e//6N4yv4oNm30kqvdmG66oZFWVlQHUWe5OjP08FuTw2IdT0EOTcYA==} + engines: {node: '>=0.10.0'} + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extendable@1.0.1: + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} + + is-extglob@1.0.0: + resolution: {integrity: sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@2.0.1: + resolution: {integrity: sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@2.1.0: + resolution: {integrity: sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==} + engines: {node: '>=0.10.0'} + + is-number@3.0.0: + resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} + engines: {node: '>=0.10.0'} + + is-number@4.0.0: + resolution: {integrity: sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-posix-bracket@0.1.1: + resolution: {integrity: sha512-Yu68oeXJ7LeWNmZ3Zov/xg/oDBnBK2RNxwYY1ilNJX+tKKZqgPK+qOn/Gs9jEu66KDY9Netf5XLKNGzas/vPfQ==} + engines: {node: '>=0.10.0'} + + is-primitive@2.0.0: + resolution: {integrity: sha512-N3w1tFaRfk3UrPfqeRyD+GYDASU3W5VinKhlORy8EWVf/sIdDL9GAcew85XmktCfH+ngG7SRXEVDoO18WMdB/Q==} + engines: {node: '>=0.10.0'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isobject@2.1.0: + resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} + engines: {node: '>=0.10.0'} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} + + kind-of@4.0.0: + resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==} + engines: {node: '>=0.10.0'} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} + + map-visit@1.0.0: + resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} + engines: {node: '>=0.10.0'} + + math-random@1.0.4: + resolution: {integrity: sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==} + + micromatch@2.3.11: + resolution: {integrity: sha512-LnU2XFEk9xxSJ6rfgAry/ty5qwUTyHYOBU0g4R6tIw5ljwgGIBmiKhRWLw5NpMOnrgUNcDJ4WMp8rl3sYVHLNA==} + engines: {node: '>=0.10.0'} + + micromatch@3.1.10: + resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} + engines: {node: '>=0.10.0'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mixin-deep@1.3.2: + resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} + engines: {node: '>=0.10.0'} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + nan@2.23.0: + resolution: {integrity: sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==} + + nanomatch@1.2.13: + resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} + engines: {node: '>=0.10.0'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + normalize-path@2.1.1: + resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} + engines: {node: '>=0.10.0'} + + object-copy@0.1.0: + resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} + engines: {node: '>=0.10.0'} + + object-visit@1.0.1: + resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} + engines: {node: '>=0.10.0'} + + object.omit@2.0.1: + resolution: {integrity: sha512-UiAM5mhmIuKLsOvrL+B0U2d1hXHF3bFYWIuH1LMpuV2EJEHG1Ntz06PgLEHjm6VFd87NpH8rastvPoyv6UW2fA==} + engines: {node: '>=0.10.0'} + + object.pick@1.3.0: + resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} + engines: {node: '>=0.10.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parse-glob@3.0.4: + resolution: {integrity: sha512-FC5TeK0AwXzq3tUBFtH74naWkPQCEWs4K+xMxWZBlKDWu0bVHXGZa+KKqxKidd7xwhdZ19ZNuF2uO1M/r196HA==} + engines: {node: '>=0.10.0'} + + pascalcase@0.1.1: + resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} + engines: {node: '>=0.10.0'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + posix-character-classes@0.1.1: + resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} + engines: {node: '>=0.10.0'} + + preserve@0.2.0: + resolution: {integrity: sha512-s/46sYeylUfHNjI+sA/78FAHlmIuKqI9wNnzEOGehAlUUYeObv5C2mOinXBjyUyWmJ2SfcS2/ydApH4hTF4WXQ==} + engines: {node: '>=0.10.0'} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + randomatic@3.1.1: + resolution: {integrity: sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==} + engines: {node: '>= 0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readdirp@2.2.1: + resolution: {integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==} + engines: {node: '>=0.10'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + regenerator-runtime@0.11.1: + resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} + + regex-cache@0.4.4: + resolution: {integrity: sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==} + engines: {node: '>=0.10.0'} + + regex-not@1.0.2: + resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} + engines: {node: '>=0.10.0'} + + remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} + + repeat-element@1.1.4: + resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} + engines: {node: '>=0.10.0'} + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + resolve-url@0.2.1: + resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} + deprecated: https://github.com/lydell/resolve-url#deprecated + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + ret@0.1.15: + resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} + engines: {node: '>=0.12'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex@1.1.0: + resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} + + sass@1.93.2: + resolution: {integrity: sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==} + engines: {node: '>=14.0.0'} + hasBin: true + + set-value@2.0.1: + resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} + engines: {node: '>=0.10.0'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + snapdragon-node@2.1.1: + resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} + engines: {node: '>=0.10.0'} + + snapdragon-util@3.0.1: + resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==} + engines: {node: '>=0.10.0'} + + snapdragon@0.8.2: + resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} + engines: {node: '>=0.10.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-resolve@0.5.3: + resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated + + source-map-url@0.4.1: + resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} + deprecated: See https://github.com/lydell/source-map-url#deprecated + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + split-string@3.1.0: + resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} + engines: {node: '>=0.10.0'} + + static-extend@0.1.2: + resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} + engines: {node: '>=0.10.0'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + subarg@1.0.0: + resolution: {integrity: sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg==} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + to-object-path@0.3.0: + resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} + engines: {node: '>=0.10.0'} + + to-regex-range@2.1.1: + resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} + engines: {node: '>=0.10.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + to-regex@3.0.2: + resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} + engines: {node: '>=0.10.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + union-value@1.0.1: + resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} + engines: {node: '>=0.10.0'} + + unset-value@1.0.0: + resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} + engines: {node: '>=0.10.0'} + + urix@0.1.0: + resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} + deprecated: Please see https://github.com/lydell/urix#deprecated + + use@3.1.1: + resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} + engines: {node: '>=0.10.0'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + +snapshots: + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.26)': + dependencies: + '@types/react': 18.3.26 + + '@types/react@18.3.26': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.1.3 + + anymatch@1.3.2: + dependencies: + micromatch: 2.3.11 + normalize-path: 2.1.1 + + arr-diff@2.0.0: + dependencies: + arr-flatten: 1.1.0 + + arr-diff@4.0.0: {} + + arr-flatten@1.1.0: {} + + arr-union@3.1.0: {} + + array-unique@0.2.1: {} + + array-unique@0.3.2: {} + + assign-symbols@1.0.0: {} + + async-each@1.0.6: {} + + atob@2.1.2: {} + + babel-runtime@6.26.0: + dependencies: + core-js: 2.6.12 + regenerator-runtime: 0.11.1 + + balanced-match@1.0.2: {} + + base@0.11.2: + dependencies: + cache-base: 1.0.1 + class-utils: 0.3.6 + component-emitter: 1.3.1 + define-property: 1.0.0 + isobject: 3.0.1 + mixin-deep: 1.3.2 + pascalcase: 0.1.1 + + binary-extensions@1.13.1: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + optional: true + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@1.8.5: + dependencies: + expand-range: 1.8.2 + preserve: 0.2.0 + repeat-element: 1.1.4 + + braces@2.3.2: + dependencies: + arr-flatten: 1.1.0 + array-unique: 0.3.2 + extend-shallow: 2.0.1 + fill-range: 4.0.0 + isobject: 3.0.1 + repeat-element: 1.1.4 + snapdragon: 0.8.2 + snapdragon-node: 2.1.1 + split-string: 3.1.0 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + optional: true + + cache-base@1.0.1: + dependencies: + collection-visit: 1.0.0 + component-emitter: 1.3.1 + get-value: 2.0.6 + has-value: 1.0.0 + isobject: 3.0.1 + set-value: 2.0.1 + to-object-path: 0.3.0 + union-value: 1.0.1 + unset-value: 1.0.0 + + chokidar@1.7.0: + dependencies: + anymatch: 1.3.2 + async-each: 1.0.6 + glob-parent: 2.0.0 + inherits: 2.0.4 + is-binary-path: 1.0.1 + is-glob: 2.0.1 + path-is-absolute: 1.0.1 + readdirp: 2.2.1 + optionalDependencies: + fsevents: 1.2.13 + transitivePeerDependencies: + - supports-color + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + class-utils@0.3.6: + dependencies: + arr-union: 3.1.0 + define-property: 0.2.5 + isobject: 3.0.1 + static-extend: 0.1.2 + + collection-visit@1.0.0: + dependencies: + map-visit: 1.0.0 + object-visit: 1.0.1 + + component-emitter@1.3.1: {} + + concat-map@0.0.1: {} + + copy-descriptor@0.1.1: {} + + core-js@2.6.12: {} + + core-util-is@1.0.3: {} + + cpx@1.5.0: + dependencies: + babel-runtime: 6.26.0 + chokidar: 1.7.0 + duplexer: 0.1.2 + glob: 7.2.3 + glob2base: 0.0.12 + minimatch: 3.1.2 + mkdirp: 0.5.6 + resolve: 1.22.11 + safe-buffer: 5.2.1 + shell-quote: 1.8.3 + subarg: 1.0.0 + transitivePeerDependencies: + - supports-color + + csstype@3.1.3: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + decode-uri-component@0.2.2: {} + + define-property@0.2.5: + dependencies: + is-descriptor: 0.1.7 + + define-property@1.0.0: + dependencies: + is-descriptor: 1.0.3 + + define-property@2.0.2: + dependencies: + is-descriptor: 1.0.3 + isobject: 3.0.1 + + detect-libc@1.0.3: + optional: true + + duplexer@0.1.2: {} + + expand-brackets@0.1.5: + dependencies: + is-posix-bracket: 0.1.1 + + expand-brackets@2.1.4: + dependencies: + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + posix-character-classes: 0.1.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + expand-range@1.8.2: + dependencies: + fill-range: 2.2.4 + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend-shallow@3.0.2: + dependencies: + assign-symbols: 1.0.0 + is-extendable: 1.0.1 + + extglob@0.3.2: + dependencies: + is-extglob: 1.0.0 + + extglob@2.0.4: + dependencies: + array-unique: 0.3.2 + define-property: 1.0.0 + expand-brackets: 2.1.4 + extend-shallow: 2.0.1 + fragment-cache: 0.2.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + file-uri-to-path@1.0.0: + optional: true + + filename-regex@2.0.1: {} + + fill-range@2.2.4: + dependencies: + is-number: 2.1.0 + isobject: 2.1.0 + randomatic: 3.1.1 + repeat-element: 1.1.4 + repeat-string: 1.6.1 + + fill-range@4.0.0: + dependencies: + extend-shallow: 2.0.1 + is-number: 3.0.0 + repeat-string: 1.6.1 + to-regex-range: 2.1.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + optional: true + + find-index@0.1.1: {} + + for-in@1.0.2: {} + + for-own@0.1.5: + dependencies: + for-in: 1.0.2 + + fragment-cache@0.2.1: + dependencies: + map-cache: 0.2.2 + + fs.realpath@1.0.0: {} + + fsevents@1.2.13: + dependencies: + bindings: 1.5.0 + nan: 2.23.0 + optional: true + + function-bind@1.1.2: {} + + get-value@2.0.6: {} + + glob-base@0.3.0: + dependencies: + glob-parent: 2.0.0 + is-glob: 2.0.1 + + glob-parent@2.0.0: + dependencies: + is-glob: 2.0.1 + + glob2base@0.0.12: + dependencies: + find-index: 0.1.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + graceful-fs@4.2.11: {} + + has-value@0.3.1: + dependencies: + get-value: 2.0.6 + has-values: 0.1.4 + isobject: 2.1.0 + + has-value@1.0.0: + dependencies: + get-value: 2.0.6 + has-values: 1.0.0 + isobject: 3.0.1 + + has-values@0.1.4: {} + + has-values@1.0.0: + dependencies: + is-number: 3.0.0 + kind-of: 4.0.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + immutable@5.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-accessor-descriptor@1.0.1: + dependencies: + hasown: 2.0.2 + + is-binary-path@1.0.1: + dependencies: + binary-extensions: 1.13.1 + + is-buffer@1.1.6: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-descriptor@1.0.1: + dependencies: + hasown: 2.0.2 + + is-descriptor@0.1.7: + dependencies: + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 + + is-descriptor@1.0.3: + dependencies: + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 + + is-dotfile@1.0.3: {} + + is-equal-shallow@0.1.3: + dependencies: + is-primitive: 2.0.0 + + is-extendable@0.1.1: {} + + is-extendable@1.0.1: + dependencies: + is-plain-object: 2.0.4 + + is-extglob@1.0.0: {} + + is-extglob@2.1.1: + optional: true + + is-glob@2.0.1: + dependencies: + is-extglob: 1.0.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + optional: true + + is-number@2.1.0: + dependencies: + kind-of: 3.2.2 + + is-number@3.0.0: + dependencies: + kind-of: 3.2.2 + + is-number@4.0.0: {} + + is-number@7.0.0: + optional: true + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-posix-bracket@0.1.1: {} + + is-primitive@2.0.0: {} + + is-windows@1.0.2: {} + + isarray@1.0.0: {} + + isobject@2.1.0: + dependencies: + isarray: 1.0.0 + + isobject@3.0.1: {} + + kind-of@3.2.2: + dependencies: + is-buffer: 1.1.6 + + kind-of@4.0.0: + dependencies: + is-buffer: 1.1.6 + + kind-of@6.0.3: {} + + map-cache@0.2.2: {} + + map-visit@1.0.0: + dependencies: + object-visit: 1.0.1 + + math-random@1.0.4: {} + + micromatch@2.3.11: + dependencies: + arr-diff: 2.0.0 + array-unique: 0.2.1 + braces: 1.8.5 + expand-brackets: 0.1.5 + extglob: 0.3.2 + filename-regex: 2.0.1 + is-extglob: 1.0.0 + is-glob: 2.0.1 + kind-of: 3.2.2 + normalize-path: 2.1.1 + object.omit: 2.0.1 + parse-glob: 3.0.4 + regex-cache: 0.4.4 + + micromatch@3.1.10: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + braces: 2.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + extglob: 2.0.4 + fragment-cache: 0.2.1 + kind-of: 6.0.3 + nanomatch: 1.2.13 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + optional: true + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimist@1.2.8: {} + + mixin-deep@1.3.2: + dependencies: + for-in: 1.0.2 + is-extendable: 1.0.1 + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + ms@2.0.0: {} + + nan@2.23.0: + optional: true + + nanomatch@1.2.13: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + fragment-cache: 0.2.1 + is-windows: 1.0.2 + kind-of: 6.0.3 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + node-addon-api@7.1.1: + optional: true + + normalize-path@2.1.1: + dependencies: + remove-trailing-separator: 1.1.0 + + object-copy@0.1.0: + dependencies: + copy-descriptor: 0.1.1 + define-property: 0.2.5 + kind-of: 3.2.2 + + object-visit@1.0.1: + dependencies: + isobject: 3.0.1 + + object.omit@2.0.1: + dependencies: + for-own: 0.1.5 + is-extendable: 0.1.1 + + object.pick@1.3.0: + dependencies: + isobject: 3.0.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parse-glob@3.0.4: + dependencies: + glob-base: 0.3.0 + is-dotfile: 1.0.3 + is-extglob: 1.0.0 + is-glob: 2.0.1 + + pascalcase@0.1.1: {} + + path-is-absolute@1.0.1: {} + + path-parse@1.0.7: {} + + picomatch@2.3.1: + optional: true + + posix-character-classes@0.1.1: {} + + preserve@0.2.0: {} + + process-nextick-args@2.0.1: {} + + randomatic@3.1.1: + dependencies: + is-number: 4.0.0 + kind-of: 6.0.3 + math-random: 1.0.4 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readdirp@2.2.1: + dependencies: + graceful-fs: 4.2.11 + micromatch: 3.1.10 + readable-stream: 2.3.8 + transitivePeerDependencies: + - supports-color + + readdirp@4.1.2: {} + + regenerator-runtime@0.11.1: {} + + regex-cache@0.4.4: + dependencies: + is-equal-shallow: 0.1.3 + + regex-not@1.0.2: + dependencies: + extend-shallow: 3.0.2 + safe-regex: 1.1.0 + + remove-trailing-separator@1.1.0: {} + + repeat-element@1.1.4: {} + + repeat-string@1.6.1: {} + + resolve-url@0.2.1: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + ret@0.1.15: {} + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-regex@1.1.0: + dependencies: + ret: 0.1.15 + + sass@1.93.2: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.4 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + set-value@2.0.1: + dependencies: + extend-shallow: 2.0.1 + is-extendable: 0.1.1 + is-plain-object: 2.0.4 + split-string: 3.1.0 + + shell-quote@1.8.3: {} + + snapdragon-node@2.1.1: + dependencies: + define-property: 1.0.0 + isobject: 3.0.1 + snapdragon-util: 3.0.1 + + snapdragon-util@3.0.1: + dependencies: + kind-of: 3.2.2 + + snapdragon@0.8.2: + dependencies: + base: 0.11.2 + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + map-cache: 0.2.2 + source-map: 0.5.7 + source-map-resolve: 0.5.3 + use: 3.1.1 + transitivePeerDependencies: + - supports-color + + source-map-js@1.2.1: {} + + source-map-resolve@0.5.3: + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.2 + resolve-url: 0.2.1 + source-map-url: 0.4.1 + urix: 0.1.0 + + source-map-url@0.4.1: {} + + source-map@0.5.7: {} + + split-string@3.1.0: + dependencies: + extend-shallow: 3.0.2 + + static-extend@0.1.2: + dependencies: + define-property: 0.2.5 + object-copy: 0.1.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + subarg@1.0.0: + dependencies: + minimist: 1.2.8 + + supports-preserve-symlinks-flag@1.0.0: {} + + to-object-path@0.3.0: + dependencies: + kind-of: 3.2.2 + + to-regex-range@2.1.1: + dependencies: + is-number: 3.0.0 + repeat-string: 1.6.1 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + optional: true + + to-regex@3.0.2: + dependencies: + define-property: 2.0.2 + extend-shallow: 3.0.2 + regex-not: 1.0.2 + safe-regex: 1.1.0 + + typescript@5.9.3: {} + + union-value@1.0.1: + dependencies: + arr-union: 3.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + set-value: 2.0.1 + + unset-value@1.0.0: + dependencies: + has-value: 0.3.1 + isobject: 3.0.1 + + urix@0.1.0: {} + + use@3.1.1: {} + + util-deprecate@1.0.2: {} + + wrappy@1.0.2: {} diff --git a/pkg/plugin/examples/react-component/yarn.lock b/pkg/plugin/examples/react-component/yarn.lock deleted file mode 100644 index 809986c5d..000000000 --- a/pkg/plugin/examples/react-component/yarn.lock +++ /dev/null @@ -1,1282 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@types/prop-types@*": - version "15.7.9" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.9.tgz#b6f785caa7ea1fe4414d9df42ee0ab67f23d8a6d" - integrity sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g== - -"@types/react-dom@^18.2.14": - version "18.2.14" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.14.tgz#c01ba40e5bb57fc1dc41569bb3ccdb19eab1c539" - integrity sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ== - dependencies: - "@types/react" "*" - -"@types/react@*", "@types/react@^18.2.31": - version "18.2.31" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.31.tgz#74ae2630e4aa9af599584157abd3b95d96fb9b40" - integrity sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/scheduler@*": - version "0.16.5" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.5.tgz#4751153abbf8d6199babb345a52e1eb4167d64af" - integrity sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw== - -anymatch@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" - integrity sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA== - dependencies: - micromatch "^2.1.5" - normalize-path "^2.0.0" - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -arr-diff@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" - integrity sha512-dtXTVMkh6VkEEA7OhXnN1Ecb8aAGFdZ1LFxtOCoqj4qkyOJMt7+qs6Ahdy6p/NQCPYsRSXXivhSB/J5E9jmYKA== - dependencies: - arr-flatten "^1.0.1" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA== - -arr-flatten@^1.0.1, arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q== - -array-unique@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" - integrity sha512-G2n5bG5fSUCpnsXz4+8FUkYsGPkNfLn9YvS66U5qbTIXI2Ynnlo4Bi42bWv+omKUCqz+ejzfClwne0alJWJPhg== - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ== - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw== - -async-each@^1.0.0: - version "1.0.6" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.6.tgz#52f1d9403818c179b7561e11a5d1b77eb2160e77" - integrity sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg== - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -babel-runtime@^6.9.2: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g== - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -binary-extensions@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" - integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^1.8.2: - version "1.8.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" - integrity sha512-xU7bpz2ytJl1bH9cgIurjpg/n8Gohy9GTw81heDYLJQ4RU60dlyJsa+atVF2pI0yMMvKxI9HkKwjePCj5XI1hw== - dependencies: - expand-range "^1.8.1" - preserve "^0.2.0" - repeat-element "^1.1.2" - -braces@^2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -"chokidar@>=3.0.0 <4.0.0": - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chokidar@^1.6.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" - integrity sha512-mk8fAWcRUOxY7btlLtitj3A45jOwSAxH4tOFOoEGbVsl6cL6pPMWUy7dwZ/canfj3QEdP6FHSnf/l1c6/WkzVg== - dependencies: - anymatch "^1.3.0" - async-each "^1.0.0" - glob-parent "^2.0.0" - inherits "^2.0.1" - is-binary-path "^1.0.0" - is-glob "^2.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.0.0" - optionalDependencies: - fsevents "^1.0.0" - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw== - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== - -core-js@^2.4.0: - version "2.6.12" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" - integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -cpx@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/cpx/-/cpx-1.5.0.tgz#185be018511d87270dedccc293171e37655ab88f" - integrity sha512-jHTjZhsbg9xWgsP2vuNW2jnnzBX+p4T+vNI9Lbjzs1n4KhOfa22bQppiFYLsWQKd8TzmL5aSP/Me3yfsCwXbDA== - dependencies: - babel-runtime "^6.9.2" - chokidar "^1.6.0" - duplexer "^0.1.1" - glob "^7.0.5" - glob2base "^0.0.12" - minimatch "^3.0.2" - mkdirp "^0.5.1" - resolve "^1.1.7" - safe-buffer "^5.0.1" - shell-quote "^1.6.1" - subarg "^1.0.0" - -csstype@^3.0.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" - integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== - -debug@^2.2.0, debug@^2.3.3: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -decode-uri-component@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" - integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA== - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA== - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -duplexer@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - -expand-brackets@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" - integrity sha512-hxx03P2dJxss6ceIeri9cmYOT4SRs3Zk3afZwWpOsRqLqprhTR8u++SlC+sFGsQr7WGFPdMF7Gjc1njDLDK6UA== - dependencies: - is-posix-bracket "^0.1.0" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA== - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expand-range@^1.8.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" - integrity sha512-AFASGfIlnIbkKPQwX1yHaDjFvh/1gyKJODme52V6IORh69uEYgZp0o9C+qsIGNVEiuuhQU0CSSl++Rlegg1qvA== - dependencies: - fill-range "^2.1.0" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q== - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extglob@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" - integrity sha512-1FOj1LOwn42TMrruOHGt18HemVnbwAmAak7krWk+wa93KXxGbK+2jpezm+ytJYDaBX0/SPLZFHKM7m+tKobWGg== - dependencies: - is-extglob "^1.0.0" - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -filename-regex@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" - integrity sha512-BTCqyBaWBTsauvnHiE8i562+EdJj+oUpkqWp2R1iCoR8f6oo8STRu3of7WJJ0TqWtxN50a5YFpzYK4Jj9esYfQ== - -fill-range@^2.1.0: - version "2.2.4" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565" - integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q== - dependencies: - is-number "^2.1.0" - isobject "^2.0.0" - randomatic "^3.0.0" - repeat-element "^1.1.2" - repeat-string "^1.5.2" - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ== - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -find-index@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" - integrity sha512-uJ5vWrfBKMcE6y2Z8834dwEZj9mNGxYa3t3I53OwFeuZ8D9oc2E5zcsrkuhX6h4iYrjhiv0T3szQmxlAV9uxDg== - -for-in@^1.0.1, for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ== - -for-own@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" - integrity sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw== - dependencies: - for-in "^1.0.1" - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA== - dependencies: - map-cache "^0.2.2" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@^1.0.0: - version "1.2.13" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" - integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== - dependencies: - bindings "^1.5.0" - nan "^2.12.1" - -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA== - -glob-base@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" - integrity sha512-ab1S1g1EbO7YzauaJLkgLp7DZVAqj9M/dvKlTt8DkXA2tiOIcSMrlVI2J1RZyB5iJVccEscjGn+kpOG9788MHA== - dependencies: - glob-parent "^2.0.0" - is-glob "^2.0.0" - -glob-parent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" - integrity sha512-JDYOvfxio/t42HKdxkAYaCiBN7oYiuxykOxKxdaUW5Qn0zaYN3gRQWolrwdnf0shM9/EP0ebuuTmyoXNr1cC5w== - dependencies: - is-glob "^2.0.0" - -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob2base@^0.0.12: - version "0.0.12" - resolved "https://registry.yarnpkg.com/glob2base/-/glob2base-0.0.12.tgz#9d419b3e28f12e83a362164a277055922c9c0d56" - integrity sha512-ZyqlgowMbfj2NPjxaZZ/EtsXlOch28FRXgMd64vqZWk1bT9+wvSRLYD1om9M7QfQru51zJPAT17qXm4/zd+9QA== - dependencies: - find-index "^0.1.1" - -glob@^7.0.5: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -graceful-fs@^4.1.11: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q== - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw== - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ== - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ== - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" - integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== - -immutable@^4.0.0: - version "4.3.4" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f" - integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.1, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A== - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q== - dependencies: - binary-extensions "^1.0.0" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-core-module@^2.13.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" - integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== - dependencies: - has "^1.0.3" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg== - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-dotfile@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" - integrity sha512-9YclgOGtN/f8zx0Pr4FQYMdibBiTaH3sn52vjYip4ZSf6C4/6RfTEZ+MR4GvKhCxdPh21Bg42/WL55f6KSnKpg== - -is-equal-shallow@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" - integrity sha512-0EygVC5qPvIyb+gSz7zdD5/AAoS6Qrx1e//6N4yv4oNm30kqvdmG66oZFWVlQHUWe5OjP08FuTw2IdT0EOTcYA== - dependencies: - is-primitive "^2.0.0" - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" - integrity sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-glob@^2.0.0, is-glob@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" - integrity sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg== - dependencies: - is-extglob "^1.0.0" - -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-number@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" - integrity sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg== - dependencies: - kind-of "^3.0.2" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg== - dependencies: - kind-of "^3.0.2" - -is-number@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" - integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-posix-bracket@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" - integrity sha512-Yu68oeXJ7LeWNmZ3Zov/xg/oDBnBK2RNxwYY1ilNJX+tKKZqgPK+qOn/Gs9jEu66KDY9Netf5XLKNGzas/vPfQ== - -is-primitive@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" - integrity sha512-N3w1tFaRfk3UrPfqeRyD+GYDASU3W5VinKhlORy8EWVf/sIdDL9GAcew85XmktCfH+ngG7SRXEVDoO18WMdB/Q== - -is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -isarray@1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA== - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ== - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw== - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg== - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w== - dependencies: - object-visit "^1.0.0" - -math-random@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" - integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== - -micromatch@^2.1.5: - version "2.3.11" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" - integrity sha512-LnU2XFEk9xxSJ6rfgAry/ty5qwUTyHYOBU0g4R6tIw5ljwgGIBmiKhRWLw5NpMOnrgUNcDJ4WMp8rl3sYVHLNA== - dependencies: - arr-diff "^2.0.0" - array-unique "^0.2.1" - braces "^1.8.2" - expand-brackets "^0.1.4" - extglob "^0.3.1" - filename-regex "^2.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.1" - kind-of "^3.0.2" - normalize-path "^2.0.1" - object.omit "^2.0.0" - parse-glob "^3.0.4" - regex-cache "^0.4.2" - -micromatch@^3.1.10: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -minimatch@^3.0.2, minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.1.0, minimist@^1.2.6: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp@^0.5.1: - version "0.5.6" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" - integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== - dependencies: - minimist "^1.2.6" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -nan@^2.12.1: - version "2.18.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" - integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -normalize-path@^2.0.0, normalize-path@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w== - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ== - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA== - dependencies: - isobject "^3.0.0" - -object.omit@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" - integrity sha512-UiAM5mhmIuKLsOvrL+B0U2d1hXHF3bFYWIuH1LMpuV2EJEHG1Ntz06PgLEHjm6VFd87NpH8rastvPoyv6UW2fA== - dependencies: - for-own "^0.1.4" - is-extendable "^0.1.1" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ== - dependencies: - isobject "^3.0.1" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -parse-glob@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" - integrity sha512-FC5TeK0AwXzq3tUBFtH74naWkPQCEWs4K+xMxWZBlKDWu0bVHXGZa+KKqxKidd7xwhdZ19ZNuF2uO1M/r196HA== - dependencies: - glob-base "^0.3.0" - is-dotfile "^1.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.0" - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg== - -preserve@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" - integrity sha512-s/46sYeylUfHNjI+sA/78FAHlmIuKqI9wNnzEOGehAlUUYeObv5C2mOinXBjyUyWmJ2SfcS2/ydApH4hTF4WXQ== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -randomatic@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" - integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw== - dependencies: - is-number "^4.0.0" - kind-of "^6.0.0" - math-random "^1.0.1" - -readable-stream@^2.0.2: - version "2.3.8" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readdirp@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - -regex-cache@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" - integrity sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ== - dependencies: - is-equal-shallow "^0.1.3" - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw== - -repeat-element@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" - integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== - -repeat-string@^1.5.2, repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg== - -resolve@^1.1.7: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -safe-buffer@^5.0.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg== - dependencies: - ret "~0.1.10" - -sass@^1.69.4: - version "1.69.4" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.4.tgz#10c735f55e3ea0b7742c6efa940bce30e07fbca2" - integrity sha512-+qEreVhqAy8o++aQfCJwp0sklr2xyEzkm9Pp/Igu9wNPoe7EZEQ8X/MBvvXggI2ql607cxKg/RKOwDj6pp2XDA== - dependencies: - chokidar ">=3.0.0 <4.0.0" - immutable "^4.0.0" - source-map-js ">=0.6.2 <2.0.0" - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -shell-quote@^1.6.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" - integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -"source-map-js@>=0.6.2 <2.0.0": - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== - -source-map-resolve@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-url@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" - integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== - -source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g== - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -subarg@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" - integrity sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg== - dependencies: - minimist "^1.1.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg== - dependencies: - kind-of "^3.0.2" - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg== - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -typescript@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" - integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ== - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg== - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== diff --git a/pkg/scene/delete.go b/pkg/scene/delete.go index 7426c390b..8ca3d6e11 100644 --- a/pkg/scene/delete.go +++ b/pkg/scene/delete.go @@ -21,6 +21,7 @@ type FileDeleter struct { } // MarkGeneratedFiles marks for deletion the generated files for the provided scene. +// Generated files bypass trash and are permanently deleted since they can be regenerated. func (d *FileDeleter) MarkGeneratedFiles(scene *models.Scene) error { sceneHash := scene.GetHash(d.FileNamingAlgo) @@ -32,7 +33,7 @@ func (d *FileDeleter) MarkGeneratedFiles(scene *models.Scene) error { exists, _ := fsutil.FileExists(markersFolder) if exists { - if err := d.Dirs([]string{markersFolder}); err != nil { + if err := d.DirsWithoutTrash([]string{markersFolder}); err != nil { return err } } @@ -75,11 +76,12 @@ func (d *FileDeleter) MarkGeneratedFiles(scene *models.Scene) error { files = append(files, heatmapPath) } - return d.Files(files) + return d.FilesWithoutTrash(files) } // MarkMarkerFiles deletes generated files for a scene marker with the // provided scene and timestamp. +// Generated files bypass trash and are permanently deleted since they can be regenerated. func (d *FileDeleter) MarkMarkerFiles(scene *models.Scene, seconds int) error { videoPath := d.Paths.SceneMarkers.GetVideoPreviewPath(scene.GetHash(d.FileNamingAlgo), seconds) imagePath := d.Paths.SceneMarkers.GetWebpPreviewPath(scene.GetHash(d.FileNamingAlgo), seconds) @@ -102,12 +104,12 @@ func (d *FileDeleter) MarkMarkerFiles(scene *models.Scene, seconds int) error { files = append(files, screenshotPath) } - return d.Files(files) + return d.FilesWithoutTrash(files) } // Destroy deletes a scene and its associated relationships from the // database. -func (s *Service) Destroy(ctx context.Context, scene *models.Scene, fileDeleter *FileDeleter, deleteGenerated, deleteFile bool) error { +func (s *Service) Destroy(ctx context.Context, scene *models.Scene, fileDeleter *FileDeleter, deleteGenerated, deleteFile, destroyFileEntry bool) error { mqb := s.MarkerRepository markers, err := mqb.FindBySceneID(ctx, scene.ID) if err != nil { @@ -124,6 +126,10 @@ func (s *Service) Destroy(ctx context.Context, scene *models.Scene, fileDeleter if err := s.deleteFiles(ctx, scene, fileDeleter); err != nil { return err } + } else if destroyFileEntry { + if err := s.destroyFileEntries(ctx, scene); err != nil { + return err + } } if deleteGenerated { @@ -178,6 +184,35 @@ func (s *Service) deleteFiles(ctx context.Context, scene *models.Scene, fileDele return nil } +// destroyFileEntries destroys file entries from the database without deleting +// the files from the filesystem +func (s *Service) destroyFileEntries(ctx context.Context, scene *models.Scene) error { + if err := scene.LoadFiles(ctx, s.Repository); err != nil { + return err + } + + for _, f := range scene.Files.List() { + // only destroy file entries where there is no other associated scene + otherScenes, err := s.Repository.FindByFileID(ctx, f.ID) + if err != nil { + return err + } + + if len(otherScenes) > 1 { + // other scenes associated, don't remove + continue + } + + const deleteFile = false + logger.Info("Destroying scene file entry: ", f.Path) + if err := file.Destroy(ctx, s.File, f, nil, deleteFile); err != nil { + return err + } + } + + return nil +} + // DestroyMarker deletes the scene marker from the database and returns a // function that removes the generated files, to be executed after the // transaction is successfully committed. diff --git a/pkg/scene/import.go b/pkg/scene/import.go index e1248a77c..b3f0f1ff1 100644 --- a/pkg/scene/import.go +++ b/pkg/scene/import.go @@ -164,7 +164,7 @@ func (i *Importer) populateFiles(ctx context.Context) error { for _, ref := range i.Input.Files { path := ref - f, err := i.FileFinder.FindByPath(ctx, path) + f, err := i.FileFinder.FindByPath(ctx, path, true) if err != nil { return fmt.Errorf("error finding file: %w", err) } @@ -213,7 +213,7 @@ func (i *Importer) populateStudio(ctx context.Context) error { } func (i *Importer) createStudio(ctx context.Context, name string) (int, error) { - newStudio := models.NewStudio() + newStudio := models.NewCreateStudioInput() newStudio.Name = name err := i.StudioWriter.Create(ctx, &newStudio) diff --git a/pkg/scene/import_test.go b/pkg/scene/import_test.go index a6e3edcdf..558b72ba2 100644 --- a/pkg/scene/import_test.go +++ b/pkg/scene/import_test.go @@ -241,9 +241,9 @@ func TestImporterPreImportWithMissingStudio(t *testing.T) { } db.Studio.On("FindByName", testCtx, missingStudioName, false).Return(nil, nil).Times(3) - db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.Studio")).Run(func(args mock.Arguments) { - s := args.Get(1).(*models.Studio) - s.ID = existingStudioID + db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.CreateStudioInput")).Run(func(args mock.Arguments) { + s := args.Get(1).(*models.CreateStudioInput) + s.Studio.ID = existingStudioID }).Return(nil) err := i.PreImport(testCtx) @@ -273,7 +273,7 @@ func TestImporterPreImportWithMissingStudioCreateErr(t *testing.T) { } db.Studio.On("FindByName", testCtx, missingStudioName, false).Return(nil, nil).Once() - db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.Studio")).Return(errors.New("Create error")) + db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.CreateStudioInput")).Return(errors.New("Create error")) err := i.PreImport(testCtx) assert.NotNil(t, err) diff --git a/pkg/scene/merge.go b/pkg/scene/merge.go index 77b551ab2..b2650ca92 100644 --- a/pkg/scene/merge.go +++ b/pkg/scene/merge.go @@ -120,7 +120,8 @@ func (s *Service) Merge(ctx context.Context, sourceIDs []int, destinationID int, for _, src := range sources { const deleteGenerated = true const deleteFile = false - if err := s.Destroy(ctx, src, fileDeleter, deleteGenerated, deleteFile); err != nil { + const destroyFileEntry = false + if err := s.Destroy(ctx, src, fileDeleter, deleteGenerated, deleteFile, destroyFileEntry); err != nil { return fmt.Errorf("deleting scene %d: %w", src.ID, err) } } diff --git a/pkg/scraper/action.go b/pkg/scraper/action.go index 74bbca415..cd31fbe72 100644 --- a/pkg/scraper/action.go +++ b/pkg/scraper/action.go @@ -24,9 +24,85 @@ func (e scraperAction) IsValid() bool { return false } -type scraperActionImpl interface { +type urlScraperActionImpl interface { scrapeByURL(ctx context.Context, url string, ty ScrapeContentType) (ScrapedContent, error) +} + +func (c Definition) getURLScraper(def ByURLDefinition, client *http.Client, globalConfig GlobalConfig) urlScraperActionImpl { + switch def.Action { + case scraperActionScript: + return &scriptURLScraper{ + scriptScraper: scriptScraper{ + definition: c, + globalConfig: globalConfig, + }, + definition: def, + } + case scraperActionStash: + return newStashScraper(client, c, globalConfig) + case scraperActionXPath: + return &xpathURLScraper{ + xpathScraper: xpathScraper{ + definition: c, + globalConfig: globalConfig, + client: client, + }, + definition: def, + } + case scraperActionJson: + return &jsonURLScraper{ + jsonScraper: jsonScraper{ + definition: c, + globalConfig: globalConfig, + client: client, + }, + definition: def, + } + } + + panic("unknown scraper action: " + def.Action) +} + +type nameScraperActionImpl interface { scrapeByName(ctx context.Context, name string, ty ScrapeContentType) ([]ScrapedContent, error) +} + +func (c Definition) getNameScraper(def ByNameDefinition, client *http.Client, globalConfig GlobalConfig) nameScraperActionImpl { + switch def.Action { + case scraperActionScript: + return &scriptNameScraper{ + scriptScraper: scriptScraper{ + definition: c, + globalConfig: globalConfig, + }, + definition: def, + } + case scraperActionStash: + return newStashScraper(client, c, globalConfig) + case scraperActionXPath: + return &xpathNameScraper{ + xpathScraper: xpathScraper{ + definition: c, + globalConfig: globalConfig, + client: client, + }, + definition: def, + } + case scraperActionJson: + return &jsonNameScraper{ + jsonScraper: jsonScraper{ + definition: c, + globalConfig: globalConfig, + client: client, + }, + definition: def, + } + } + + panic("unknown scraper action: " + def.Action) +} + +type fragmentScraperActionImpl interface { scrapeByFragment(ctx context.Context, input Input) (ScrapedContent, error) scrapeSceneByScene(ctx context.Context, scene *models.Scene) (*models.ScrapedScene, error) @@ -34,17 +110,37 @@ type scraperActionImpl interface { scrapeImageByImage(ctx context.Context, image *models.Image) (*models.ScrapedImage, error) } -func (c config) getScraper(scraper scraperTypeConfig, client *http.Client, globalConfig GlobalConfig) scraperActionImpl { - switch scraper.Action { +func (c Definition) getFragmentScraper(actionDef ByFragmentDefinition, client *http.Client, globalConfig GlobalConfig) fragmentScraperActionImpl { + switch actionDef.Action { case scraperActionScript: - return newScriptScraper(scraper, c, globalConfig) + return &scriptFragmentScraper{ + scriptScraper: scriptScraper{ + definition: c, + globalConfig: globalConfig, + }, + definition: actionDef, + } case scraperActionStash: - return newStashScraper(scraper, client, c, globalConfig) + return newStashScraper(client, c, globalConfig) case scraperActionXPath: - return newXpathScraper(scraper, client, c, globalConfig) + return &xpathFragmentScraper{ + xpathScraper: xpathScraper{ + definition: c, + globalConfig: globalConfig, + client: client, + }, + definition: actionDef, + } case scraperActionJson: - return newJsonScraper(scraper, client, c, globalConfig) + return &jsonFragmentScraper{ + jsonScraper: jsonScraper{ + definition: c, + globalConfig: globalConfig, + client: client, + }, + definition: actionDef, + } } - panic("unknown scraper action: " + scraper.Action) + panic("unknown scraper action: " + actionDef.Action) } diff --git a/pkg/scraper/cache.go b/pkg/scraper/cache.go index d2c3bd5d9..6aeb95fcf 100644 --- a/pkg/scraper/cache.go +++ b/pkg/scraper/cache.go @@ -16,7 +16,6 @@ import ( "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/match" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil" "github.com/stashapp/stash/pkg/txn" ) @@ -183,7 +182,7 @@ func (c *Cache) ReloadScrapers() { if err != nil { logger.Errorf("Error loading scraper %s: %v", fp, err) } else { - scraper := newGroupScraper(*conf, c.globalConfig) + scraper := scraperFromDefinition(*conf, c.globalConfig) scrapers[scraper.spec().ID] = scraper } } @@ -262,19 +261,23 @@ func (c Cache) ScrapeName(ctx context.Context, id, query string, ty ScrapeConten return nil, fmt.Errorf("error while name scraping with scraper %s: %w", id, err) } - ignoredRegex := c.compileExcludeTagPatterns() - - var ignoredTags []string - for i, cc := range content { - var thisIgnoredTags []string - content[i], thisIgnoredTags, err = c.postScrape(ctx, cc, ignoredRegex) - if err != nil { - return nil, fmt.Errorf("error while post-scraping with scraper %s: %w", id, err) + pp := postScraper{ + Cache: c, + excludeTagRE: c.compileExcludeTagPatterns(), + } + if err := c.repository.WithReadTxn(ctx, func(ctx context.Context) error { + for i, cc := range content { + content[i], err = pp.postScrape(ctx, cc) + if err != nil { + return fmt.Errorf("error while post-scraping with scraper %s: %w", id, err) + } } - ignoredTags = sliceutil.AppendUniques(ignoredTags, thisIgnoredTags) + return nil + }); err != nil { + return nil, err } - LogIgnoredTags(ignoredTags) + LogIgnoredTags(pp.ignoredTags) return content, nil } diff --git a/pkg/scraper/cookies.go b/pkg/scraper/cookies.go index 0a2877b7b..c76dae037 100644 --- a/pkg/scraper/cookies.go +++ b/pkg/scraper/cookies.go @@ -18,7 +18,7 @@ import ( ) // jar constructs a cookie jar from a configuration -func (c config) jar() (*cookiejar.Jar, error) { +func (c Definition) jar() (*cookiejar.Jar, error) { opts := c.DriverOptions jar, err := cookiejar.New(&cookiejar.Options{ PublicSuffixList: publicsuffix.List, @@ -77,7 +77,7 @@ func randomSequence(n int) string { } // printCookies prints all cookies from the given cookie jar -func printCookies(jar *cookiejar.Jar, scraperConfig config, msg string) { +func printCookies(jar *cookiejar.Jar, scraperConfig Definition, msg string) { driverOptions := scraperConfig.DriverOptions if driverOptions != nil && !driverOptions.UseCDP { var foundURLs []*url.URL diff --git a/pkg/scraper/group.go b/pkg/scraper/defined_scraper.go similarity index 56% rename from pkg/scraper/group.go rename to pkg/scraper/defined_scraper.go index 43fd2a37b..0287101d0 100644 --- a/pkg/scraper/group.go +++ b/pkg/scraper/defined_scraper.go @@ -8,25 +8,26 @@ import ( "github.com/stashapp/stash/pkg/models" ) -type group struct { - config config +// definedScraper implements the scraper interface using a Definition object. +type definedScraper struct { + config Definition globalConf GlobalConfig } -func newGroupScraper(c config, globalConfig GlobalConfig) scraper { - return group{ +func scraperFromDefinition(c Definition, globalConfig GlobalConfig) definedScraper { + return definedScraper{ config: c, globalConf: globalConfig, } } -func (g group) spec() Scraper { +func (g definedScraper) spec() Scraper { return g.config.spec() } // fragmentScraper finds an appropriate fragment scraper based on input. -func (g group) fragmentScraper(input Input) *scraperTypeConfig { +func (g definedScraper) fragmentScraper(input Input) *ByFragmentDefinition { switch { case input.Performer != nil: return g.config.PerformerByFragment @@ -43,7 +44,7 @@ func (g group) fragmentScraper(input Input) *scraperTypeConfig { return nil } -func (g group) viaFragment(ctx context.Context, client *http.Client, input Input) (ScrapedContent, error) { +func (g definedScraper) viaFragment(ctx context.Context, client *http.Client, input Input) (ScrapedContent, error) { stc := g.fragmentScraper(input) if stc == nil { // If there's no performer fragment scraper in the group, we try to use @@ -56,38 +57,38 @@ func (g group) viaFragment(ctx context.Context, client *http.Client, input Input return nil, ErrNotSupported } - s := g.config.getScraper(*stc, client, g.globalConf) + s := g.config.getFragmentScraper(*stc, client, g.globalConf) return s.scrapeByFragment(ctx, input) } -func (g group) viaScene(ctx context.Context, client *http.Client, scene *models.Scene) (*models.ScrapedScene, error) { +func (g definedScraper) viaScene(ctx context.Context, client *http.Client, scene *models.Scene) (*models.ScrapedScene, error) { if g.config.SceneByFragment == nil { return nil, ErrNotSupported } - s := g.config.getScraper(*g.config.SceneByFragment, client, g.globalConf) + s := g.config.getFragmentScraper(*g.config.SceneByFragment, client, g.globalConf) return s.scrapeSceneByScene(ctx, scene) } -func (g group) viaGallery(ctx context.Context, client *http.Client, gallery *models.Gallery) (*models.ScrapedGallery, error) { +func (g definedScraper) viaGallery(ctx context.Context, client *http.Client, gallery *models.Gallery) (*models.ScrapedGallery, error) { if g.config.GalleryByFragment == nil { return nil, ErrNotSupported } - s := g.config.getScraper(*g.config.GalleryByFragment, client, g.globalConf) + s := g.config.getFragmentScraper(*g.config.GalleryByFragment, client, g.globalConf) return s.scrapeGalleryByGallery(ctx, gallery) } -func (g group) viaImage(ctx context.Context, client *http.Client, gallery *models.Image) (*models.ScrapedImage, error) { +func (g definedScraper) viaImage(ctx context.Context, client *http.Client, gallery *models.Image) (*models.ScrapedImage, error) { if g.config.ImageByFragment == nil { return nil, ErrNotSupported } - s := g.config.getScraper(*g.config.ImageByFragment, client, g.globalConf) + s := g.config.getFragmentScraper(*g.config.ImageByFragment, client, g.globalConf) return s.scrapeImageByImage(ctx, gallery) } -func loadUrlCandidates(c config, ty ScrapeContentType) []*scrapeByURLConfig { +func loadUrlCandidates(c Definition, ty ScrapeContentType) []*ByURLDefinition { switch ty { case ScrapeContentTypePerformer: return c.PerformerByURL @@ -104,12 +105,13 @@ func loadUrlCandidates(c config, ty ScrapeContentType) []*scrapeByURLConfig { panic("loadUrlCandidates: unreachable") } -func (g group) viaURL(ctx context.Context, client *http.Client, url string, ty ScrapeContentType) (ScrapedContent, error) { +func (g definedScraper) viaURL(ctx context.Context, client *http.Client, url string, ty ScrapeContentType) (ScrapedContent, error) { candidates := loadUrlCandidates(g.config, ty) for _, scraper := range candidates { if scraper.matchesURL(url) { - s := g.config.getScraper(scraper.scraperTypeConfig, client, g.globalConf) - ret, err := s.scrapeByURL(ctx, url, ty) + u := replaceURL(url, *scraper) // allow a URL Replace for url-queries + s := g.config.getURLScraper(*scraper, client, g.globalConf) + ret, err := s.scrapeByURL(ctx, u, ty) if err != nil { return nil, err } @@ -123,31 +125,31 @@ func (g group) viaURL(ctx context.Context, client *http.Client, url string, ty S return nil, nil } -func (g group) viaName(ctx context.Context, client *http.Client, name string, ty ScrapeContentType) ([]ScrapedContent, error) { +func (g definedScraper) viaName(ctx context.Context, client *http.Client, name string, ty ScrapeContentType) ([]ScrapedContent, error) { switch ty { case ScrapeContentTypePerformer: if g.config.PerformerByName == nil { break } - s := g.config.getScraper(*g.config.PerformerByName, client, g.globalConf) + s := g.config.getNameScraper(*g.config.PerformerByName, client, g.globalConf) return s.scrapeByName(ctx, name, ty) case ScrapeContentTypeScene: if g.config.SceneByName == nil { break } - s := g.config.getScraper(*g.config.SceneByName, client, g.globalConf) + s := g.config.getNameScraper(*g.config.SceneByName, client, g.globalConf) return s.scrapeByName(ctx, name, ty) } return nil, fmt.Errorf("%w: cannot load %v by name", ErrNotSupported, ty) } -func (g group) supports(ty ScrapeContentType) bool { +func (g definedScraper) supports(ty ScrapeContentType) bool { return g.config.supports(ty) } -func (g group) supportsURL(url string, ty ScrapeContentType) bool { +func (g definedScraper) supportsURL(url string, ty ScrapeContentType) bool { return g.config.matchesURL(url, ty) } diff --git a/pkg/scraper/config.go b/pkg/scraper/definition.go similarity index 80% rename from pkg/scraper/config.go rename to pkg/scraper/definition.go index 5775dc97c..03ba4d75b 100644 --- a/pkg/scraper/config.go +++ b/pkg/scraper/definition.go @@ -11,7 +11,8 @@ import ( "gopkg.in/yaml.v2" ) -type config struct { +// Definition represents a scraper definition (typically) loaded from a YAML configuration file. +type Definition struct { ID string path string @@ -19,43 +20,43 @@ type config struct { Name string `yaml:"name"` // Configuration for querying performers by name - PerformerByName *scraperTypeConfig `yaml:"performerByName"` + PerformerByName *ByNameDefinition `yaml:"performerByName"` // Configuration for querying performers by a Performer fragment - PerformerByFragment *scraperTypeConfig `yaml:"performerByFragment"` + PerformerByFragment *ByFragmentDefinition `yaml:"performerByFragment"` // Configuration for querying a performer by a URL - PerformerByURL []*scrapeByURLConfig `yaml:"performerByURL"` + PerformerByURL []*ByURLDefinition `yaml:"performerByURL"` // Configuration for querying scenes by a Scene fragment - SceneByFragment *scraperTypeConfig `yaml:"sceneByFragment"` + SceneByFragment *ByFragmentDefinition `yaml:"sceneByFragment"` // Configuration for querying gallery by a Gallery fragment - GalleryByFragment *scraperTypeConfig `yaml:"galleryByFragment"` + GalleryByFragment *ByFragmentDefinition `yaml:"galleryByFragment"` // Configuration for querying scenes by name - SceneByName *scraperTypeConfig `yaml:"sceneByName"` + SceneByName *ByNameDefinition `yaml:"sceneByName"` // Configuration for querying scenes by query fragment - SceneByQueryFragment *scraperTypeConfig `yaml:"sceneByQueryFragment"` + SceneByQueryFragment *ByFragmentDefinition `yaml:"sceneByQueryFragment"` // Configuration for querying a scene by a URL - SceneByURL []*scrapeByURLConfig `yaml:"sceneByURL"` + SceneByURL []*ByURLDefinition `yaml:"sceneByURL"` // Configuration for querying a gallery by a URL - GalleryByURL []*scrapeByURLConfig `yaml:"galleryByURL"` + GalleryByURL []*ByURLDefinition `yaml:"galleryByURL"` // Configuration for querying an image by a URL - ImageByURL []*scrapeByURLConfig `yaml:"imageByURL"` + ImageByURL []*ByURLDefinition `yaml:"imageByURL"` // Configuration for querying image by an Image fragment - ImageByFragment *scraperTypeConfig `yaml:"imageByFragment"` + ImageByFragment *ByFragmentDefinition `yaml:"imageByFragment"` // Configuration for querying a movie by a URL - deprecated, use GroupByURL - MovieByURL []*scrapeByURLConfig `yaml:"movieByURL"` + MovieByURL []*ByURLDefinition `yaml:"movieByURL"` // Configuration for querying a group by a URL - GroupByURL []*scrapeByURLConfig `yaml:"groupByURL"` + GroupByURL []*ByURLDefinition `yaml:"groupByURL"` // Scraper debugging options DebugOptions *scraperDebugOptions `yaml:"debug"` @@ -73,7 +74,7 @@ type config struct { DriverOptions *scraperDriverOptions `yaml:"driver"` } -func (c config) validate() error { +func (c Definition) validate() error { if strings.TrimSpace(c.Name) == "" { return errors.New("name must not be empty") } @@ -126,17 +127,13 @@ type stashServer struct { ApiKey string `yaml:"apiKey"` } -type scraperTypeConfig struct { +type ActionDefinition struct { Action scraperAction `yaml:"action"` Script []string `yaml:"script,flow"` Scraper string `yaml:"scraper"` - - // for xpath name scraper only - QueryURL string `yaml:"queryURL"` - QueryURLReplacements queryURLReplacements `yaml:"queryURLReplace"` } -func (c scraperTypeConfig) validate() error { +func (c ActionDefinition) validate() error { if !c.Action.IsValid() { return fmt.Errorf("%s is not a valid scraper action", c.Action) } @@ -148,20 +145,22 @@ func (c scraperTypeConfig) validate() error { return nil } -type scrapeByURLConfig struct { - scraperTypeConfig `yaml:",inline"` - URL []string `yaml:"url,flow"` +type ByURLDefinition struct { + ActionDefinition `yaml:",inline"` + URL []string `yaml:"url,flow"` + QueryURL string `yaml:"queryURL"` + QueryURLReplacements queryURLReplacements `yaml:"queryURLReplace"` } -func (c scrapeByURLConfig) validate() error { +func (c ByURLDefinition) validate() error { if len(c.URL) == 0 { return errors.New("url is mandatory for scrape by url scrapers") } - return c.scraperTypeConfig.validate() + return c.ActionDefinition.validate() } -func (c scrapeByURLConfig) matchesURL(url string) bool { +func (c ByURLDefinition) matchesURL(url string) bool { for _, thisURL := range c.URL { if strings.Contains(url, thisURL) { return true @@ -171,6 +170,18 @@ func (c scrapeByURLConfig) matchesURL(url string) bool { return false } +type ByFragmentDefinition struct { + ActionDefinition `yaml:",inline"` + + QueryURL string `yaml:"queryURL"` + QueryURLReplacements queryURLReplacements `yaml:"queryURLReplace"` +} + +type ByNameDefinition struct { + ActionDefinition `yaml:",inline"` + QueryURL string `yaml:"queryURL"` +} + type scraperDebugOptions struct { PrintHTML bool `yaml:"printHTML"` } @@ -206,8 +217,8 @@ type scraperDriverOptions struct { Headers []*header `yaml:"headers"` } -func loadConfigFromYAML(id string, reader io.Reader) (*config, error) { - ret := &config{} +func loadConfigFromYAML(id string, reader io.Reader) (*Definition, error) { + ret := &Definition{} parser := yaml.NewDecoder(reader) parser.SetStrict(true) @@ -225,7 +236,7 @@ func loadConfigFromYAML(id string, reader io.Reader) (*config, error) { return ret, nil } -func loadConfigFromYAMLFile(path string) (*config, error) { +func loadConfigFromYAMLFile(path string) (*Definition, error) { file, err := os.Open(path) if err != nil { return nil, err @@ -246,7 +257,7 @@ func loadConfigFromYAMLFile(path string) (*config, error) { return ret, nil } -func (c config) spec() Scraper { +func (c Definition) spec() Scraper { ret := Scraper{ ID: c.ID, Name: c.Name, @@ -334,7 +345,7 @@ func (c config) spec() Scraper { return ret } -func (c config) supports(ty ScrapeContentType) bool { +func (c Definition) supports(ty ScrapeContentType) bool { switch ty { case ScrapeContentTypePerformer: return c.PerformerByName != nil || c.PerformerByFragment != nil || len(c.PerformerByURL) > 0 @@ -351,7 +362,7 @@ func (c config) supports(ty ScrapeContentType) bool { panic("Unhandled ScrapeContentType") } -func (c config) matchesURL(url string, ty ScrapeContentType) bool { +func (c Definition) matchesURL(url string, ty ScrapeContentType) bool { switch ty { case ScrapeContentTypePerformer: for _, scraper := range c.PerformerByURL { diff --git a/pkg/scraper/freeones.go b/pkg/scraper/freeones.go index 96caf2fec..e78488b24 100644 --- a/pkg/scraper/freeones.go +++ b/pkg/scraper/freeones.go @@ -139,5 +139,5 @@ func getFreeonesScraper(globalConfig GlobalConfig) scraper { logger.Fatalf("Error loading builtin freeones scraper: %s", err.Error()) } - return newGroupScraper(*c, globalConfig) + return scraperFromDefinition(*c, globalConfig) } diff --git a/pkg/scraper/image.go b/pkg/scraper/image.go index 93ed7a037..2f2e038af 100644 --- a/pkg/scraper/image.go +++ b/pkg/scraper/image.go @@ -37,88 +37,49 @@ func setPerformerImage(ctx context.Context, client *http.Client, p *models.Scrap return nil } -func setSceneImage(ctx context.Context, client *http.Client, s *models.ScrapedScene, globalConfig GlobalConfig) error { - // don't try to get the image if it doesn't appear to be a URL - if s.Image == nil || !strings.HasPrefix(*s.Image, "http") { +func setStudioImage(ctx context.Context, client *http.Client, p *models.ScrapedStudio, globalConfig GlobalConfig) error { + // backwards compatibility: we fetch the image if it's a URL and set it to the first image + // Image is deprecated, so only do this if Images is unset + if p.Image == nil || len(p.Images) > 0 { // nothing to do return nil } - img, err := getImage(ctx, *s.Image, client, globalConfig) + // don't try to get the image if it doesn't appear to be a URL + if !strings.HasPrefix(*p.Image, "http") { + p.Images = []string{*p.Image} + return nil + } + + img, err := getImage(ctx, *p.Image, client, globalConfig) if err != nil { return err } - s.Image = img + p.Image = img + // Image is deprecated. Use images instead + p.Images = []string{*img} return nil } -func setMovieFrontImage(ctx context.Context, client *http.Client, m *models.ScrapedMovie, globalConfig GlobalConfig) error { - // don't try to get the image if it doesn't appear to be a URL - if m.FrontImage == nil || !strings.HasPrefix(*m.FrontImage, "http") { - // nothing to do +func processImageField(ctx context.Context, imageField *string, client *http.Client, globalConfig GlobalConfig) error { + if imageField == nil { return nil } - img, err := getImage(ctx, *m.FrontImage, client, globalConfig) + // don't try to get the image if it doesn't appear to be a URL + // this allows scrapers to return base64 data URIs directly + if !strings.HasPrefix(*imageField, "http") { + return nil + } + + img, err := getImage(ctx, *imageField, client, globalConfig) if err != nil { return err } - m.FrontImage = img - - return nil -} - -func setMovieBackImage(ctx context.Context, client *http.Client, m *models.ScrapedMovie, globalConfig GlobalConfig) error { - // don't try to get the image if it doesn't appear to be a URL - if m.BackImage == nil || !strings.HasPrefix(*m.BackImage, "http") { - // nothing to do - return nil - } - - img, err := getImage(ctx, *m.BackImage, client, globalConfig) - if err != nil { - return err - } - - m.BackImage = img - - return nil -} - -func setGroupFrontImage(ctx context.Context, client *http.Client, m *models.ScrapedGroup, globalConfig GlobalConfig) error { - // don't try to get the image if it doesn't appear to be a URL - if m.FrontImage == nil || !strings.HasPrefix(*m.FrontImage, "http") { - // nothing to do - return nil - } - - img, err := getImage(ctx, *m.FrontImage, client, globalConfig) - if err != nil { - return err - } - - m.FrontImage = img - - return nil -} - -func setGroupBackImage(ctx context.Context, client *http.Client, m *models.ScrapedGroup, globalConfig GlobalConfig) error { - // don't try to get the image if it doesn't appear to be a URL - if m.BackImage == nil || !strings.HasPrefix(*m.BackImage, "http") { - // nothing to do - return nil - } - - img, err := getImage(ctx, *m.BackImage, client, globalConfig) - if err != nil { - return err - } - - m.BackImage = img - + *imageField = *img return nil } diff --git a/pkg/scraper/json.go b/pkg/scraper/json.go index fc7eb17a2..1dcb887da 100644 --- a/pkg/scraper/json.go +++ b/pkg/scraper/json.go @@ -15,43 +15,22 @@ import ( ) type jsonScraper struct { - scraper scraperTypeConfig - config config + definition Definition globalConfig GlobalConfig client *http.Client } -func newJsonScraper(scraper scraperTypeConfig, client *http.Client, config config, globalConfig GlobalConfig) *jsonScraper { - return &jsonScraper{ - scraper: scraper, - config: config, - client: client, - globalConfig: globalConfig, - } -} - -func (s *jsonScraper) getJsonScraper() *mappedScraper { - return s.config.JsonScrapers[s.scraper.Scraper] -} - -func (s *jsonScraper) scrapeURL(ctx context.Context, url string) (string, *mappedScraper, error) { - scraper := s.getJsonScraper() - - if scraper == nil { - return "", nil, errors.New("json scraper with name " + s.scraper.Scraper + " not found in config") +func (s *jsonScraper) getJsonScraper(name string) (*mappedScraper, error) { + ret, ok := s.definition.JsonScrapers[name] + if !ok { + return nil, fmt.Errorf("json scraper with name %s not found in config", name) } - doc, err := s.loadURL(ctx, url) - - if err != nil { - return "", nil, err - } - - return doc, scraper, nil + return &ret, nil } func (s *jsonScraper) loadURL(ctx context.Context, url string) (string, error) { - r, err := loadURL(ctx, url, s.client, s.config, s.globalConfig) + r, err := loadURL(ctx, url, s.client, s.definition, s.globalConfig) if err != nil { return "", err } @@ -66,21 +45,30 @@ func (s *jsonScraper) loadURL(ctx context.Context, url string) (string, error) { return "", errors.New("not valid json") } - if s.config.DebugOptions != nil && s.config.DebugOptions.PrintHTML { + if s.definition.DebugOptions != nil && s.definition.DebugOptions.PrintHTML { logger.Infof("loadURL (%s) response: \n%s", url, docStr) } return docStr, err } -func (s *jsonScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeContentType) (ScrapedContent, error) { - u := replaceURL(url, s.scraper) // allow a URL Replace for url-queries - doc, scraper, err := s.scrapeURL(ctx, u) +type jsonURLScraper struct { + jsonScraper + definition ByURLDefinition +} + +func (s *jsonURLScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeContentType) (ScrapedContent, error) { + scraper, err := s.getJsonScraper(s.definition.Scraper) if err != nil { return nil, err } - q := s.getJsonQuery(doc) + doc, err := s.loadURL(ctx, url) + if err != nil { + return nil, err + } + + q := s.getJsonQuery(doc, url) // if these just return the return values from scraper.scrape* functions then // it ends up returning ScrapedContent(nil) rather than nil switch ty { @@ -119,11 +107,15 @@ func (s *jsonScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeCont return nil, ErrNotSupported } -func (s *jsonScraper) scrapeByName(ctx context.Context, name string, ty ScrapeContentType) ([]ScrapedContent, error) { - scraper := s.getJsonScraper() +type jsonNameScraper struct { + jsonScraper + definition ByNameDefinition +} - if scraper == nil { - return nil, fmt.Errorf("%w: name %v", ErrNotFound, s.scraper.Scraper) +func (s *jsonNameScraper) scrapeByName(ctx context.Context, name string, ty ScrapeContentType) ([]ScrapedContent, error) { + scraper, err := s.getJsonScraper(s.definition.Scraper) + if err != nil { + return nil, err } const placeholder = "{}" @@ -131,7 +123,7 @@ func (s *jsonScraper) scrapeByName(ctx context.Context, name string, ty ScrapeCo // replace the placeholder string with the URL-escaped name escapedName := url.QueryEscape(name) - url := s.scraper.QueryURL + url := s.definition.QueryURL url = strings.ReplaceAll(url, placeholder, escapedName) doc, err := s.loadURL(ctx, url) @@ -140,7 +132,7 @@ func (s *jsonScraper) scrapeByName(ctx context.Context, name string, ty ScrapeCo return nil, err } - q := s.getJsonQuery(doc) + q := s.getJsonQuery(doc, url) q.setType(SearchQuery) var content []ScrapedContent @@ -172,18 +164,22 @@ func (s *jsonScraper) scrapeByName(ctx context.Context, name string, ty ScrapeCo return nil, ErrNotSupported } -func (s *jsonScraper) scrapeSceneByScene(ctx context.Context, scene *models.Scene) (*models.ScrapedScene, error) { +type jsonFragmentScraper struct { + jsonScraper + definition ByFragmentDefinition +} + +func (s *jsonFragmentScraper) scrapeSceneByScene(ctx context.Context, scene *models.Scene) (*models.ScrapedScene, error) { // construct the URL queryURL := queryURLParametersFromScene(scene) - if s.scraper.QueryURLReplacements != nil { - queryURL.applyReplacements(s.scraper.QueryURLReplacements) + if s.definition.QueryURLReplacements != nil { + queryURL.applyReplacements(s.definition.QueryURLReplacements) } - url := queryURL.constructURL(s.scraper.QueryURL) + url := queryURL.constructURL(s.definition.QueryURL) - scraper := s.getJsonScraper() - - if scraper == nil { - return nil, errors.New("json scraper with name " + s.scraper.Scraper + " not found in config") + scraper, err := s.getJsonScraper(s.definition.Scraper) + if err != nil { + return nil, err } doc, err := s.loadURL(ctx, url) @@ -192,11 +188,11 @@ func (s *jsonScraper) scrapeSceneByScene(ctx context.Context, scene *models.Scen return nil, err } - q := s.getJsonQuery(doc) + q := s.getJsonQuery(doc, url) return scraper.scrapeScene(ctx, q) } -func (s *jsonScraper) scrapeByFragment(ctx context.Context, input Input) (ScrapedContent, error) { +func (s *jsonFragmentScraper) scrapeByFragment(ctx context.Context, input Input) (ScrapedContent, error) { switch { case input.Gallery != nil: return nil, fmt.Errorf("%w: cannot use a json scraper as a gallery fragment scraper", ErrNotSupported) @@ -210,15 +206,14 @@ func (s *jsonScraper) scrapeByFragment(ctx context.Context, input Input) (Scrape // construct the URL queryURL := queryURLParametersFromScrapedScene(scene) - if s.scraper.QueryURLReplacements != nil { - queryURL.applyReplacements(s.scraper.QueryURLReplacements) + if s.definition.QueryURLReplacements != nil { + queryURL.applyReplacements(s.definition.QueryURLReplacements) } - url := queryURL.constructURL(s.scraper.QueryURL) + url := queryURL.constructURL(s.definition.QueryURL) - scraper := s.getJsonScraper() - - if scraper == nil { - return nil, errors.New("xpath scraper with name " + s.scraper.Scraper + " not found in config") + scraper, err := s.getJsonScraper(s.definition.Scraper) + if err != nil { + return nil, err } doc, err := s.loadURL(ctx, url) @@ -227,22 +222,21 @@ func (s *jsonScraper) scrapeByFragment(ctx context.Context, input Input) (Scrape return nil, err } - q := s.getJsonQuery(doc) + q := s.getJsonQuery(doc, url) return scraper.scrapeScene(ctx, q) } -func (s *jsonScraper) scrapeImageByImage(ctx context.Context, image *models.Image) (*models.ScrapedImage, error) { +func (s *jsonFragmentScraper) scrapeImageByImage(ctx context.Context, image *models.Image) (*models.ScrapedImage, error) { // construct the URL queryURL := queryURLParametersFromImage(image) - if s.scraper.QueryURLReplacements != nil { - queryURL.applyReplacements(s.scraper.QueryURLReplacements) + if s.definition.QueryURLReplacements != nil { + queryURL.applyReplacements(s.definition.QueryURLReplacements) } - url := queryURL.constructURL(s.scraper.QueryURL) + url := queryURL.constructURL(s.definition.QueryURL) - scraper := s.getJsonScraper() - - if scraper == nil { - return nil, errors.New("json scraper with name " + s.scraper.Scraper + " not found in config") + scraper, err := s.getJsonScraper(s.definition.Scraper) + if err != nil { + return nil, err } doc, err := s.loadURL(ctx, url) @@ -251,22 +245,21 @@ func (s *jsonScraper) scrapeImageByImage(ctx context.Context, image *models.Imag return nil, err } - q := s.getJsonQuery(doc) + q := s.getJsonQuery(doc, url) return scraper.scrapeImage(ctx, q) } -func (s *jsonScraper) scrapeGalleryByGallery(ctx context.Context, gallery *models.Gallery) (*models.ScrapedGallery, error) { +func (s *jsonFragmentScraper) scrapeGalleryByGallery(ctx context.Context, gallery *models.Gallery) (*models.ScrapedGallery, error) { // construct the URL queryURL := queryURLParametersFromGallery(gallery) - if s.scraper.QueryURLReplacements != nil { - queryURL.applyReplacements(s.scraper.QueryURLReplacements) + if s.definition.QueryURLReplacements != nil { + queryURL.applyReplacements(s.definition.QueryURLReplacements) } - url := queryURL.constructURL(s.scraper.QueryURL) + url := queryURL.constructURL(s.definition.QueryURL) - scraper := s.getJsonScraper() - - if scraper == nil { - return nil, errors.New("json scraper with name " + s.scraper.Scraper + " not found in config") + scraper, err := s.getJsonScraper(s.definition.Scraper) + if err != nil { + return nil, err } doc, err := s.loadURL(ctx, url) @@ -275,14 +268,15 @@ func (s *jsonScraper) scrapeGalleryByGallery(ctx context.Context, gallery *model return nil, err } - q := s.getJsonQuery(doc) + q := s.getJsonQuery(doc, url) return scraper.scrapeGallery(ctx, q) } -func (s *jsonScraper) getJsonQuery(doc string) *jsonQuery { +func (s *jsonScraper) getJsonQuery(doc string, url string) *jsonQuery { return &jsonQuery{ doc: doc, scraper: s, + url: url, } } @@ -290,6 +284,7 @@ type jsonQuery struct { doc string scraper *jsonScraper queryType QueryType + url string } func (q *jsonQuery) getType() QueryType { @@ -300,6 +295,10 @@ func (q *jsonQuery) setType(t QueryType) { q.queryType = t } +func (q *jsonQuery) getURL() string { + return q.url +} + func (q *jsonQuery) runQuery(selector string) ([]string, error) { value := gjson.Get(q.doc, selector) @@ -331,5 +330,5 @@ func (q *jsonQuery) subScrape(ctx context.Context, value string) mappedQuery { return nil } - return q.scraper.getJsonQuery(doc) + return q.scraper.getJsonQuery(doc, value) } diff --git a/pkg/scraper/json_test.go b/pkg/scraper/json_test.go index 249f17ad6..285c15489 100644 --- a/pkg/scraper/json_test.go +++ b/pkg/scraper/json_test.go @@ -68,7 +68,7 @@ jsonScrapers: } ` - c := &config{} + c := &Definition{} err := yaml.Unmarshal([]byte(yamlStr), &c) if err != nil { diff --git a/pkg/scraper/mapped.go b/pkg/scraper/mapped.go index 4b2559334..d92415c61 100644 --- a/pkg/scraper/mapped.go +++ b/pkg/scraper/mapped.go @@ -2,21 +2,9 @@ package scraper import ( "context" - "errors" - "fmt" - "math" - "reflect" - "regexp" - "strconv" - "strings" - "time" - "gopkg.in/yaml.v2" - - "github.com/stashapp/stash/pkg/javascript" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil" ) type mappedQuery interface { @@ -24,826 +12,10 @@ type mappedQuery interface { getType() QueryType setType(QueryType) subScrape(ctx context.Context, value string) mappedQuery + getURL() string } -type commonMappedConfig map[string]string - -type mappedConfig map[string]mappedScraperAttrConfig - -func (s mappedConfig) applyCommon(c commonMappedConfig, src string) string { - if c == nil { - return src - } - - ret := src - for commonKey, commonVal := range c { - ret = strings.ReplaceAll(ret, commonKey, commonVal) - } - - return ret -} - -type isMultiFunc func(key string) bool - -func (s mappedConfig) process(ctx context.Context, q mappedQuery, common commonMappedConfig, isMulti isMultiFunc) mappedResults { - var ret mappedResults - - for k, attrConfig := range s { - - if attrConfig.Fixed != "" { - // TODO - not sure if this needs to set _all_ indexes for the key - const i = 0 - ret = ret.setSingleValue(i, k, attrConfig.Fixed) - } else { - selector := attrConfig.Selector - selector = s.applyCommon(common, selector) - - found, err := q.runQuery(selector) - if err != nil { - logger.Warnf("key '%v': %v", k, err) - } - - if len(found) > 0 { - result := s.postProcess(ctx, q, attrConfig, found) - - // HACK - if the key is URLs, then we need to set the value as a multi-value - isMulti := isMulti != nil && isMulti(k) - if isMulti { - ret = ret.setMultiValue(0, k, result) - } else { - for i, text := range result { - ret = ret.setSingleValue(i, k, text) - } - } - } - } - } - - return ret -} - -func (s mappedConfig) postProcess(ctx context.Context, q mappedQuery, attrConfig mappedScraperAttrConfig, found []string) []string { - // check if we're concatenating the results into a single result - var ret []string - if attrConfig.hasConcat() { - result := attrConfig.concatenateResults(found) - result = attrConfig.postProcess(ctx, result, q) - if attrConfig.hasSplit() { - results := attrConfig.splitString(result) - // skip cleaning when the query is used for searching - if q.getType() == SearchQuery { - return results - } - results = attrConfig.cleanResults(results) - return results - } - - ret = []string{result} - } else { - for _, text := range found { - text = attrConfig.postProcess(ctx, text, q) - if attrConfig.hasSplit() { - return attrConfig.splitString(text) - } - - ret = append(ret, text) - } - // skip cleaning when the query is used for searching - if q.getType() == SearchQuery { - return ret - } - ret = attrConfig.cleanResults(ret) - - } - - return ret -} - -type mappedSceneScraperConfig struct { - mappedConfig - - Tags mappedConfig `yaml:"Tags"` - Performers mappedPerformerScraperConfig `yaml:"Performers"` - Studio mappedConfig `yaml:"Studio"` - Movies mappedConfig `yaml:"Movies"` -} -type _mappedSceneScraperConfig mappedSceneScraperConfig - -const ( - mappedScraperConfigSceneTags = "Tags" - mappedScraperConfigScenePerformers = "Performers" - mappedScraperConfigSceneStudio = "Studio" - mappedScraperConfigSceneMovies = "Movies" -) - -func (s *mappedSceneScraperConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { - // HACK - unmarshal to map first, then remove known scene sub-fields, then - // remarshal to yaml and pass that down to the base map - parentMap := make(map[string]interface{}) - if err := unmarshal(parentMap); err != nil { - return err - } - - // move the known sub-fields to a separate map - thisMap := make(map[string]interface{}) - - thisMap[mappedScraperConfigSceneTags] = parentMap[mappedScraperConfigSceneTags] - thisMap[mappedScraperConfigScenePerformers] = parentMap[mappedScraperConfigScenePerformers] - thisMap[mappedScraperConfigSceneStudio] = parentMap[mappedScraperConfigSceneStudio] - thisMap[mappedScraperConfigSceneMovies] = parentMap[mappedScraperConfigSceneMovies] - - delete(parentMap, mappedScraperConfigSceneTags) - delete(parentMap, mappedScraperConfigScenePerformers) - delete(parentMap, mappedScraperConfigSceneStudio) - delete(parentMap, mappedScraperConfigSceneMovies) - - // re-unmarshal the sub-fields - yml, err := yaml.Marshal(thisMap) - if err != nil { - return err - } - - // needs to be a different type to prevent infinite recursion - c := _mappedSceneScraperConfig{} - if err := yaml.Unmarshal(yml, &c); err != nil { - return err - } - - *s = mappedSceneScraperConfig(c) - - yml, err = yaml.Marshal(parentMap) - if err != nil { - return err - } - - if err := yaml.Unmarshal(yml, &s.mappedConfig); err != nil { - return err - } - - return nil -} - -type mappedGalleryScraperConfig struct { - mappedConfig - - Tags mappedConfig `yaml:"Tags"` - Performers mappedConfig `yaml:"Performers"` - Studio mappedConfig `yaml:"Studio"` -} - -type _mappedGalleryScraperConfig mappedGalleryScraperConfig - -func (s *mappedGalleryScraperConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { - // HACK - unmarshal to map first, then remove known scene sub-fields, then - // remarshal to yaml and pass that down to the base map - parentMap := make(map[string]interface{}) - if err := unmarshal(parentMap); err != nil { - return err - } - - // move the known sub-fields to a separate map - thisMap := make(map[string]interface{}) - - thisMap[mappedScraperConfigSceneTags] = parentMap[mappedScraperConfigSceneTags] - thisMap[mappedScraperConfigScenePerformers] = parentMap[mappedScraperConfigScenePerformers] - thisMap[mappedScraperConfigSceneStudio] = parentMap[mappedScraperConfigSceneStudio] - - delete(parentMap, mappedScraperConfigSceneTags) - delete(parentMap, mappedScraperConfigScenePerformers) - delete(parentMap, mappedScraperConfigSceneStudio) - - // re-unmarshal the sub-fields - yml, err := yaml.Marshal(thisMap) - if err != nil { - return err - } - - // needs to be a different type to prevent infinite recursion - c := _mappedGalleryScraperConfig{} - if err := yaml.Unmarshal(yml, &c); err != nil { - return err - } - - *s = mappedGalleryScraperConfig(c) - - yml, err = yaml.Marshal(parentMap) - if err != nil { - return err - } - - if err := yaml.Unmarshal(yml, &s.mappedConfig); err != nil { - return err - } - - return nil -} - -type mappedImageScraperConfig struct { - mappedConfig - - Tags mappedConfig `yaml:"Tags"` - Performers mappedConfig `yaml:"Performers"` - Studio mappedConfig `yaml:"Studio"` -} -type _mappedImageScraperConfig mappedImageScraperConfig - -func (s *mappedImageScraperConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { - // HACK - unmarshal to map first, then remove known scene sub-fields, then - // remarshal to yaml and pass that down to the base map - parentMap := make(map[string]interface{}) - if err := unmarshal(parentMap); err != nil { - return err - } - - // move the known sub-fields to a separate map - thisMap := make(map[string]interface{}) - - thisMap[mappedScraperConfigSceneTags] = parentMap[mappedScraperConfigSceneTags] - thisMap[mappedScraperConfigScenePerformers] = parentMap[mappedScraperConfigScenePerformers] - thisMap[mappedScraperConfigSceneStudio] = parentMap[mappedScraperConfigSceneStudio] - - delete(parentMap, mappedScraperConfigSceneTags) - delete(parentMap, mappedScraperConfigScenePerformers) - delete(parentMap, mappedScraperConfigSceneStudio) - - // re-unmarshal the sub-fields - yml, err := yaml.Marshal(thisMap) - if err != nil { - return err - } - - // needs to be a different type to prevent infinite recursion - c := _mappedImageScraperConfig{} - if err := yaml.Unmarshal(yml, &c); err != nil { - return err - } - - *s = mappedImageScraperConfig(c) - - yml, err = yaml.Marshal(parentMap) - if err != nil { - return err - } - - if err := yaml.Unmarshal(yml, &s.mappedConfig); err != nil { - return err - } - - return nil -} - -type mappedPerformerScraperConfig struct { - mappedConfig - - Tags mappedConfig `yaml:"Tags"` -} -type _mappedPerformerScraperConfig mappedPerformerScraperConfig - -const ( - mappedScraperConfigPerformerTags = "Tags" -) - -func (s *mappedPerformerScraperConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { - // HACK - unmarshal to map first, then remove known scene sub-fields, then - // remarshal to yaml and pass that down to the base map - parentMap := make(map[string]interface{}) - if err := unmarshal(parentMap); err != nil { - return err - } - - // move the known sub-fields to a separate map - thisMap := make(map[string]interface{}) - - thisMap[mappedScraperConfigPerformerTags] = parentMap[mappedScraperConfigPerformerTags] - - delete(parentMap, mappedScraperConfigPerformerTags) - - // re-unmarshal the sub-fields - yml, err := yaml.Marshal(thisMap) - if err != nil { - return err - } - - // needs to be a different type to prevent infinite recursion - c := _mappedPerformerScraperConfig{} - if err := yaml.Unmarshal(yml, &c); err != nil { - return err - } - - *s = mappedPerformerScraperConfig(c) - - yml, err = yaml.Marshal(parentMap) - if err != nil { - return err - } - - if err := yaml.Unmarshal(yml, &s.mappedConfig); err != nil { - return err - } - - return nil -} - -type mappedMovieScraperConfig struct { - mappedConfig - - Studio mappedConfig `yaml:"Studio"` - Tags mappedConfig `yaml:"Tags"` -} -type _mappedMovieScraperConfig mappedMovieScraperConfig - -const ( - mappedScraperConfigMovieStudio = "Studio" - mappedScraperConfigMovieTags = "Tags" -) - -func (s *mappedMovieScraperConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { - // HACK - unmarshal to map first, then remove known movie sub-fields, then - // remarshal to yaml and pass that down to the base map - parentMap := make(map[string]interface{}) - if err := unmarshal(parentMap); err != nil { - return err - } - - // move the known sub-fields to a separate map - thisMap := make(map[string]interface{}) - - thisMap[mappedScraperConfigMovieStudio] = parentMap[mappedScraperConfigMovieStudio] - delete(parentMap, mappedScraperConfigMovieStudio) - - thisMap[mappedScraperConfigMovieTags] = parentMap[mappedScraperConfigMovieTags] - delete(parentMap, mappedScraperConfigMovieTags) - - // re-unmarshal the sub-fields - yml, err := yaml.Marshal(thisMap) - if err != nil { - return err - } - - // needs to be a different type to prevent infinite recursion - c := _mappedMovieScraperConfig{} - if err := yaml.Unmarshal(yml, &c); err != nil { - return err - } - - *s = mappedMovieScraperConfig(c) - - yml, err = yaml.Marshal(parentMap) - if err != nil { - return err - } - - if err := yaml.Unmarshal(yml, &s.mappedConfig); err != nil { - return err - } - - return nil -} - -type mappedRegexConfig struct { - Regex string `yaml:"regex"` - With string `yaml:"with"` -} - -type mappedRegexConfigs []mappedRegexConfig - -func (c mappedRegexConfig) apply(value string) string { - if c.Regex != "" { - re, err := regexp.Compile(c.Regex) - if err != nil { - logger.Warnf("Error compiling regex '%s': %s", c.Regex, err.Error()) - return value - } - - ret := re.ReplaceAllString(value, c.With) - - // trim leading and trailing whitespace - // this is done to maintain backwards compatibility with existing - // scrapers - ret = strings.TrimSpace(ret) - - logger.Debugf(`Replace: '%s' with '%s'`, c.Regex, c.With) - logger.Debugf("Before: %s", value) - logger.Debugf("After: %s", ret) - return ret - } - - return value -} - -func (c mappedRegexConfigs) apply(value string) string { - // apply regex in order - for _, config := range c { - value = config.apply(value) - } - - return value -} - -type postProcessAction interface { - Apply(ctx context.Context, value string, q mappedQuery) string -} - -type postProcessParseDate string - -func (p *postProcessParseDate) Apply(ctx context.Context, value string, q mappedQuery) string { - parseDate := string(*p) - - const internalDateFormat = "2006-01-02" - - valueLower := strings.ToLower(value) - if valueLower == "today" || valueLower == "yesterday" { // handle today, yesterday - dt := time.Now() - if valueLower == "yesterday" { // subtract 1 day from now - dt = dt.AddDate(0, 0, -1) - } - return dt.Format(internalDateFormat) - } - - if parseDate == "" { - return value - } - - if parseDate == "unix" { - // try to parse the date using unix timestamp format - // if it fails, then just fall back to the original value - timeAsInt, err := strconv.ParseInt(value, 10, 64) - if err != nil { - logger.Warnf("Error parsing date string '%s' using unix timestamp format : %s", value, err.Error()) - return value - } - parsedValue := time.Unix(timeAsInt, 0) - - return parsedValue.Format(internalDateFormat) - } - - // try to parse the date using the pattern - // if it fails, then just fall back to the original value - parsedValue, err := time.Parse(parseDate, value) - if err != nil { - logger.Warnf("Error parsing date string '%s' using format '%s': %s", value, parseDate, err.Error()) - return value - } - - // convert it into our date format - return parsedValue.Format(internalDateFormat) -} - -type postProcessSubtractDays bool - -func (p *postProcessSubtractDays) Apply(ctx context.Context, value string, q mappedQuery) string { - const internalDateFormat = "2006-01-02" - - i, err := strconv.Atoi(value) - if err != nil { - logger.Warnf("Error parsing day string %s: %s", value, err) - return value - } - - dt := time.Now() - dt = dt.AddDate(0, 0, -i) - return dt.Format(internalDateFormat) -} - -type postProcessReplace mappedRegexConfigs - -func (c *postProcessReplace) Apply(ctx context.Context, value string, q mappedQuery) string { - replace := mappedRegexConfigs(*c) - return replace.apply(value) -} - -type postProcessSubScraper mappedScraperAttrConfig - -func (p *postProcessSubScraper) Apply(ctx context.Context, value string, q mappedQuery) string { - subScrapeConfig := mappedScraperAttrConfig(*p) - - logger.Debugf("Sub-scraping for: %s", value) - ss := q.subScrape(ctx, value) - - if ss != nil { - found, err := ss.runQuery(subScrapeConfig.Selector) - if err != nil { - logger.Warnf("subscrape for '%v': %v", value, err) - } - - if len(found) > 0 { - // check if we're concatenating the results into a single result - var result string - if subScrapeConfig.hasConcat() { - result = subScrapeConfig.concatenateResults(found) - } else { - result = found[0] - } - - result = subScrapeConfig.postProcess(ctx, result, ss) - return result - } - } - - return "" -} - -type postProcessMap map[string]string - -func (p *postProcessMap) Apply(ctx context.Context, value string, q mappedQuery) string { - // return the mapped value if present - m := *p - mapped, ok := m[value] - - if ok { - return mapped - } - - return value -} - -type postProcessFeetToCm bool - -func (p *postProcessFeetToCm) Apply(ctx context.Context, value string, q mappedQuery) string { - const foot_in_cm = 30.48 - const inch_in_cm = 2.54 - - reg := regexp.MustCompile("[0-9]+") - filtered := reg.FindAllString(value, -1) - - var feet float64 - var inches float64 - if len(filtered) > 0 { - feet, _ = strconv.ParseFloat(filtered[0], 64) - } - if len(filtered) > 1 { - inches, _ = strconv.ParseFloat(filtered[1], 64) - } - - var centimeters = feet*foot_in_cm + inches*inch_in_cm - - // Return rounded integer string - return strconv.Itoa(int(math.Round(centimeters))) -} - -type postProcessLbToKg bool - -func (p *postProcessLbToKg) Apply(ctx context.Context, value string, q mappedQuery) string { - const lb_in_kg = 0.45359237 - w, err := strconv.ParseFloat(value, 64) - if err == nil { - w *= lb_in_kg - value = strconv.Itoa(int(math.Round(w))) - } - return value -} - -type postProcessJavascript string - -func (p *postProcessJavascript) Apply(ctx context.Context, value string, q mappedQuery) string { - vm := javascript.NewVM() - if err := vm.Set("value", value); err != nil { - logger.Warnf("javascript failed to set value: %v", err) - return value - } - - log := &javascript.Log{ - Logger: logger.Logger, - Prefix: "", - ProgressChan: make(chan float64), - } - - if err := log.AddToVM("log", vm); err != nil { - logger.Logger.Errorf("error adding log API: %w", err) - } - - util := &javascript.Util{} - if err := util.AddToVM("util", vm); err != nil { - logger.Logger.Errorf("error adding util API: %w", err) - } - - script, err := javascript.CompileScript("", "(function() { "+string(*p)+"})()") - if err != nil { - logger.Warnf("javascript failed to compile: %v", err) - return value - } - - output, err := vm.RunProgram(script) - if err != nil { - logger.Warnf("javascript failed to run: %v", err) - return value - } - - // assume output is string - return output.String() -} - -type mappedPostProcessAction struct { - ParseDate string `yaml:"parseDate"` - SubtractDays bool `yaml:"subtractDays"` - Replace mappedRegexConfigs `yaml:"replace"` - SubScraper *mappedScraperAttrConfig `yaml:"subScraper"` - Map map[string]string `yaml:"map"` - FeetToCm bool `yaml:"feetToCm"` - LbToKg bool `yaml:"lbToKg"` - Javascript string `yaml:"javascript"` -} - -func (a mappedPostProcessAction) ToPostProcessAction() (postProcessAction, error) { - var found string - var ret postProcessAction - - ensureOnly := func(field string) error { - if found != "" { - return fmt.Errorf("post-process actions must have a single field, found %s and %s", found, field) - } - found = field - return nil - } - - if a.ParseDate != "" { - found = "parseDate" - action := postProcessParseDate(a.ParseDate) - ret = &action - } - if len(a.Replace) > 0 { - if err := ensureOnly("replace"); err != nil { - return nil, err - } - action := postProcessReplace(a.Replace) - ret = &action - } - if a.SubScraper != nil { - if err := ensureOnly("subScraper"); err != nil { - return nil, err - } - action := postProcessSubScraper(*a.SubScraper) - ret = &action - } - if a.Map != nil { - if err := ensureOnly("map"); err != nil { - return nil, err - } - action := postProcessMap(a.Map) - ret = &action - } - if a.FeetToCm { - if err := ensureOnly("feetToCm"); err != nil { - return nil, err - } - action := postProcessFeetToCm(a.FeetToCm) - ret = &action - } - if a.LbToKg { - if err := ensureOnly("lbToKg"); err != nil { - return nil, err - } - action := postProcessLbToKg(a.LbToKg) - ret = &action - } - if a.SubtractDays { - if err := ensureOnly("subtractDays"); err != nil { - return nil, err - } - action := postProcessSubtractDays(a.SubtractDays) - ret = &action - } - if a.Javascript != "" { - if err := ensureOnly("javascript"); err != nil { - return nil, err - } - action := postProcessJavascript(a.Javascript) - ret = &action - } - - if ret == nil { - return nil, errors.New("invalid post-process action") - } - - return ret, nil -} - -type mappedScraperAttrConfig struct { - Selector string `yaml:"selector"` - Fixed string `yaml:"fixed"` - PostProcess []mappedPostProcessAction `yaml:"postProcess"` - Concat string `yaml:"concat"` - Split string `yaml:"split"` - - postProcessActions []postProcessAction - - // Deprecated: use PostProcess instead - ParseDate string `yaml:"parseDate"` - Replace mappedRegexConfigs `yaml:"replace"` - SubScraper *mappedScraperAttrConfig `yaml:"subScraper"` -} - -type _mappedScraperAttrConfig mappedScraperAttrConfig - -func (c *mappedScraperAttrConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { - // try unmarshalling into a string first - if err := unmarshal(&c.Selector); err != nil { - // if it's a type error then we try to unmarshall to the full object - var typeErr *yaml.TypeError - if !errors.As(err, &typeErr) { - return err - } - - // unmarshall to full object - // need it as a separate object - t := _mappedScraperAttrConfig{} - if err = unmarshal(&t); err != nil { - return err - } - - *c = mappedScraperAttrConfig(t) - } - - return c.convertPostProcessActions() -} - -func (c *mappedScraperAttrConfig) convertPostProcessActions() error { - // ensure we don't have the old deprecated fields and the new post process field - if len(c.PostProcess) > 0 { - if c.ParseDate != "" || len(c.Replace) > 0 || c.SubScraper != nil { - return errors.New("cannot include postProcess and (parseDate, replace, subScraper) deprecated fields") - } - - // convert xpathPostProcessAction actions to postProcessActions - for _, a := range c.PostProcess { - action, err := a.ToPostProcessAction() - if err != nil { - return err - } - c.postProcessActions = append(c.postProcessActions, action) - } - - c.PostProcess = nil - } else { - // convert old deprecated fields if present - // in same order as they used to be executed - if len(c.Replace) > 0 { - action := postProcessReplace(c.Replace) - c.postProcessActions = append(c.postProcessActions, &action) - c.Replace = nil - } - - if c.SubScraper != nil { - action := postProcessSubScraper(*c.SubScraper) - c.postProcessActions = append(c.postProcessActions, &action) - c.SubScraper = nil - } - - if c.ParseDate != "" { - action := postProcessParseDate(c.ParseDate) - c.postProcessActions = append(c.postProcessActions, &action) - c.ParseDate = "" - } - } - - return nil -} - -func (c mappedScraperAttrConfig) hasConcat() bool { - return c.Concat != "" -} - -func (c mappedScraperAttrConfig) hasSplit() bool { - return c.Split != "" -} - -func (c mappedScraperAttrConfig) concatenateResults(nodes []string) string { - separator := c.Concat - return strings.Join(nodes, separator) -} - -func (c mappedScraperAttrConfig) cleanResults(nodes []string) []string { - cleaned := sliceutil.Unique(nodes) // remove duplicate values - cleaned = sliceutil.Delete(cleaned, "") // remove empty values - return cleaned -} - -func (c mappedScraperAttrConfig) splitString(value string) []string { - separator := c.Split - var res []string - - if separator == "" { - return []string{value} - } - - for _, str := range strings.Split(value, separator) { - if str != "" { - res = append(res, str) - } - } - - return res -} - -func (c mappedScraperAttrConfig) postProcess(ctx context.Context, value string, q mappedQuery) string { - for _, action := range c.postProcessActions { - value = action.Apply(ctx, value, q) - } - - return value -} - -type mappedScrapers map[string]*mappedScraper +type mappedScrapers map[string]mappedScraper type mappedScraper struct { Common commonMappedConfig `yaml:"common"` @@ -857,97 +29,12 @@ type mappedScraper struct { Movie *mappedMovieScraperConfig `yaml:"movie"` } -type mappedResult map[string]interface{} -type mappedResults []mappedResult - -func (r mappedResult) apply(dest interface{}) { - destVal := reflect.ValueOf(dest).Elem() - - // all fields are either string pointers or string slices - for key, value := range r { - if err := mapFieldValue(destVal, key, value); err != nil { - logger.Errorf("Error mapping field %s in %T: %v", key, dest, err) - } - } -} - -func mapFieldValue(destVal reflect.Value, key string, value interface{}) error { - field := destVal.FieldByName(key) - fieldType := field.Type() - - if field.IsValid() && field.CanSet() { - switch v := value.(type) { - case string: - // if the field is a pointer to a string, then we need to convert the string to a pointer - // if the field is a string slice, then we need to convert the string to a slice - switch { - case fieldType.Kind() == reflect.String: - field.SetString(v) - case fieldType.Kind() == reflect.Ptr && fieldType.Elem().Kind() == reflect.String: - ptr := reflect.New(fieldType.Elem()) - ptr.Elem().SetString(v) - field.Set(ptr) - case fieldType.Kind() == reflect.Slice && fieldType.Elem().Kind() == reflect.String: - field.Set(reflect.ValueOf([]string{v})) - default: - return fmt.Errorf("cannot convert %T to %s", value, fieldType) - } - case []string: - // expect the field to be a string slice - if fieldType.Kind() == reflect.Slice && fieldType.Elem().Kind() == reflect.String { - field.Set(reflect.ValueOf(v)) - } else { - return fmt.Errorf("cannot convert %T to %s", value, fieldType) - } - default: - // fallback to reflection - reflectValue := reflect.ValueOf(value) - reflectValueType := reflectValue.Type() - - switch { - case reflectValueType.ConvertibleTo(fieldType): - field.Set(reflectValue.Convert(fieldType)) - case fieldType.Kind() == reflect.Pointer && reflectValueType.ConvertibleTo(fieldType.Elem()): - ptr := reflect.New(fieldType.Elem()) - ptr.Elem().Set(reflectValue.Convert(fieldType.Elem())) - field.Set(ptr) - default: - return fmt.Errorf("cannot convert %T to %s", value, fieldType) - } - } - } else { - return fmt.Errorf("field does not exist or cannot be set") - } - - return nil -} - -func (r mappedResults) setSingleValue(index int, key string, value string) mappedResults { - if index >= len(r) { - r = append(r, make(mappedResult)) - } - - logger.Debugf(`[%d][%s] = %s`, index, key, value) - r[index][key] = value - return r -} - -func (r mappedResults) setMultiValue(index int, key string, value []string) mappedResults { - if index >= len(r) { - r = append(r, make(mappedResult)) - } - - logger.Debugf(`[%d][%s] = %s`, index, key, value) - r[index][key] = value - return r -} - func urlsIsMulti(key string) bool { return key == "URLs" } func (s mappedScraper) scrapePerformer(ctx context.Context, q mappedQuery) (*models.ScrapedPerformer, error) { - var ret models.ScrapedPerformer + var ret *models.ScrapedPerformer performerMap := s.Performer if performerMap == nil { @@ -959,31 +46,26 @@ func (s mappedScraper) scrapePerformer(ctx context.Context, q mappedQuery) (*mod results := performerMap.process(ctx, q, s.Common, urlsIsMulti) // now apply the tags + var tagResults mappedResults + if performerTagsMap != nil { logger.Debug(`Processing performer tags:`) - tagResults := performerTagsMap.process(ctx, q, s.Common, nil) - - for _, p := range tagResults { - tag := &models.ScrapedTag{} - p.apply(tag) - ret.Tags = append(ret.Tags, tag) - } + tagResults = performerTagsMap.process(ctx, q, s.Common, nil) } - if len(results) == 0 && len(ret.Tags) == 0 { + if len(results) == 0 { return nil, nil } if len(results) > 0 { - results[0].apply(&ret) + ret = results[0].scrapedPerformer() + ret.Tags = tagResults.scrapedTags() } - return &ret, nil + return ret, nil } func (s mappedScraper) scrapePerformers(ctx context.Context, q mappedQuery) ([]*models.ScrapedPerformer, error) { - var ret []*models.ScrapedPerformer - performerMap := s.Performer if performerMap == nil { return nil, nil @@ -991,13 +73,7 @@ func (s mappedScraper) scrapePerformers(ctx context.Context, q mappedQuery) ([]* // isMulti is nil because it will behave incorrect when scraping multiple performers results := performerMap.process(ctx, q, s.Common, nil) - for _, r := range results { - var p models.ScrapedPerformer - r.apply(&p) - ret = append(ret, &p) - } - - return ret, nil + return results.scrapedPerformers(), nil } // processSceneRelationships sets the relationships on the models.ScrapedScene. It returns true if any relationships were set. @@ -1008,13 +84,14 @@ func (s mappedScraper) processSceneRelationships(ctx context.Context, q mappedQu sceneTagsMap := sceneScraperConfig.Tags sceneStudioMap := sceneScraperConfig.Studio sceneMoviesMap := sceneScraperConfig.Movies + sceneGroupsMap := sceneScraperConfig.Groups ret.Performers = s.processPerformers(ctx, scenePerformersMap, q) if sceneTagsMap != nil { logger.Debug(`Processing scene tags:`) - ret.Tags = processRelationships[models.ScrapedTag](ctx, s, sceneTagsMap, q) + ret.Tags = sceneTagsMap.process(ctx, q, s.Common, nil).scrapedTags() } if sceneStudioMap != nil { @@ -1022,19 +99,23 @@ func (s mappedScraper) processSceneRelationships(ctx context.Context, q mappedQu studioResults := sceneStudioMap.process(ctx, q, s.Common, nil) if len(studioResults) > 0 && resultIndex < len(studioResults) { - studio := &models.ScrapedStudio{} // when doing a `search` scrape get the related studio - studioResults[resultIndex].apply(studio) + studio := studioResults[resultIndex].scrapedStudio() ret.Studio = studio } } if sceneMoviesMap != nil { logger.Debug(`Processing scene movies:`) - ret.Movies = processRelationships[models.ScrapedMovie](ctx, s, sceneMoviesMap, q) + ret.Movies = sceneMoviesMap.process(ctx, q, s.Common, nil).scrapedMovies() } - return len(ret.Performers) > 0 || len(ret.Tags) > 0 || ret.Studio != nil || len(ret.Movies) > 0 + if sceneGroupsMap != nil { + logger.Debug(`Processing scene groups:`) + ret.Groups = sceneGroupsMap.process(ctx, q, s.Common, nil).scrapedGroups() + } + + return len(ret.Performers) > 0 || len(ret.Tags) > 0 || ret.Studio != nil || len(ret.Movies) > 0 || len(ret.Groups) > 0 } func (s mappedScraper) processPerformers(ctx context.Context, performersMap mappedPerformerScraperConfig, q mappedQuery) []*models.ScrapedPerformer { @@ -1055,12 +136,10 @@ func (s mappedScraper) processPerformers(ctx context.Context, performersMap mapp } for _, p := range performerResults { - performer := &models.ScrapedPerformer{} - p.apply(performer) + performer := p.scrapedPerformer() for _, p := range performerTagResults { - tag := &models.ScrapedTag{} - p.apply(tag) + tag := p.scrapedTag() performer.Tags = append(performer.Tags, tag) } @@ -1071,20 +150,6 @@ func (s mappedScraper) processPerformers(ctx context.Context, performersMap mapp return ret } -func processRelationships[T any](ctx context.Context, s mappedScraper, relationshipMap mappedConfig, q mappedQuery) []*T { - var ret []*T - - results := relationshipMap.process(ctx, q, s.Common, nil) - - for _, p := range results { - var value T - p.apply(&value) - ret = append(ret, &value) - } - - return ret -} - func (s mappedScraper) scrapeScenes(ctx context.Context, q mappedQuery) ([]*models.ScrapedScene, error) { var ret []*models.ScrapedScene @@ -1100,10 +165,9 @@ func (s mappedScraper) scrapeScenes(ctx context.Context, q mappedQuery) ([]*mode for i, r := range results { logger.Debug(`Processing scene:`) - var thisScene models.ScrapedScene - r.apply(&thisScene) - s.processSceneRelationships(ctx, q, i, &thisScene) - ret = append(ret, &thisScene) + thisScene := r.scrapedScene() + s.processSceneRelationships(ctx, q, i, thisScene) + ret = append(ret, thisScene) } return ret, nil @@ -1120,17 +184,17 @@ func (s mappedScraper) scrapeScene(ctx context.Context, q mappedQuery) (*models. logger.Debug(`Processing scene:`) results := sceneMap.process(ctx, q, s.Common, urlsIsMulti) - var ret models.ScrapedScene + var ret *models.ScrapedScene if len(results) > 0 { - results[0].apply(&ret) + ret = results[0].scrapedScene() } - hasRelationships := s.processSceneRelationships(ctx, q, 0, &ret) + hasRelationships := s.processSceneRelationships(ctx, q, 0, ret) // #3953 - process only returns results if the non-relationship fields are // populated // only return if we have results or relationships if len(results) > 0 || hasRelationships { - return &ret, nil + return ret, nil } return nil, nil @@ -1153,15 +217,19 @@ func (s mappedScraper) scrapeImage(ctx context.Context, q mappedQuery) (*models. logger.Debug(`Processing image:`) results := imageMap.process(ctx, q, s.Common, urlsIsMulti) + if len(results) > 0 { + ret = *results[0].scrapedImage() + } + // now apply the performers and tags if imagePerformersMap != nil { logger.Debug(`Processing image performers:`) - ret.Performers = processRelationships[models.ScrapedPerformer](ctx, s, imagePerformersMap, q) + ret.Performers = imagePerformersMap.process(ctx, q, s.Common, nil).scrapedPerformers() } if imageTagsMap != nil { logger.Debug(`Processing image tags:`) - ret.Tags = processRelationships[models.ScrapedTag](ctx, s, imageTagsMap, q) + ret.Tags = imageTagsMap.process(ctx, q, s.Common, nil).scrapedTags() } if imageStudioMap != nil { @@ -1169,9 +237,7 @@ func (s mappedScraper) scrapeImage(ctx context.Context, q mappedQuery) (*models. studioResults := imageStudioMap.process(ctx, q, s.Common, nil) if len(studioResults) > 0 { - studio := &models.ScrapedStudio{} - studioResults[0].apply(studio) - ret.Studio = studio + ret.Studio = studioResults[0].scrapedStudio() } } @@ -1180,10 +246,6 @@ func (s mappedScraper) scrapeImage(ctx context.Context, q mappedQuery) (*models. return nil, nil } - if len(results) > 0 { - results[0].apply(&ret) - } - return &ret, nil } @@ -1204,27 +266,22 @@ func (s mappedScraper) scrapeGallery(ctx context.Context, q mappedQuery) (*model logger.Debug(`Processing gallery:`) results := galleryMap.process(ctx, q, s.Common, urlsIsMulti) + if len(results) > 0 { + ret = *results[0].scrapedGallery() + } + // now apply the performers and tags if galleryPerformersMap != nil { logger.Debug(`Processing gallery performers:`) performerResults := galleryPerformersMap.process(ctx, q, s.Common, urlsIsMulti) - for _, p := range performerResults { - performer := &models.ScrapedPerformer{} - p.apply(performer) - ret.Performers = append(ret.Performers, performer) - } + ret.Performers = performerResults.scrapedPerformers() } if galleryTagsMap != nil { logger.Debug(`Processing gallery tags:`) tagResults := galleryTagsMap.process(ctx, q, s.Common, nil) - - for _, p := range tagResults { - tag := &models.ScrapedTag{} - p.apply(tag) - ret.Tags = append(ret.Tags, tag) - } + ret.Tags = tagResults.scrapedTags() } if galleryStudioMap != nil { @@ -1232,9 +289,7 @@ func (s mappedScraper) scrapeGallery(ctx context.Context, q mappedQuery) (*model studioResults := galleryStudioMap.process(ctx, q, s.Common, nil) if len(studioResults) > 0 { - studio := &models.ScrapedStudio{} - studioResults[0].apply(studio) - ret.Studio = studio + ret.Studio = studioResults[0].scrapedStudio() } } @@ -1243,10 +298,6 @@ func (s mappedScraper) scrapeGallery(ctx context.Context, q mappedQuery) (*model return nil, nil } - if len(results) > 0 { - results[0].apply(&ret) - } - return &ret, nil } @@ -1270,14 +321,16 @@ func (s mappedScraper) scrapeGroup(ctx context.Context, q mappedQuery) (*models. results := groupMap.process(ctx, q, s.Common, urlsIsMulti) + if len(results) > 0 { + ret = *results[0].scrapedGroup() + } + if groupStudioMap != nil { logger.Debug(`Processing group studio:`) studioResults := groupStudioMap.process(ctx, q, s.Common, nil) if len(studioResults) > 0 { - studio := &models.ScrapedStudio{} - studioResults[0].apply(studio) - ret.Studio = studio + ret.Studio = studioResults[0].scrapedStudio() } } @@ -1286,20 +339,12 @@ func (s mappedScraper) scrapeGroup(ctx context.Context, q mappedQuery) (*models. logger.Debug(`Processing group tags:`) tagResults := groupTagsMap.process(ctx, q, s.Common, nil) - for _, p := range tagResults { - tag := &models.ScrapedTag{} - p.apply(tag) - ret.Tags = append(ret.Tags, tag) - } + ret.Tags = tagResults.scrapedTags() } if len(results) == 0 && ret.Studio == nil && len(ret.Tags) == 0 { return nil, nil } - if len(results) > 0 { - results[0].apply(&ret) - } - return &ret, nil } diff --git a/pkg/scraper/mapped_config.go b/pkg/scraper/mapped_config.go new file mode 100644 index 000000000..920bf74b4 --- /dev/null +++ b/pkg/scraper/mapped_config.go @@ -0,0 +1,537 @@ +package scraper + +import ( + "context" + "errors" + "net/url" + "strings" + + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/sliceutil" + "gopkg.in/yaml.v2" +) + +type commonMappedConfig map[string]string + +type mappedConfig map[string]mappedScraperAttrConfig + +func (s mappedConfig) applyCommon(c commonMappedConfig, src string) string { + if c == nil { + return src + } + + ret := src + for commonKey, commonVal := range c { + ret = strings.ReplaceAll(ret, commonKey, commonVal) + } + + return ret +} + +// extractHostname parses a URL string and returns the hostname. +// Returns empty string if the URL cannot be parsed. +func extractHostname(urlStr string) string { + if urlStr == "" { + return "" + } + + u, err := url.Parse(urlStr) + if err != nil { + logger.Warnf("Error parsing URL '%s': %s", urlStr, err.Error()) + return "" + } + + return u.Hostname() +} + +type isMultiFunc func(key string) bool + +func (s mappedConfig) process(ctx context.Context, q mappedQuery, common commonMappedConfig, isMulti isMultiFunc) mappedResults { + var ret mappedResults + + for k, attrConfig := range s { + + if attrConfig.Fixed != "" { + // TODO - not sure if this needs to set _all_ indexes for the key + const i = 0 + // Support {inputURL} and {inputHostname} placeholders in fixed values + value := strings.ReplaceAll(attrConfig.Fixed, "{inputURL}", q.getURL()) + value = strings.ReplaceAll(value, "{inputHostname}", extractHostname(q.getURL())) + ret = ret.setSingleValue(i, k, value) + } else { + selector := attrConfig.Selector + selector = s.applyCommon(common, selector) + // Support {inputURL} and {inputHostname} placeholders in selectors + selector = strings.ReplaceAll(selector, "{inputURL}", q.getURL()) + selector = strings.ReplaceAll(selector, "{inputHostname}", extractHostname(q.getURL())) + + found, err := q.runQuery(selector) + if err != nil { + logger.Warnf("key '%v': %v", k, err) + } + + if len(found) > 0 { + result := s.postProcess(ctx, q, attrConfig, found) + + // HACK - if the key is URLs, then we need to set the value as a multi-value + isMulti := isMulti != nil && isMulti(k) + if isMulti { + ret = ret.setMultiValue(0, k, result) + } else { + for i, text := range result { + ret = ret.setSingleValue(i, k, text) + } + } + } + } + } + + return ret +} + +func (s mappedConfig) postProcess(ctx context.Context, q mappedQuery, attrConfig mappedScraperAttrConfig, found []string) []string { + // check if we're concatenating the results into a single result + var ret []string + if attrConfig.hasConcat() { + result := attrConfig.concatenateResults(found) + result = attrConfig.postProcess(ctx, result, q) + if attrConfig.hasSplit() { + results := attrConfig.splitString(result) + // skip cleaning when the query is used for searching + if q.getType() == SearchQuery { + return results + } + results = attrConfig.cleanResults(results) + return results + } + + ret = []string{result} + } else { + for _, text := range found { + text = attrConfig.postProcess(ctx, text, q) + if attrConfig.hasSplit() { + return attrConfig.splitString(text) + } + + ret = append(ret, text) + } + // skip cleaning when the query is used for searching + if q.getType() == SearchQuery { + return ret + } + ret = attrConfig.cleanResults(ret) + + } + + return ret +} + +type mappedSceneScraperConfig struct { + mappedConfig + + Tags mappedConfig `yaml:"Tags"` + Performers mappedPerformerScraperConfig `yaml:"Performers"` + Studio mappedConfig `yaml:"Studio"` + Movies mappedConfig `yaml:"Movies"` + Groups mappedConfig `yaml:"Groups"` +} +type _mappedSceneScraperConfig mappedSceneScraperConfig + +const ( + mappedScraperConfigSceneTags = "Tags" + mappedScraperConfigScenePerformers = "Performers" + mappedScraperConfigSceneStudio = "Studio" + mappedScraperConfigSceneMovies = "Movies" + mappedScraperConfigSceneGroups = "Groups" +) + +func (s *mappedSceneScraperConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + // HACK - unmarshal to map first, then remove known scene sub-fields, then + // remarshal to yaml and pass that down to the base map + parentMap := make(map[string]interface{}) + if err := unmarshal(parentMap); err != nil { + return err + } + + // move the known sub-fields to a separate map + thisMap := make(map[string]interface{}) + + thisMap[mappedScraperConfigSceneTags] = parentMap[mappedScraperConfigSceneTags] + thisMap[mappedScraperConfigScenePerformers] = parentMap[mappedScraperConfigScenePerformers] + thisMap[mappedScraperConfigSceneStudio] = parentMap[mappedScraperConfigSceneStudio] + thisMap[mappedScraperConfigSceneMovies] = parentMap[mappedScraperConfigSceneMovies] + thisMap[mappedScraperConfigSceneGroups] = parentMap[mappedScraperConfigSceneGroups] + + delete(parentMap, mappedScraperConfigSceneTags) + delete(parentMap, mappedScraperConfigScenePerformers) + delete(parentMap, mappedScraperConfigSceneStudio) + delete(parentMap, mappedScraperConfigSceneMovies) + delete(parentMap, mappedScraperConfigSceneGroups) + + // re-unmarshal the sub-fields + yml, err := yaml.Marshal(thisMap) + if err != nil { + return err + } + + // needs to be a different type to prevent infinite recursion + c := _mappedSceneScraperConfig{} + if err := yaml.Unmarshal(yml, &c); err != nil { + return err + } + + *s = mappedSceneScraperConfig(c) + + yml, err = yaml.Marshal(parentMap) + if err != nil { + return err + } + + if err := yaml.Unmarshal(yml, &s.mappedConfig); err != nil { + return err + } + + return nil +} + +type mappedGalleryScraperConfig struct { + mappedConfig + + Tags mappedConfig `yaml:"Tags"` + Performers mappedConfig `yaml:"Performers"` + Studio mappedConfig `yaml:"Studio"` +} + +type _mappedGalleryScraperConfig mappedGalleryScraperConfig + +func (s *mappedGalleryScraperConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + // HACK - unmarshal to map first, then remove known scene sub-fields, then + // remarshal to yaml and pass that down to the base map + parentMap := make(map[string]interface{}) + if err := unmarshal(parentMap); err != nil { + return err + } + + // move the known sub-fields to a separate map + thisMap := make(map[string]interface{}) + + thisMap[mappedScraperConfigSceneTags] = parentMap[mappedScraperConfigSceneTags] + thisMap[mappedScraperConfigScenePerformers] = parentMap[mappedScraperConfigScenePerformers] + thisMap[mappedScraperConfigSceneStudio] = parentMap[mappedScraperConfigSceneStudio] + + delete(parentMap, mappedScraperConfigSceneTags) + delete(parentMap, mappedScraperConfigScenePerformers) + delete(parentMap, mappedScraperConfigSceneStudio) + + // re-unmarshal the sub-fields + yml, err := yaml.Marshal(thisMap) + if err != nil { + return err + } + + // needs to be a different type to prevent infinite recursion + c := _mappedGalleryScraperConfig{} + if err := yaml.Unmarshal(yml, &c); err != nil { + return err + } + + *s = mappedGalleryScraperConfig(c) + + yml, err = yaml.Marshal(parentMap) + if err != nil { + return err + } + + if err := yaml.Unmarshal(yml, &s.mappedConfig); err != nil { + return err + } + + return nil +} + +type mappedImageScraperConfig struct { + mappedConfig + + Tags mappedConfig `yaml:"Tags"` + Performers mappedConfig `yaml:"Performers"` + Studio mappedConfig `yaml:"Studio"` +} +type _mappedImageScraperConfig mappedImageScraperConfig + +func (s *mappedImageScraperConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + // HACK - unmarshal to map first, then remove known scene sub-fields, then + // remarshal to yaml and pass that down to the base map + parentMap := make(map[string]interface{}) + if err := unmarshal(parentMap); err != nil { + return err + } + + // move the known sub-fields to a separate map + thisMap := make(map[string]interface{}) + + thisMap[mappedScraperConfigSceneTags] = parentMap[mappedScraperConfigSceneTags] + thisMap[mappedScraperConfigScenePerformers] = parentMap[mappedScraperConfigScenePerformers] + thisMap[mappedScraperConfigSceneStudio] = parentMap[mappedScraperConfigSceneStudio] + + delete(parentMap, mappedScraperConfigSceneTags) + delete(parentMap, mappedScraperConfigScenePerformers) + delete(parentMap, mappedScraperConfigSceneStudio) + + // re-unmarshal the sub-fields + yml, err := yaml.Marshal(thisMap) + if err != nil { + return err + } + + // needs to be a different type to prevent infinite recursion + c := _mappedImageScraperConfig{} + if err := yaml.Unmarshal(yml, &c); err != nil { + return err + } + + *s = mappedImageScraperConfig(c) + + yml, err = yaml.Marshal(parentMap) + if err != nil { + return err + } + + if err := yaml.Unmarshal(yml, &s.mappedConfig); err != nil { + return err + } + + return nil +} + +type mappedPerformerScraperConfig struct { + mappedConfig + + Tags mappedConfig `yaml:"Tags"` +} +type _mappedPerformerScraperConfig mappedPerformerScraperConfig + +const ( + mappedScraperConfigPerformerTags = "Tags" +) + +func (s *mappedPerformerScraperConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + // HACK - unmarshal to map first, then remove known scene sub-fields, then + // remarshal to yaml and pass that down to the base map + parentMap := make(map[string]interface{}) + if err := unmarshal(parentMap); err != nil { + return err + } + + // move the known sub-fields to a separate map + thisMap := make(map[string]interface{}) + + thisMap[mappedScraperConfigPerformerTags] = parentMap[mappedScraperConfigPerformerTags] + + delete(parentMap, mappedScraperConfigPerformerTags) + + // re-unmarshal the sub-fields + yml, err := yaml.Marshal(thisMap) + if err != nil { + return err + } + + // needs to be a different type to prevent infinite recursion + c := _mappedPerformerScraperConfig{} + if err := yaml.Unmarshal(yml, &c); err != nil { + return err + } + + *s = mappedPerformerScraperConfig(c) + + yml, err = yaml.Marshal(parentMap) + if err != nil { + return err + } + + if err := yaml.Unmarshal(yml, &s.mappedConfig); err != nil { + return err + } + + return nil +} + +type mappedMovieScraperConfig struct { + mappedConfig + + Studio mappedConfig `yaml:"Studio"` + Tags mappedConfig `yaml:"Tags"` +} +type _mappedMovieScraperConfig mappedMovieScraperConfig + +const ( + mappedScraperConfigMovieStudio = "Studio" + mappedScraperConfigMovieTags = "Tags" +) + +func (s *mappedMovieScraperConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + // HACK - unmarshal to map first, then remove known movie sub-fields, then + // remarshal to yaml and pass that down to the base map + parentMap := make(map[string]interface{}) + if err := unmarshal(parentMap); err != nil { + return err + } + + // move the known sub-fields to a separate map + thisMap := make(map[string]interface{}) + + thisMap[mappedScraperConfigMovieStudio] = parentMap[mappedScraperConfigMovieStudio] + delete(parentMap, mappedScraperConfigMovieStudio) + + thisMap[mappedScraperConfigMovieTags] = parentMap[mappedScraperConfigMovieTags] + delete(parentMap, mappedScraperConfigMovieTags) + + // re-unmarshal the sub-fields + yml, err := yaml.Marshal(thisMap) + if err != nil { + return err + } + + // needs to be a different type to prevent infinite recursion + c := _mappedMovieScraperConfig{} + if err := yaml.Unmarshal(yml, &c); err != nil { + return err + } + + *s = mappedMovieScraperConfig(c) + + yml, err = yaml.Marshal(parentMap) + if err != nil { + return err + } + + if err := yaml.Unmarshal(yml, &s.mappedConfig); err != nil { + return err + } + + return nil +} + +type mappedScraperAttrConfig struct { + Selector string `yaml:"selector"` + Fixed string `yaml:"fixed"` + PostProcess []mappedPostProcessAction `yaml:"postProcess"` + Concat string `yaml:"concat"` + Split string `yaml:"split"` + + postProcessActions []postProcessAction + + // Deprecated: use PostProcess instead + ParseDate string `yaml:"parseDate"` + Replace mappedRegexConfigs `yaml:"replace"` + SubScraper *mappedScraperAttrConfig `yaml:"subScraper"` +} + +type _mappedScraperAttrConfig mappedScraperAttrConfig + +func (c *mappedScraperAttrConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + // try unmarshalling into a string first + if err := unmarshal(&c.Selector); err != nil { + // if it's a type error then we try to unmarshall to the full object + var typeErr *yaml.TypeError + if !errors.As(err, &typeErr) { + return err + } + + // unmarshall to full object + // need it as a separate object + t := _mappedScraperAttrConfig{} + if err = unmarshal(&t); err != nil { + return err + } + + *c = mappedScraperAttrConfig(t) + } + + return c.convertPostProcessActions() +} + +func (c *mappedScraperAttrConfig) convertPostProcessActions() error { + // ensure we don't have the old deprecated fields and the new post process field + if len(c.PostProcess) > 0 { + if c.ParseDate != "" || len(c.Replace) > 0 || c.SubScraper != nil { + return errors.New("cannot include postProcess and (parseDate, replace, subScraper) deprecated fields") + } + + // convert xpathPostProcessAction actions to postProcessActions + for _, a := range c.PostProcess { + action, err := a.ToPostProcessAction() + if err != nil { + return err + } + c.postProcessActions = append(c.postProcessActions, action) + } + + c.PostProcess = nil + } else { + // convert old deprecated fields if present + // in same order as they used to be executed + if len(c.Replace) > 0 { + action := postProcessReplace(c.Replace) + c.postProcessActions = append(c.postProcessActions, &action) + c.Replace = nil + } + + if c.SubScraper != nil { + action := postProcessSubScraper(*c.SubScraper) + c.postProcessActions = append(c.postProcessActions, &action) + c.SubScraper = nil + } + + if c.ParseDate != "" { + action := postProcessParseDate(c.ParseDate) + c.postProcessActions = append(c.postProcessActions, &action) + c.ParseDate = "" + } + } + + return nil +} + +func (c mappedScraperAttrConfig) hasConcat() bool { + return c.Concat != "" +} + +func (c mappedScraperAttrConfig) hasSplit() bool { + return c.Split != "" +} + +func (c mappedScraperAttrConfig) concatenateResults(nodes []string) string { + separator := c.Concat + return strings.Join(nodes, separator) +} + +func (c mappedScraperAttrConfig) cleanResults(nodes []string) []string { + cleaned := sliceutil.Unique(nodes) // remove duplicate values + cleaned = sliceutil.Delete(cleaned, "") // remove empty values + return cleaned +} + +func (c mappedScraperAttrConfig) splitString(value string) []string { + separator := c.Split + var res []string + + if separator == "" { + return []string{value} + } + + for _, str := range strings.Split(value, separator) { + if str != "" { + res = append(res, str) + } + } + + return res +} + +func (c mappedScraperAttrConfig) postProcess(ctx context.Context, value string, q mappedQuery) string { + for _, action := range c.postProcessActions { + value = action.Apply(ctx, value, q) + } + + return value +} diff --git a/pkg/scraper/mapped_postprocessing.go b/pkg/scraper/mapped_postprocessing.go new file mode 100644 index 000000000..22a8b748a --- /dev/null +++ b/pkg/scraper/mapped_postprocessing.go @@ -0,0 +1,333 @@ +package scraper + +import ( + "context" + "errors" + "fmt" + "math" + "regexp" + "strconv" + "strings" + "time" + + "github.com/stashapp/stash/pkg/javascript" + "github.com/stashapp/stash/pkg/logger" +) + +type mappedRegexConfig struct { + Regex string `yaml:"regex"` + With string `yaml:"with"` +} + +type mappedRegexConfigs []mappedRegexConfig + +func (c mappedRegexConfig) apply(value string) string { + if c.Regex != "" { + re, err := regexp.Compile(c.Regex) + if err != nil { + logger.Warnf("Error compiling regex '%s': %s", c.Regex, err.Error()) + return value + } + + ret := re.ReplaceAllString(value, c.With) + + // trim leading and trailing whitespace + // this is done to maintain backwards compatibility with existing + // scrapers + ret = strings.TrimSpace(ret) + + logger.Debugf(`Replace: '%s' with '%s'`, c.Regex, c.With) + logger.Debugf("Before: %s", value) + logger.Debugf("After: %s", ret) + return ret + } + + return value +} + +func (c mappedRegexConfigs) apply(value string) string { + // apply regex in order + for _, config := range c { + value = config.apply(value) + } + + return value +} + +type postProcessAction interface { + Apply(ctx context.Context, value string, q mappedQuery) string +} + +type postProcessParseDate string + +func (p *postProcessParseDate) Apply(ctx context.Context, value string, q mappedQuery) string { + parseDate := string(*p) + + const internalDateFormat = "2006-01-02" + + valueLower := strings.ToLower(value) + if valueLower == "today" || valueLower == "yesterday" { // handle today, yesterday + dt := time.Now() + if valueLower == "yesterday" { // subtract 1 day from now + dt = dt.AddDate(0, 0, -1) + } + return dt.Format(internalDateFormat) + } + + if parseDate == "" { + return value + } + + if parseDate == "unix" { + // try to parse the date using unix timestamp format + // if it fails, then just fall back to the original value + timeAsInt, err := strconv.ParseInt(value, 10, 64) + if err != nil { + logger.Warnf("Error parsing date string '%s' using unix timestamp format : %s", value, err.Error()) + return value + } + parsedValue := time.Unix(timeAsInt, 0) + + return parsedValue.Format(internalDateFormat) + } + + // try to parse the date using the pattern + // if it fails, then just fall back to the original value + parsedValue, err := time.Parse(parseDate, value) + if err != nil { + logger.Warnf("Error parsing date string '%s' using format '%s': %s", value, parseDate, err.Error()) + return value + } + + // convert it into our date format + return parsedValue.Format(internalDateFormat) +} + +type postProcessSubtractDays bool + +func (p *postProcessSubtractDays) Apply(ctx context.Context, value string, q mappedQuery) string { + const internalDateFormat = "2006-01-02" + + i, err := strconv.Atoi(value) + if err != nil { + logger.Warnf("Error parsing day string %s: %s", value, err) + return value + } + + dt := time.Now() + dt = dt.AddDate(0, 0, -i) + return dt.Format(internalDateFormat) +} + +type postProcessReplace mappedRegexConfigs + +func (c *postProcessReplace) Apply(ctx context.Context, value string, q mappedQuery) string { + replace := mappedRegexConfigs(*c) + return replace.apply(value) +} + +type postProcessSubScraper mappedScraperAttrConfig + +func (p *postProcessSubScraper) Apply(ctx context.Context, value string, q mappedQuery) string { + subScrapeConfig := mappedScraperAttrConfig(*p) + + logger.Debugf("Sub-scraping for: %s", value) + ss := q.subScrape(ctx, value) + + if ss != nil { + found, err := ss.runQuery(subScrapeConfig.Selector) + if err != nil { + logger.Warnf("subscrape for '%v': %v", value, err) + } + + if len(found) > 0 { + // check if we're concatenating the results into a single result + var result string + if subScrapeConfig.hasConcat() { + result = subScrapeConfig.concatenateResults(found) + } else { + result = found[0] + } + + result = subScrapeConfig.postProcess(ctx, result, ss) + return result + } + } + + return "" +} + +type postProcessMap map[string]string + +func (p *postProcessMap) Apply(ctx context.Context, value string, q mappedQuery) string { + // return the mapped value if present + m := *p + mapped, ok := m[value] + + if ok { + return mapped + } + + return value +} + +type postProcessFeetToCm bool + +func (p *postProcessFeetToCm) Apply(ctx context.Context, value string, q mappedQuery) string { + const foot_in_cm = 30.48 + const inch_in_cm = 2.54 + + reg := regexp.MustCompile("[0-9]+") + filtered := reg.FindAllString(value, -1) + + var feet float64 + var inches float64 + if len(filtered) > 0 { + feet, _ = strconv.ParseFloat(filtered[0], 64) + } + if len(filtered) > 1 { + inches, _ = strconv.ParseFloat(filtered[1], 64) + } + + var centimeters = feet*foot_in_cm + inches*inch_in_cm + + // Return rounded integer string + return strconv.Itoa(int(math.Round(centimeters))) +} + +type postProcessLbToKg bool + +func (p *postProcessLbToKg) Apply(ctx context.Context, value string, q mappedQuery) string { + const lb_in_kg = 0.45359237 + w, err := strconv.ParseFloat(value, 64) + if err == nil { + w *= lb_in_kg + value = strconv.Itoa(int(math.Round(w))) + } + return value +} + +type postProcessJavascript string + +func (p *postProcessJavascript) Apply(ctx context.Context, value string, q mappedQuery) string { + vm := javascript.NewVM() + if err := vm.Set("value", value); err != nil { + logger.Warnf("javascript failed to set value: %v", err) + return value + } + + log := &javascript.Log{ + Logger: logger.Logger, + Prefix: "", + ProgressChan: make(chan float64), + } + + if err := log.AddToVM("log", vm); err != nil { + logger.Logger.Errorf("error adding log API: %w", err) + } + + util := &javascript.Util{} + if err := util.AddToVM("util", vm); err != nil { + logger.Logger.Errorf("error adding util API: %w", err) + } + + script, err := javascript.CompileScript("", "(function() { "+string(*p)+"})()") + if err != nil { + logger.Warnf("javascript failed to compile: %v", err) + return value + } + + output, err := vm.RunProgram(script) + if err != nil { + logger.Warnf("javascript failed to run: %v", err) + return value + } + + // assume output is string + return output.String() +} + +type mappedPostProcessAction struct { + ParseDate string `yaml:"parseDate"` + SubtractDays bool `yaml:"subtractDays"` + Replace mappedRegexConfigs `yaml:"replace"` + SubScraper *mappedScraperAttrConfig `yaml:"subScraper"` + Map map[string]string `yaml:"map"` + FeetToCm bool `yaml:"feetToCm"` + LbToKg bool `yaml:"lbToKg"` + Javascript string `yaml:"javascript"` +} + +func (a mappedPostProcessAction) ToPostProcessAction() (postProcessAction, error) { + var found string + var ret postProcessAction + + ensureOnly := func(field string) error { + if found != "" { + return fmt.Errorf("post-process actions must have a single field, found %s and %s", found, field) + } + found = field + return nil + } + + if a.ParseDate != "" { + found = "parseDate" + action := postProcessParseDate(a.ParseDate) + ret = &action + } + if len(a.Replace) > 0 { + if err := ensureOnly("replace"); err != nil { + return nil, err + } + action := postProcessReplace(a.Replace) + ret = &action + } + if a.SubScraper != nil { + if err := ensureOnly("subScraper"); err != nil { + return nil, err + } + action := postProcessSubScraper(*a.SubScraper) + ret = &action + } + if a.Map != nil { + if err := ensureOnly("map"); err != nil { + return nil, err + } + action := postProcessMap(a.Map) + ret = &action + } + if a.FeetToCm { + if err := ensureOnly("feetToCm"); err != nil { + return nil, err + } + action := postProcessFeetToCm(a.FeetToCm) + ret = &action + } + if a.LbToKg { + if err := ensureOnly("lbToKg"); err != nil { + return nil, err + } + action := postProcessLbToKg(a.LbToKg) + ret = &action + } + if a.SubtractDays { + if err := ensureOnly("subtractDays"); err != nil { + return nil, err + } + action := postProcessSubtractDays(a.SubtractDays) + ret = &action + } + if a.Javascript != "" { + if err := ensureOnly("javascript"); err != nil { + return nil, err + } + action := postProcessJavascript(a.Javascript) + ret = &action + } + + if ret == nil { + return nil, errors.New("invalid post-process action") + } + + return ret, nil +} diff --git a/pkg/scraper/mapped_result.go b/pkg/scraper/mapped_result.go new file mode 100644 index 000000000..eb06a4eba --- /dev/null +++ b/pkg/scraper/mapped_result.go @@ -0,0 +1,276 @@ +package scraper + +import ( + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/models" +) + +type mappedResult map[string]interface{} +type mappedResults []mappedResult + +func (r mappedResult) string(key string) (string, bool) { + v, ok := r[key] + if !ok { + return "", false + } + + val, ok := v.(string) + if !ok { + logger.Errorf("String field %s is %T in mappedResult", key, r[key]) + } + + return val, true +} + +func (r mappedResult) mustString(key string) string { + v, ok := r[key] + if !ok { + logger.Errorf("Missing required string field %s in mappedResult", key) + return "" + } + + val, ok := v.(string) + if !ok { + logger.Errorf("String field %s is %T in mappedResult", key, r[key]) + } + + return val +} + +func (r mappedResult) stringPtr(key string) *string { + val, ok := r.string(key) + if !ok { + return nil + } + return &val +} + +func (r mappedResult) stringSlice(key string) []string { + v, ok := r[key] + if !ok { + return nil + } + + // need to try both []string and string + val, ok := v.([]string) + + if ok { + return val + } + + // try single string + singleVal, ok := v.(string) + if !ok { + logger.Errorf("String slice field %s is %T in mappedResult", key, r[key]) + return nil + } + + return []string{singleVal} +} + +func (r mappedResult) IntPtr(key string) *int { + v, ok := r[key] + if !ok { + return nil + } + + val, ok := v.(int) + if !ok { + logger.Errorf("Int field %s is %T in mappedResult", key, r[key]) + return nil + } + + return &val +} + +func (r mappedResults) setSingleValue(index int, key string, value string) mappedResults { + if index >= len(r) { + r = append(r, make(mappedResult)) + } + + logger.Debugf(`[%d][%s] = %s`, index, key, value) + r[index][key] = value + return r +} + +func (r mappedResults) setMultiValue(index int, key string, value []string) mappedResults { + if index >= len(r) { + r = append(r, make(mappedResult)) + } + + logger.Debugf(`[%d][%s] = %s`, index, key, value) + r[index][key] = value + return r +} + +func (r mappedResults) scrapedTags() []*models.ScrapedTag { + if len(r) == 0 { + return nil + } + + ret := make([]*models.ScrapedTag, len(r)) + for i, result := range r { + ret[i] = result.scrapedTag() + } + + return ret +} + +func (r mappedResult) scrapedTag() *models.ScrapedTag { + return &models.ScrapedTag{ + Name: r.mustString("Name"), + } +} + +func (r mappedResult) scrapedPerformer() *models.ScrapedPerformer { + ret := &models.ScrapedPerformer{ + Name: r.stringPtr("Name"), + Disambiguation: r.stringPtr("Disambiguation"), + Gender: r.stringPtr("Gender"), + URL: r.stringPtr("URL"), + URLs: r.stringSlice("URLs"), + Twitter: r.stringPtr("Twitter"), + Birthdate: r.stringPtr("Birthdate"), + Ethnicity: r.stringPtr("Ethnicity"), + Country: r.stringPtr("Country"), + EyeColor: r.stringPtr("EyeColor"), + Height: r.stringPtr("Height"), + Measurements: r.stringPtr("Measurements"), + FakeTits: r.stringPtr("FakeTits"), + PenisLength: r.stringPtr("PenisLength"), + Circumcised: r.stringPtr("Circumcised"), + CareerLength: r.stringPtr("CareerLength"), + Tattoos: r.stringPtr("Tattoos"), + Piercings: r.stringPtr("Piercings"), + Aliases: r.stringPtr("Aliases"), + Image: r.stringPtr("Image"), + Images: r.stringSlice("Images"), + Details: r.stringPtr("Details"), + DeathDate: r.stringPtr("DeathDate"), + HairColor: r.stringPtr("HairColor"), + Weight: r.stringPtr("Weight"), + } + return ret +} + +func (r mappedResults) scrapedPerformers() []*models.ScrapedPerformer { + if len(r) == 0 { + return nil + } + + ret := make([]*models.ScrapedPerformer, len(r)) + for i, result := range r { + ret[i] = result.scrapedPerformer() + } + + return ret +} + +func (r mappedResult) scrapedScene() *models.ScrapedScene { + ret := &models.ScrapedScene{ + Title: r.stringPtr("Title"), + Code: r.stringPtr("Code"), + Details: r.stringPtr("Details"), + Director: r.stringPtr("Director"), + URL: r.stringPtr("URL"), + URLs: r.stringSlice("URLs"), + Date: r.stringPtr("Date"), + Image: r.stringPtr("Image"), + Duration: r.IntPtr("Duration"), + } + return ret +} + +func (r mappedResult) scrapedImage() *models.ScrapedImage { + ret := &models.ScrapedImage{ + Title: r.stringPtr("Title"), + Code: r.stringPtr("Code"), + Details: r.stringPtr("Details"), + Photographer: r.stringPtr("Photographer"), + URLs: r.stringSlice("URLs"), + Date: r.stringPtr("Date"), + } + return ret +} + +func (r mappedResult) scrapedGallery() *models.ScrapedGallery { + ret := &models.ScrapedGallery{ + Title: r.stringPtr("Title"), + Code: r.stringPtr("Code"), + Details: r.stringPtr("Details"), + Photographer: r.stringPtr("Photographer"), + URL: r.stringPtr("URL"), + URLs: r.stringSlice("URLs"), + Date: r.stringPtr("Date"), + } + return ret +} + +func (r mappedResult) scrapedStudio() *models.ScrapedStudio { + ret := &models.ScrapedStudio{ + Name: r.mustString("Name"), + URL: r.stringPtr("URL"), + URLs: r.stringSlice("URLs"), + Image: r.stringPtr("Image"), + Details: r.stringPtr("Details"), + Aliases: r.stringPtr("Aliases"), + } + return ret +} + +func (r mappedResult) scrapedMovie() *models.ScrapedMovie { + ret := &models.ScrapedMovie{ + Name: r.stringPtr("Name"), + Aliases: r.stringPtr("Aliases"), + URLs: r.stringSlice("URLs"), + Duration: r.stringPtr("Duration"), + Date: r.stringPtr("Date"), + Director: r.stringPtr("Director"), + Synopsis: r.stringPtr("Synopsis"), + FrontImage: r.stringPtr("FrontImage"), + BackImage: r.stringPtr("BackImage"), + } + + return ret +} + +func (r mappedResult) scrapedGroup() *models.ScrapedGroup { + ret := &models.ScrapedGroup{ + Name: r.stringPtr("Name"), + Aliases: r.stringPtr("Aliases"), + URL: r.stringPtr("URL"), + URLs: r.stringSlice("URLs"), + Duration: r.stringPtr("Duration"), + Date: r.stringPtr("Date"), + Director: r.stringPtr("Director"), + Synopsis: r.stringPtr("Synopsis"), + FrontImage: r.stringPtr("FrontImage"), + BackImage: r.stringPtr("BackImage"), + } + + return ret +} + +func (r mappedResults) scrapedMovies() []*models.ScrapedMovie { + if len(r) == 0 { + return nil + } + ret := make([]*models.ScrapedMovie, len(r)) + for i, result := range r { + ret[i] = result.scrapedMovie() + } + + return ret +} + +func (r mappedResults) scrapedGroups() []*models.ScrapedGroup { + if len(r) == 0 { + return nil + } + ret := make([]*models.ScrapedGroup, len(r)) + for i, result := range r { + ret[i] = result.scrapedGroup() + } + + return ret +} diff --git a/pkg/scraper/mapped_result_test.go b/pkg/scraper/mapped_result_test.go new file mode 100644 index 000000000..db6d921bf --- /dev/null +++ b/pkg/scraper/mapped_result_test.go @@ -0,0 +1,908 @@ +package scraper + +import ( + "testing" + + "github.com/stashapp/stash/pkg/models" + "github.com/stretchr/testify/assert" +) + +// Test string method +func TestMappedResultString(t *testing.T) { + tests := []struct { + name string + data mappedResult + key string + expectedValue string + expectedOk bool + }{ + { + name: "valid string", + data: mappedResult{"name": "test"}, + key: "name", + expectedValue: "test", + expectedOk: true, + }, + { + name: "missing key", + data: mappedResult{}, + key: "missing", + expectedValue: "", + expectedOk: false, + }, + { + name: "wrong type still returns ok true but empty value", + data: mappedResult{"num": 123}, + key: "num", + expectedValue: "", + expectedOk: true, // logs error but returns ok=true + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + val, ok := test.data.string(test.key) + assert.Equal(t, test.expectedValue, val) + assert.Equal(t, test.expectedOk, ok) + }) + } +} + +// Test mustString method +func TestMappedResultMustString(t *testing.T) { + tests := []struct { + name string + data mappedResult + key string + expectedValue string + }{ + { + name: "valid string", + data: mappedResult{"name": "test"}, + key: "name", + expectedValue: "test", + }, + { + name: "missing key returns empty string", + data: mappedResult{}, + key: "missing", + expectedValue: "", + }, + { + name: "wrong type returns empty string", + data: mappedResult{"num": 123}, + key: "num", + expectedValue: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + val := test.data.mustString(test.key) + assert.Equal(t, test.expectedValue, val) + }) + } +} + +// Test stringPtr method +func TestMappedResultStringPtr(t *testing.T) { + tests := []struct { + name string + data mappedResult + key string + expectedValue *string + }{ + { + name: "valid string", + data: mappedResult{"name": "test"}, + key: "name", + expectedValue: strPtr("test"), + }, + { + name: "missing key returns nil", + data: mappedResult{}, + key: "missing", + expectedValue: nil, + }, + { + name: "wrong type returns non-nil pointer to empty string", + data: mappedResult{"num": 123}, + key: "num", + expectedValue: strPtr(""), // string() returns empty string but ok=true + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + val := test.data.stringPtr(test.key) + if test.expectedValue == nil { + assert.Nil(t, val) + } else { + assert.NotNil(t, val) + assert.Equal(t, *test.expectedValue, *val) + } + }) + } +} + +// Test stringSlice method +func TestMappedResultStringSlice(t *testing.T) { + tests := []struct { + name string + data mappedResult + key string + expectedValue []string + }{ + { + name: "valid slice", + data: mappedResult{"tags": []string{"a", "b", "c"}}, + key: "tags", + expectedValue: []string{"a", "b", "c"}, + }, + { + name: "missing key returns nil", + data: mappedResult{}, + key: "missing", + expectedValue: nil, + }, + { + name: "single value converted to slice", + data: mappedResult{"tags": "not a slice"}, + key: "tags", + expectedValue: []string{"not a slice"}, + }, + { + name: "wrong type returns nil", + data: mappedResult{"tags": 123}, + key: "tags", + expectedValue: nil, + }, + { + name: "empty slice", + data: mappedResult{"tags": []string{}}, + key: "tags", + expectedValue: []string{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + val := test.data.stringSlice(test.key) + assert.Equal(t, test.expectedValue, val) + }) + } +} + +// Test IntPtr method +func TestMappedResultIntPtr(t *testing.T) { + tests := []struct { + name string + data mappedResult + key string + expectedValue *int + }{ + { + name: "valid int", + data: mappedResult{"duration": 120}, + key: "duration", + expectedValue: intPtr(120), + }, + { + name: "missing key returns nil", + data: mappedResult{}, + key: "missing", + expectedValue: nil, + }, + { + name: "wrong type returns nil", + data: mappedResult{"duration": "120"}, + key: "duration", + expectedValue: nil, + }, + { + name: "zero value", + data: mappedResult{"duration": 0}, + key: "duration", + expectedValue: intPtr(0), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + val := test.data.IntPtr(test.key) + assert.Equal(t, test.expectedValue, val) + }) + } +} + +// Test setSingleValue method +func TestMappedResultsSetSingleValue(t *testing.T) { + tests := []struct { + name string + initialResults mappedResults + index int + key string + value string + expectedLen int + shouldPanic bool + }{ + { + name: "append to empty", + initialResults: mappedResults{}, + index: 0, + key: "name", + value: "test", + expectedLen: 1, + shouldPanic: false, + }, + { + name: "set in existing", + initialResults: mappedResults{mappedResult{}}, + index: 0, + key: "name", + value: "test", + expectedLen: 1, + shouldPanic: false, + }, + { + name: "append to existing", + initialResults: mappedResults{mappedResult{}}, + index: 1, + key: "name", + value: "test", + expectedLen: 2, + shouldPanic: false, + }, + { + name: "sparse index causes panic", + initialResults: mappedResults{mappedResult{}}, + index: 5, + key: "name", + value: "test", + expectedLen: 6, + shouldPanic: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.shouldPanic { + assert.Panics(t, func() { + test.initialResults.setSingleValue(test.index, test.key, test.value) + }) + } else { + results := test.initialResults.setSingleValue(test.index, test.key, test.value) + assert.Equal(t, test.expectedLen, len(results)) + assert.Equal(t, test.value, results[test.index][test.key]) + } + }) + } +} + +// Test setMultiValue method +func TestMappedResultsSetMultiValue(t *testing.T) { + tests := []struct { + name string + initialResults mappedResults + index int + key string + value []string + expectedLen int + }{ + { + name: "append to empty", + initialResults: mappedResults{}, + index: 0, + key: "tags", + value: []string{"a", "b"}, + expectedLen: 1, + }, + { + name: "set in existing", + initialResults: mappedResults{mappedResult{}}, + index: 0, + key: "tags", + value: []string{"a", "b"}, + expectedLen: 1, + }, + { + name: "append to existing", + initialResults: mappedResults{mappedResult{}}, + index: 1, + key: "tags", + value: []string{"x", "y"}, + expectedLen: 2, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + results := test.initialResults.setMultiValue(test.index, test.key, test.value) + assert.Equal(t, test.expectedLen, len(results)) + assert.Equal(t, test.value, results[test.index][test.key]) + }) + } +} + +// Test scrapedTag method +func TestMappedResultScrapedTag(t *testing.T) { + tests := []struct { + name string + data mappedResult + expectedName string + }{ + { + name: "valid tag", + data: mappedResult{"Name": "Action"}, + expectedName: "Action", + }, + { + name: "missing name", + data: mappedResult{}, + expectedName: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tag := test.data.scrapedTag() + assert.NotNil(t, tag) + assert.Equal(t, test.expectedName, tag.Name) + }) + } +} + +// Test scrapedTags method +func TestMappedResultsScrapedTags(t *testing.T) { + tests := []struct { + name string + data mappedResults + expectedCount int + expectedNames []string + }{ + { + name: "empty results", + data: mappedResults{}, + expectedCount: 0, + }, + { + name: "single tag", + data: mappedResults{ + mappedResult{"Name": "Action"}, + }, + expectedCount: 1, + expectedNames: []string{"Action"}, + }, + { + name: "multiple tags", + data: mappedResults{ + mappedResult{"Name": "Action"}, + mappedResult{"Name": "Drama"}, + mappedResult{"Name": "Comedy"}, + }, + expectedCount: 3, + expectedNames: []string{"Action", "Drama", "Comedy"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tags := test.data.scrapedTags() + if test.expectedCount == 0 { + assert.Nil(t, tags) + } else { + assert.NotNil(t, tags) + assert.Equal(t, test.expectedCount, len(tags)) + for i, expectedName := range test.expectedNames { + assert.Equal(t, expectedName, tags[i].Name) + } + } + }) + } +} + +// Test scrapedPerformer method +func TestMappedResultScrapedPerformer(t *testing.T) { + tests := []struct { + name string + data mappedResult + validate func(t *testing.T, p *models.ScrapedPerformer) + }{ + { + name: "full performer", + data: mappedResult{ + "Name": "Jane Doe", + "Disambiguation": "Actress", + "Gender": "Female", + "URL": "https://example.com/jane", + "URLs": []string{"url1", "url2"}, + "Twitter": "@jane", + "Birthdate": "1990-01-01", + "Ethnicity": "Caucasian", + "Country": "USA", + "EyeColor": "Blue", + "Height": "5'6\"", + "Measurements": "36-24-36", + "FakeTits": "No", + "PenisLength": "N/A", + "Circumcised": "N/A", + "CareerLength": "10 years", + "Tattoos": "Yes", + "Piercings": "Yes", + "Aliases": "Jane Smith", + "Image": "image.jpg", + "Images": []string{"img1", "img2"}, + "Details": "Some details", + "DeathDate": "N/A", + "HairColor": "Blonde", + "Weight": "130 lbs", + }, + validate: func(t *testing.T, p *models.ScrapedPerformer) { + assert.NotNil(t, p) + assert.Equal(t, "Jane Doe", *p.Name) + assert.Equal(t, "Actress", *p.Disambiguation) + assert.Equal(t, "Female", *p.Gender) + assert.Equal(t, "https://example.com/jane", *p.URL) + assert.Equal(t, []string{"url1", "url2"}, p.URLs) + assert.Equal(t, "@jane", *p.Twitter) + assert.Equal(t, "Blonde", *p.HairColor) + assert.Equal(t, "130 lbs", *p.Weight) + }, + }, + { + name: "minimal performer", + data: mappedResult{}, + validate: func(t *testing.T, p *models.ScrapedPerformer) { + assert.NotNil(t, p) + assert.Nil(t, p.Name) + assert.Nil(t, p.Gender) + assert.Empty(t, p.URLs) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + performer := test.data.scrapedPerformer() + test.validate(t, performer) + }) + } +} + +// Test scrapedPerformers method +func TestMappedResultsScrapedPerformers(t *testing.T) { + tests := []struct { + name string + data mappedResults + expectedCount int + }{ + { + name: "empty results", + data: mappedResults{}, + expectedCount: 0, + }, + { + name: "single performer", + data: mappedResults{ + mappedResult{"Name": "Jane Doe"}, + }, + expectedCount: 1, + }, + { + name: "multiple performers", + data: mappedResults{ + mappedResult{"Name": "Jane Doe"}, + mappedResult{"Name": "John Doe"}, + mappedResult{"Name": "Alice"}, + }, + expectedCount: 3, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + performers := test.data.scrapedPerformers() + if test.expectedCount == 0 { + assert.Nil(t, performers) + } else { + assert.NotNil(t, performers) + assert.Equal(t, test.expectedCount, len(performers)) + } + }) + } +} + +// Test scrapedScene method +func TestMappedResultScrapedScene(t *testing.T) { + tests := []struct { + name string + data mappedResult + validate func(t *testing.T, s *models.ScrapedScene) + }{ + { + name: "full scene", + data: mappedResult{ + "Title": "Scene Title", + "Code": "CODE123", + "Details": "Scene details", + "Director": "John Smith", + "URL": "https://example.com/scene", + "URLs": []string{"url1", "url2"}, + "Date": "2020-01-01", + "Image": "scene.jpg", + "Duration": 3600, + }, + validate: func(t *testing.T, s *models.ScrapedScene) { + assert.NotNil(t, s) + assert.Equal(t, "Scene Title", *s.Title) + assert.Equal(t, "CODE123", *s.Code) + assert.Equal(t, "Scene details", *s.Details) + assert.Equal(t, "John Smith", *s.Director) + assert.Equal(t, "https://example.com/scene", *s.URL) + assert.Equal(t, []string{"url1", "url2"}, s.URLs) + assert.Equal(t, "2020-01-01", *s.Date) + assert.Equal(t, "scene.jpg", *s.Image) + assert.Equal(t, 3600, *s.Duration) + }, + }, + { + name: "minimal scene", + data: mappedResult{}, + validate: func(t *testing.T, s *models.ScrapedScene) { + assert.NotNil(t, s) + assert.Nil(t, s.Title) + assert.Nil(t, s.Duration) + assert.Empty(t, s.URLs) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + scene := test.data.scrapedScene() + test.validate(t, scene) + }) + } +} + +// Test scrapedImage method +func TestMappedResultScrapedImage(t *testing.T) { + tests := []struct { + name string + data mappedResult + validate func(t *testing.T, i *models.ScrapedImage) + }{ + { + name: "full image", + data: mappedResult{ + "Title": "Image Title", + "Code": "IMG123", + "Details": "Image details", + "Photographer": "Jane Photographer", + "URLs": []string{"url1", "url2"}, + "Date": "2020-06-15", + }, + validate: func(t *testing.T, i *models.ScrapedImage) { + assert.NotNil(t, i) + assert.Equal(t, "Image Title", *i.Title) + assert.Equal(t, "IMG123", *i.Code) + assert.Equal(t, "Image details", *i.Details) + assert.Equal(t, "Jane Photographer", *i.Photographer) + assert.Equal(t, []string{"url1", "url2"}, i.URLs) + assert.Equal(t, "2020-06-15", *i.Date) + }, + }, + { + name: "minimal image", + data: mappedResult{}, + validate: func(t *testing.T, i *models.ScrapedImage) { + assert.NotNil(t, i) + assert.Nil(t, i.Title) + assert.Empty(t, i.URLs) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + image := test.data.scrapedImage() + test.validate(t, image) + }) + } +} + +// Test scrapedGallery method +func TestMappedResultScrapedGallery(t *testing.T) { + tests := []struct { + name string + data mappedResult + validate func(t *testing.T, g *models.ScrapedGallery) + }{ + { + name: "full gallery", + data: mappedResult{ + "Title": "Gallery Title", + "Code": "GAL123", + "Details": "Gallery details", + "Photographer": "Jane Photographer", + "URL": "https://example.com/gallery", + "URLs": []string{"url1", "url2"}, + "Date": "2020-07-20", + }, + validate: func(t *testing.T, g *models.ScrapedGallery) { + assert.NotNil(t, g) + assert.Equal(t, "Gallery Title", *g.Title) + assert.Equal(t, "GAL123", *g.Code) + assert.Equal(t, "Gallery details", *g.Details) + assert.Equal(t, "Jane Photographer", *g.Photographer) + assert.Equal(t, "https://example.com/gallery", *g.URL) + assert.Equal(t, []string{"url1", "url2"}, g.URLs) + assert.Equal(t, "2020-07-20", *g.Date) + }, + }, + { + name: "minimal gallery", + data: mappedResult{}, + validate: func(t *testing.T, g *models.ScrapedGallery) { + assert.NotNil(t, g) + assert.Nil(t, g.Title) + assert.Empty(t, g.URLs) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gallery := test.data.scrapedGallery() + test.validate(t, gallery) + }) + } +} + +// Test scrapedStudio method +func TestMappedResultScrapedStudio(t *testing.T) { + tests := []struct { + name string + data mappedResult + validate func(t *testing.T, st *models.ScrapedStudio) + }{ + { + name: "full studio", + data: mappedResult{ + "Name": "Studio Name", + "URL": "https://example.com/studio", + "URLs": []string{"url1", "url2"}, + "Image": "studio.jpg", + "Details": "Studio details", + "Aliases": "Studio Alias", + }, + validate: func(t *testing.T, st *models.ScrapedStudio) { + assert.NotNil(t, st) + assert.Equal(t, "Studio Name", st.Name) + assert.Equal(t, "https://example.com/studio", *st.URL) + assert.Equal(t, []string{"url1", "url2"}, st.URLs) + assert.Equal(t, "studio.jpg", *st.Image) + assert.Equal(t, "Studio details", *st.Details) + assert.Equal(t, "Studio Alias", *st.Aliases) + }, + }, + { + name: "minimal studio", + data: mappedResult{}, + validate: func(t *testing.T, st *models.ScrapedStudio) { + assert.NotNil(t, st) + assert.Equal(t, "", st.Name) // mustString returns empty string + assert.Nil(t, st.URL) + assert.Empty(t, st.URLs) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + studio := test.data.scrapedStudio() + test.validate(t, studio) + }) + } +} + +// Test scrapedMovie method +func TestMappedResultScrapedMovie(t *testing.T) { + tests := []struct { + name string + data mappedResult + validate func(t *testing.T, m *models.ScrapedMovie) + }{ + { + name: "full movie", + data: mappedResult{ + "Name": "Movie Title", + "Aliases": "Movie Alias", + "URLs": []string{"url1", "url2"}, + "Duration": "120 minutes", + "Date": "2020-05-10", + "Director": "John Director", + "Synopsis": "Movie synopsis", + "FrontImage": "front.jpg", + "BackImage": "back.jpg", + }, + validate: func(t *testing.T, m *models.ScrapedMovie) { + assert.NotNil(t, m) + assert.Equal(t, "Movie Title", *m.Name) + assert.Equal(t, "Movie Alias", *m.Aliases) + assert.Equal(t, []string{"url1", "url2"}, m.URLs) + assert.Equal(t, "120 minutes", *m.Duration) + assert.Equal(t, "2020-05-10", *m.Date) + assert.Equal(t, "John Director", *m.Director) + assert.Equal(t, "Movie synopsis", *m.Synopsis) + assert.Equal(t, "front.jpg", *m.FrontImage) + assert.Equal(t, "back.jpg", *m.BackImage) + }, + }, + { + name: "minimal movie", + data: mappedResult{}, + validate: func(t *testing.T, m *models.ScrapedMovie) { + assert.NotNil(t, m) + assert.Nil(t, m.Name) + assert.Empty(t, m.URLs) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + movie := test.data.scrapedMovie() + test.validate(t, movie) + }) + } +} + +// Test scrapedMovies method +func TestMappedResultsScrapedMovies(t *testing.T) { + tests := []struct { + name string + data mappedResults + expectedCount int + }{ + { + name: "empty results", + data: mappedResults{}, + expectedCount: 0, + }, + { + name: "single movie", + data: mappedResults{ + mappedResult{"Name": "Movie 1"}, + }, + expectedCount: 1, + }, + { + name: "multiple movies", + data: mappedResults{ + mappedResult{"Name": "Movie 1"}, + mappedResult{"Name": "Movie 2"}, + mappedResult{"Name": "Movie 3"}, + }, + expectedCount: 3, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + movies := test.data.scrapedMovies() + if test.expectedCount == 0 { + assert.Nil(t, movies) + } else { + assert.NotNil(t, movies) + assert.Equal(t, test.expectedCount, len(movies)) + } + }) + } +} + +// Test scrapedGroup method +func TestMappedResultScrapedGroup(t *testing.T) { + tests := []struct { + name string + data mappedResult + validate func(t *testing.T, g *models.ScrapedGroup) + }{ + { + name: "full group", + data: mappedResult{ + "Name": "Group Title", + "Aliases": "Group Alias", + "URL": "https://example.com/group", + "URLs": []string{"url1", "url2"}, + "Duration": "240 minutes", + "Date": "2020-08-15", + "Director": "Jane Director", + "Synopsis": "Group synopsis", + "FrontImage": "front.jpg", + "BackImage": "back.jpg", + }, + validate: func(t *testing.T, g *models.ScrapedGroup) { + assert.NotNil(t, g) + assert.Equal(t, "Group Title", *g.Name) + assert.Equal(t, "Group Alias", *g.Aliases) + assert.Equal(t, "https://example.com/group", *g.URL) + assert.Equal(t, []string{"url1", "url2"}, g.URLs) + assert.Equal(t, "240 minutes", *g.Duration) + assert.Equal(t, "2020-08-15", *g.Date) + assert.Equal(t, "Jane Director", *g.Director) + assert.Equal(t, "Group synopsis", *g.Synopsis) + assert.Equal(t, "front.jpg", *g.FrontImage) + assert.Equal(t, "back.jpg", *g.BackImage) + }, + }, + { + name: "minimal group", + data: mappedResult{}, + validate: func(t *testing.T, g *models.ScrapedGroup) { + assert.NotNil(t, g) + assert.Nil(t, g.Name) + assert.Empty(t, g.URLs) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + group := test.data.scrapedGroup() + test.validate(t, group) + }) + } +} + +// Test scrapedGroups method +func TestMappedResultsScrapedGroups(t *testing.T) { + tests := []struct { + name string + data mappedResults + expectedCount int + }{ + { + name: "empty results", + data: mappedResults{}, + expectedCount: 0, + }, + { + name: "single group", + data: mappedResults{ + mappedResult{"Name": "Group 1"}, + }, + expectedCount: 1, + }, + { + name: "multiple groups", + data: mappedResults{ + mappedResult{"Name": "Group 1"}, + mappedResult{"Name": "Group 2"}, + mappedResult{"Name": "Group 3"}, + }, + expectedCount: 3, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + groups := test.data.scrapedGroups() + if test.expectedCount == 0 { + assert.Nil(t, groups) + } else { + assert.NotNil(t, groups) + assert.Equal(t, test.expectedCount, len(groups)) + } + }) + } +} + +// Helper functions +func strPtr(s string) *string { + return &s +} + +func intPtr(i int) *int { + return &i +} diff --git a/pkg/scraper/mapped_test.go b/pkg/scraper/mapped_test.go index 5f44e17af..667bb8385 100644 --- a/pkg/scraper/mapped_test.go +++ b/pkg/scraper/mapped_test.go @@ -25,7 +25,7 @@ xPathScrapers: - anything ` - c := &config{} + c := &Definition{} err := yaml.Unmarshal([]byte(yamlStr), &c) if err == nil { diff --git a/pkg/scraper/postprocessing.go b/pkg/scraper/postprocessing.go index e12c1664f..c2653743a 100644 --- a/pkg/scraper/postprocessing.go +++ b/pkg/scraper/postprocessing.go @@ -11,85 +11,91 @@ import ( "github.com/stashapp/stash/pkg/utils" ) +type postScraper struct { + Cache + excludeTagRE []*regexp.Regexp + + // ignoredTags is a list of tags that were ignored during post-processing + ignoredTags []string +} + // postScrape handles post-processing of scraped content. If the content // requires post-processing, this function fans out to the given content // type and post-processes it. -func (c Cache) postScrape(ctx context.Context, content ScrapedContent, excludeTagRE []*regexp.Regexp) (_ ScrapedContent, ignoredTags []string, err error) { +// Assumes called within a read transaction. +func (c *postScraper) postScrape(ctx context.Context, content ScrapedContent) (_ ScrapedContent, err error) { + const related = false + // Analyze the concrete type, call the right post-processing function switch v := content.(type) { case *models.ScrapedPerformer: if v != nil { - return c.postScrapePerformer(ctx, *v, excludeTagRE) + return c.postScrapePerformer(ctx, *v, related) } case models.ScrapedPerformer: - return c.postScrapePerformer(ctx, v, excludeTagRE) + return c.postScrapePerformer(ctx, v, related) case *models.ScrapedScene: if v != nil { - return c.postScrapeScene(ctx, *v, excludeTagRE) + return c.postScrapeScene(ctx, *v) } case models.ScrapedScene: - return c.postScrapeScene(ctx, v, excludeTagRE) + return c.postScrapeScene(ctx, v) case *models.ScrapedGallery: if v != nil { - return c.postScrapeGallery(ctx, *v, excludeTagRE) + return c.postScrapeGallery(ctx, *v) } case models.ScrapedGallery: - return c.postScrapeGallery(ctx, v, excludeTagRE) + return c.postScrapeGallery(ctx, v) case *models.ScrapedImage: if v != nil { - return c.postScrapeImage(ctx, *v, excludeTagRE) + return c.postScrapeImage(ctx, *v) } case models.ScrapedImage: - return c.postScrapeImage(ctx, v, excludeTagRE) + return c.postScrapeImage(ctx, v) case *models.ScrapedMovie: if v != nil { - return c.postScrapeMovie(ctx, *v, excludeTagRE) + return c.postScrapeMovie(ctx, *v, related) } case models.ScrapedMovie: - return c.postScrapeMovie(ctx, v, excludeTagRE) + return c.postScrapeMovie(ctx, v, related) case *models.ScrapedGroup: if v != nil { - return c.postScrapeGroup(ctx, *v, excludeTagRE) + return c.postScrapeGroup(ctx, *v, related) } case models.ScrapedGroup: - return c.postScrapeGroup(ctx, v, excludeTagRE) + return c.postScrapeGroup(ctx, v, related) } // If nothing matches, pass the content through - return content, nil, nil + return content, nil } -// postScrapeSingle handles post-processing of a single scraped content item. -// This is a convenience function that includes logging the ignored tags, as opposed to logging them in the caller. -func (c Cache) postScrapeSingle(ctx context.Context, content ScrapedContent) (ScrapedContent, error) { - ret, ignoredTags, err := c.postScrape(ctx, content, c.compileExcludeTagPatterns()) +func (c *postScraper) filterTags(tags []*models.ScrapedTag) []*models.ScrapedTag { + var ret []*models.ScrapedTag + var thisIgnoredTags []string + ret, thisIgnoredTags = FilterTags(c.excludeTagRE, tags) + c.ignoredTags = sliceutil.AppendUniques(c.ignoredTags, thisIgnoredTags) + + return ret +} + +func (c *postScraper) postScrapePerformer(ctx context.Context, p models.ScrapedPerformer, related bool) (_ ScrapedContent, err error) { + r := c.repository + tqb := r.TagFinder + + tags, err := postProcessTags(ctx, tqb, p.Tags) if err != nil { return nil, err } - LogIgnoredTags(ignoredTags) - return ret, nil -} - -func (c Cache) postScrapePerformer(ctx context.Context, p models.ScrapedPerformer, excludeTagRE []*regexp.Regexp) (_ ScrapedContent, ignoredTags []string, err error) { - r := c.repository - if err := r.WithReadTxn(ctx, func(ctx context.Context) error { - tqb := r.TagFinder - - tags, err := postProcessTags(ctx, tqb, p.Tags) - if err != nil { - return err - } - p.Tags, ignoredTags = FilterTags(excludeTagRE, tags) - - return nil - }); err != nil { - return nil, nil, err - } + p.Tags = c.filterTags(tags) // post-process - set the image if applicable - if err := setPerformerImage(ctx, c.client, &p, c.globalConfig); err != nil { - logger.Warnf("Could not set image using URL %s: %s", *p.Image, err.Error()) + // don't set image for related performers to avoid excessive network calls + if !related { + if err := setPerformerImage(ctx, c.client, &p, c.globalConfig); err != nil { + logger.Warnf("Could not set image using URL %s: %s", *p.Image, err.Error()) + } } p.Country = resolveCountryName(p.Country) @@ -119,89 +125,224 @@ func (c Cache) postScrapePerformer(ctx context.Context, p models.ScrapedPerforme } } - return p, ignoredTags, nil + return p, nil } -func (c Cache) postScrapeMovie(ctx context.Context, m models.ScrapedMovie, excludeTagRE []*regexp.Regexp) (_ ScrapedContent, ignoredTags []string, err error) { +func (c *postScraper) postScrapeMovie(ctx context.Context, m models.ScrapedMovie, related bool) (_ ScrapedContent, err error) { r := c.repository - if err := r.WithReadTxn(ctx, func(ctx context.Context) error { - tqb := r.TagFinder - tags, err := postProcessTags(ctx, tqb, m.Tags) - if err != nil { - return err - } - m.Tags, ignoredTags = FilterTags(excludeTagRE, tags) - - if m.Studio != nil { - if err := match.ScrapedStudio(ctx, r.StudioFinder, m.Studio, ""); err != nil { - return err - } - } - - return nil - }); err != nil { - return nil, nil, err - } - - // post-process - set the image if applicable - if err := setMovieFrontImage(ctx, c.client, &m, c.globalConfig); err != nil { - logger.Warnf("could not set front image using URL %s: %v", *m.FrontImage, err) - } - if err := setMovieBackImage(ctx, c.client, &m, c.globalConfig); err != nil { - logger.Warnf("could not set back image using URL %s: %v", *m.BackImage, err) - } - - return m, ignoredTags, nil -} - -func (c Cache) postScrapeGroup(ctx context.Context, m models.ScrapedGroup, excludeTagRE []*regexp.Regexp) (_ ScrapedContent, ignoredTags []string, err error) { - r := c.repository - if err := r.WithReadTxn(ctx, func(ctx context.Context) error { - tqb := r.TagFinder - tags, err := postProcessTags(ctx, tqb, m.Tags) - if err != nil { - return err - } - m.Tags, ignoredTags = FilterTags(excludeTagRE, tags) - - if m.Studio != nil { - if err := match.ScrapedStudio(ctx, r.StudioFinder, m.Studio, ""); err != nil { - return err - } - } - - return nil - }); err != nil { - return nil, nil, err - } - - // post-process - set the image if applicable - if err := setGroupFrontImage(ctx, c.client, &m, c.globalConfig); err != nil { - logger.Warnf("could not set front image using URL %s: %v", *m.FrontImage, err) - } - if err := setGroupBackImage(ctx, c.client, &m, c.globalConfig); err != nil { - logger.Warnf("could not set back image using URL %s: %v", *m.BackImage, err) - } - - return m, ignoredTags, nil -} - -func (c Cache) postScrapeScenePerformer(ctx context.Context, p models.ScrapedPerformer, excludeTagRE []*regexp.Regexp) (ignoredTags []string, err error) { - tqb := c.repository.TagFinder - - tags, err := postProcessTags(ctx, tqb, p.Tags) + tqb := r.TagFinder + tags, err := postProcessTags(ctx, tqb, m.Tags) if err != nil { return nil, err } - p.Tags = tags - p.Tags, ignoredTags = FilterTags(excludeTagRE, tags) + m.Tags = c.filterTags(tags) - p.Country = resolveCountryName(p.Country) + if m.Studio != nil { + if err := match.ScrapedStudio(ctx, r.StudioFinder, m.Studio, ""); err != nil { + return nil, err + } + } - return ignoredTags, nil + // populate URL/URLs + // if URLs are provided, only use those + if len(m.URLs) > 0 { + m.URL = &m.URLs[0] + } else { + urls := []string{} + if m.URL != nil { + urls = append(urls, *m.URL) + } + + if len(urls) > 0 { + m.URLs = urls + } + } + + // post-process - set the image if applicable + // don't set images for related movies to avoid excessive network calls + if !related { + if err := processImageField(ctx, m.FrontImage, c.client, c.globalConfig); err != nil { + logger.Warnf("could not set front image using URL %s: %v", *m.FrontImage, err) + } + if err := processImageField(ctx, m.BackImage, c.client, c.globalConfig); err != nil { + logger.Warnf("could not set back image using URL %s: %v", *m.BackImage, err) + } + } + + return m, nil } -func (c Cache) postScrapeScene(ctx context.Context, scene models.ScrapedScene, excludeTagRE []*regexp.Regexp) (_ ScrapedContent, ignoredTags []string, err error) { +func (c *postScraper) postScrapeGroup(ctx context.Context, m models.ScrapedGroup, related bool) (_ ScrapedContent, err error) { + r := c.repository + tqb := r.TagFinder + tags, err := postProcessTags(ctx, tqb, m.Tags) + if err != nil { + return nil, err + } + m.Tags = c.filterTags(tags) + + if m.Studio != nil { + if err := match.ScrapedStudio(ctx, r.StudioFinder, m.Studio, ""); err != nil { + return nil, err + } + } + + // populate URL/URLs + // if URLs are provided, only use those + if len(m.URLs) > 0 { + m.URL = &m.URLs[0] + } else { + urls := []string{} + if m.URL != nil { + urls = append(urls, *m.URL) + } + + if len(urls) > 0 { + m.URLs = urls + } + } + + // post-process - set the image if applicable + // don't set images for related groups to avoid excessive network calls + if !related { + if err := processImageField(ctx, m.FrontImage, c.client, c.globalConfig); err != nil { + logger.Warnf("could not set front image using URL %s: %v", *m.FrontImage, err) + } + if err := processImageField(ctx, m.BackImage, c.client, c.globalConfig); err != nil { + logger.Warnf("could not set back image using URL %s: %v", *m.BackImage, err) + } + } + + return m, nil +} + +// postScrapeRelatedPerformers post-processes a list of performers. +// It modifies the performers in place. +func (c *postScraper) postScrapeRelatedPerformers(ctx context.Context, items []*models.ScrapedPerformer) error { + for _, p := range items { + if p == nil { + continue + } + + const related = true + sc, err := c.postScrapePerformer(ctx, *p, related) + if err != nil { + return err + } + newP := sc.(models.ScrapedPerformer) + *p = newP + + if err := match.ScrapedPerformer(ctx, c.repository.PerformerFinder, p, ""); err != nil { + return err + } + } + return nil +} + +func (c *postScraper) postScrapeRelatedMovies(ctx context.Context, items []*models.ScrapedMovie) error { + for _, p := range items { + const related = true + sc, err := c.postScrapeMovie(ctx, *p, related) + if err != nil { + return err + } + newP := sc.(models.ScrapedMovie) + *p = newP + + matchedID, err := match.ScrapedGroup(ctx, c.repository.GroupFinder, p.StoredID, p.Name) + if err != nil { + return err + } + + if matchedID != nil { + p.StoredID = matchedID + } + } + + return nil +} + +func (c *postScraper) postScrapeRelatedGroups(ctx context.Context, items []*models.ScrapedGroup) error { + for _, p := range items { + const related = true + sc, err := c.postScrapeGroup(ctx, *p, related) + if err != nil { + return err + } + newP := sc.(models.ScrapedGroup) + *p = newP + + matchedID, err := match.ScrapedGroup(ctx, c.repository.GroupFinder, p.StoredID, p.Name) + if err != nil { + return err + } + + if matchedID != nil { + p.StoredID = matchedID + } + } + + return nil +} + +func (c *postScraper) postScrapeStudio(ctx context.Context, s models.ScrapedStudio, related bool) (_ ScrapedContent, err error) { + r := c.repository + tqb := r.TagFinder + + tags, err := postProcessTags(ctx, tqb, s.Tags) + if err != nil { + return nil, err + } + + s.Tags = c.filterTags(tags) + + // post-process - set the image if applicable + // don't set image for related studios to avoid excessive network calls + if !related { + if err := setStudioImage(ctx, c.client, &s, c.globalConfig); err != nil { + logger.Warnf("Could not set image using URL %s: %s", *s.Image, err.Error()) + } + } + + // populate URL/URLs + // if URLs are provided, only use those + if len(s.URLs) > 0 { + s.URL = &s.URLs[0] + } else { + urls := []string{} + if s.URL != nil { + urls = append(urls, *s.URL) + } + + if len(urls) > 0 { + s.URLs = urls + } + } + + return s, nil +} + +func (c *postScraper) postScrapeRelatedStudio(ctx context.Context, s *models.ScrapedStudio) error { + if s == nil { + return nil + } + + const related = true + sc, err := c.postScrapeStudio(ctx, *s, related) + if err != nil { + return err + } + newS := sc.(models.ScrapedStudio) + *s = newS + + if err = match.ScrapedStudio(ctx, c.repository.StudioFinder, s, ""); err != nil { + return err + } + + return nil +} + +func (c *postScraper) postScrapeScene(ctx context.Context, scene models.ScrapedScene) (_ ScrapedContent, err error) { // set the URL/URLs field if scene.URL == nil && len(scene.URLs) > 0 { scene.URL = &scene.URLs[0] @@ -211,92 +352,53 @@ func (c Cache) postScrapeScene(ctx context.Context, scene models.ScrapedScene, e } r := c.repository - if err := r.WithReadTxn(ctx, func(ctx context.Context) error { - pqb := r.PerformerFinder - gqb := r.GroupFinder - tqb := r.TagFinder - sqb := r.StudioFinder + tqb := r.TagFinder - for _, p := range scene.Performers { - if p == nil { - continue - } + if err = c.postScrapeRelatedPerformers(ctx, scene.Performers); err != nil { + return nil, err + } - thisIgnoredTags, err := c.postScrapeScenePerformer(ctx, *p, excludeTagRE) - if err != nil { - return err - } + if err = c.postScrapeRelatedMovies(ctx, scene.Movies); err != nil { + return nil, err + } - if err := match.ScrapedPerformer(ctx, pqb, p, ""); err != nil { - return err - } + if err = c.postScrapeRelatedGroups(ctx, scene.Groups); err != nil { + return nil, err + } - ignoredTags = sliceutil.AppendUniques(ignoredTags, thisIgnoredTags) + // HACK - if movies was returned but not groups, add the groups from the movies + // if groups was returned but not movies, add the movies from the groups for backward compatibility + if len(scene.Movies) > 0 && len(scene.Groups) == 0 { + for _, m := range scene.Movies { + g := m.ScrapedGroup() + scene.Groups = append(scene.Groups, &g) } - - for _, p := range scene.Movies { - matchedID, err := match.ScrapedGroup(ctx, gqb, p.StoredID, p.Name) - if err != nil { - return err - } - - if matchedID != nil { - p.StoredID = matchedID - } + } else if len(scene.Groups) > 0 && len(scene.Movies) == 0 { + for _, g := range scene.Groups { + m := g.ScrapedMovie() + scene.Movies = append(scene.Movies, &m) } + } - for _, p := range scene.Groups { - matchedID, err := match.ScrapedGroup(ctx, gqb, p.StoredID, p.Name) - if err != nil { - return err - } + tags, err := postProcessTags(ctx, tqb, scene.Tags) + if err != nil { + return nil, err + } + scene.Tags = c.filterTags(tags) - if matchedID != nil { - p.StoredID = matchedID - } - } - - // HACK - if movies was returned but not groups, add the groups from the movies - // if groups was returned but not movies, add the movies from the groups for backward compatibility - if len(scene.Movies) > 0 && len(scene.Groups) == 0 { - for _, m := range scene.Movies { - g := m.ScrapedGroup() - scene.Groups = append(scene.Groups, &g) - } - } else if len(scene.Groups) > 0 && len(scene.Movies) == 0 { - for _, g := range scene.Groups { - m := g.ScrapedMovie() - scene.Movies = append(scene.Movies, &m) - } - } - - tags, err := postProcessTags(ctx, tqb, scene.Tags) - if err != nil { - return err - } - scene.Tags, ignoredTags = FilterTags(excludeTagRE, tags) - - if scene.Studio != nil { - err := match.ScrapedStudio(ctx, sqb, scene.Studio, "") - if err != nil { - return err - } - } - - return nil - }); err != nil { - return nil, nil, err + if err := c.postScrapeRelatedStudio(ctx, scene.Studio); err != nil { + return nil, err } // post-process - set the image if applicable - if err := setSceneImage(ctx, c.client, &scene, c.globalConfig); err != nil { + if err := processImageField(ctx, scene.Image, c.client, c.globalConfig); err != nil { logger.Warnf("Could not set image using URL %s: %v", *scene.Image, err) } - return scene, ignoredTags, nil + return scene, nil } -func (c Cache) postScrapeGallery(ctx context.Context, g models.ScrapedGallery, excludeTagRE []*regexp.Regexp) (_ ScrapedContent, ignoredTags []string, err error) { +func (c *postScraper) postScrapeGallery(ctx context.Context, g models.ScrapedGallery) (_ ScrapedContent, err error) { // set the URL/URLs field if g.URL == nil && len(g.URLs) > 0 { g.URL = &g.URLs[0] @@ -306,70 +408,65 @@ func (c Cache) postScrapeGallery(ctx context.Context, g models.ScrapedGallery, e } r := c.repository - if err := r.WithReadTxn(ctx, func(ctx context.Context) error { - pqb := r.PerformerFinder - tqb := r.TagFinder - sqb := r.StudioFinder + tqb := r.TagFinder - for _, p := range g.Performers { - err := match.ScrapedPerformer(ctx, pqb, p, "") - if err != nil { - return err - } - } - - tags, err := postProcessTags(ctx, tqb, g.Tags) - if err != nil { - return err - } - g.Tags, ignoredTags = FilterTags(excludeTagRE, tags) - - if g.Studio != nil { - err := match.ScrapedStudio(ctx, sqb, g.Studio, "") - if err != nil { - return err - } - } - - return nil - }); err != nil { - return nil, nil, err + if err = c.postScrapeRelatedPerformers(ctx, g.Performers); err != nil { + return nil, err } - return g, ignoredTags, nil + tags, err := postProcessTags(ctx, tqb, g.Tags) + if err != nil { + return nil, err + } + g.Tags = c.filterTags(tags) + + if err := c.postScrapeRelatedStudio(ctx, g.Studio); err != nil { + return nil, err + } + + return g, nil } -func (c Cache) postScrapeImage(ctx context.Context, image models.ScrapedImage, excludeTagRE []*regexp.Regexp) (_ ScrapedContent, ignoredTags []string, err error) { +func (c *postScraper) postScrapeImage(ctx context.Context, image models.ScrapedImage) (_ ScrapedContent, err error) { r := c.repository - if err := r.WithReadTxn(ctx, func(ctx context.Context) error { - pqb := r.PerformerFinder - tqb := r.TagFinder - sqb := r.StudioFinder + tqb := r.TagFinder - for _, p := range image.Performers { - if err := match.ScrapedPerformer(ctx, pqb, p, ""); err != nil { - return err - } - } + if err = c.postScrapeRelatedPerformers(ctx, image.Performers); err != nil { + return nil, err + } - tags, err := postProcessTags(ctx, tqb, image.Tags) + tags, err := postProcessTags(ctx, tqb, image.Tags) + if err != nil { + return nil, err + } + + image.Tags = c.filterTags(tags) + + if err := c.postScrapeRelatedStudio(ctx, image.Studio); err != nil { + return nil, err + } + + return image, nil +} + +// postScrapeSingle handles post-processing of a single scraped content item. +// This is a convenience function that includes logging the ignored tags, as opposed to logging them in the caller. +func (c Cache) postScrapeSingle(ctx context.Context, content ScrapedContent) (ret ScrapedContent, err error) { + pp := postScraper{ + Cache: c, + excludeTagRE: c.compileExcludeTagPatterns(), + } + + if err := c.repository.WithReadTxn(ctx, func(ctx context.Context) error { + ret, err = pp.postScrape(ctx, content) if err != nil { return err } - - image.Tags, ignoredTags = FilterTags(excludeTagRE, tags) - - if image.Studio != nil { - err := match.ScrapedStudio(ctx, sqb, image.Studio, "") - if err != nil { - return err - } - } - return nil }); err != nil { - return nil, nil, err + return nil, err } - return image, ignoredTags, nil + LogIgnoredTags(pp.ignoredTags) + return ret, nil } diff --git a/pkg/scraper/query_url.go b/pkg/scraper/query_url.go index 91adb7d67..2cd9f683e 100644 --- a/pkg/scraper/query_url.go +++ b/pkg/scraper/query_url.go @@ -110,7 +110,7 @@ func (p queryURLParameters) constructURL(url string) string { } // replaceURL does a partial URL Replace ( only url parameter is used) -func replaceURL(url string, scraperConfig scraperTypeConfig) string { +func replaceURL(url string, scraperConfig ByURLDefinition) string { u := url queryURL := queryURLParameterFromURL(u) if scraperConfig.QueryURLReplacements != nil { diff --git a/pkg/scraper/script.go b/pkg/scraper/script.go index 866c92365..f8e47b5d8 100644 --- a/pkg/scraper/script.go +++ b/pkg/scraper/script.go @@ -208,22 +208,11 @@ func galleryInputFromGallery(gallery *models.Gallery) galleryInput { var ErrScraperScript = errors.New("scraper script error") type scriptScraper struct { - scraper scraperTypeConfig - config config + definition Definition globalConfig GlobalConfig } -func newScriptScraper(scraper scraperTypeConfig, config config, globalConfig GlobalConfig) *scriptScraper { - return &scriptScraper{ - scraper: scraper, - config: config, - globalConfig: globalConfig, - } -} - -func (s *scriptScraper) runScraperScript(ctx context.Context, inString string, out interface{}) error { - command := s.scraper.Script - +func (s *scriptScraper) runScraperScript(ctx context.Context, command []string, inString string, out interface{}) error { var cmd *exec.Cmd if python.IsPythonCommand(command[0]) { pythonPath := s.globalConfig.GetPythonPath() @@ -233,7 +222,7 @@ func (s *scriptScraper) runScraperScript(ctx context.Context, inString string, o logger.Warnf("%s", err) } else { cmd = p.Command(ctx, command[1:]) - envVariable, _ := filepath.Abs(filepath.Dir(filepath.Dir(s.config.path))) + envVariable, _ := filepath.Abs(filepath.Dir(filepath.Dir(s.definition.path))) python.AppendPythonPath(cmd, envVariable) } } @@ -243,7 +232,7 @@ func (s *scriptScraper) runScraperScript(ctx context.Context, inString string, o cmd = stashExec.CommandContext(ctx, command[0], command[1:]...) } - cmd.Dir = filepath.Dir(s.config.path) + cmd.Dir = filepath.Dir(s.definition.path) stdin, err := cmd.StdinPipe() if err != nil { @@ -273,7 +262,7 @@ func (s *scriptScraper) runScraperScript(ctx context.Context, inString string, o return errors.New("error running scraper script") } - go handleScraperStderr(s.config.Name, stderr) + go handleScraperStderr(s.definition.Name, stderr) logger.Debugf("Scraper script <%s> started", strings.Join(cmd.Args, " ")) @@ -312,7 +301,39 @@ func (s *scriptScraper) runScraperScript(ctx context.Context, inString string, o return nil } -func (s *scriptScraper) scrapeByName(ctx context.Context, name string, ty ScrapeContentType) ([]ScrapedContent, error) { +func (s *scriptScraper) scrape(ctx context.Context, command []string, input string, ty ScrapeContentType) (ScrapedContent, error) { + switch ty { + case ScrapeContentTypePerformer: + var performer *models.ScrapedPerformer + err := s.runScraperScript(ctx, command, input, &performer) + return performer, err + case ScrapeContentTypeGallery: + var gallery *models.ScrapedGallery + err := s.runScraperScript(ctx, command, input, &gallery) + return gallery, err + case ScrapeContentTypeScene: + var scene *models.ScrapedScene + err := s.runScraperScript(ctx, command, input, &scene) + return scene, err + case ScrapeContentTypeMovie, ScrapeContentTypeGroup: + var movie *models.ScrapedMovie + err := s.runScraperScript(ctx, command, input, &movie) + return movie, err + case ScrapeContentTypeImage: + var image *models.ScrapedImage + err := s.runScraperScript(ctx, command, input, &image) + return image, err + } + + return nil, ErrNotSupported +} + +type scriptNameScraper struct { + scriptScraper + definition ByNameDefinition +} + +func (s *scriptNameScraper) scrapeByName(ctx context.Context, name string, ty ScrapeContentType) ([]ScrapedContent, error) { input := `{"name": "` + name + `"}` var ret []ScrapedContent @@ -320,7 +341,7 @@ func (s *scriptScraper) scrapeByName(ctx context.Context, name string, ty Scrape switch ty { case ScrapeContentTypePerformer: var performers []models.ScrapedPerformer - err = s.runScraperScript(ctx, input, &performers) + err = s.runScraperScript(ctx, s.definition.Script, input, &performers) if err == nil { for _, p := range performers { v := p @@ -329,7 +350,7 @@ func (s *scriptScraper) scrapeByName(ctx context.Context, name string, ty Scrape } case ScrapeContentTypeScene: var scenes []models.ScrapedScene - err = s.runScraperScript(ctx, input, &scenes) + err = s.runScraperScript(ctx, s.definition.Script, input, &scenes) if err == nil { for _, s := range scenes { v := s @@ -343,7 +364,21 @@ func (s *scriptScraper) scrapeByName(ctx context.Context, name string, ty Scrape return ret, err } -func (s *scriptScraper) scrapeByFragment(ctx context.Context, input Input) (ScrapedContent, error) { +type scriptURLScraper struct { + scriptScraper + definition ByURLDefinition +} + +func (s *scriptURLScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeContentType) (ScrapedContent, error) { + return s.scrape(ctx, s.definition.Script, `{"url": "`+url+`"}`, ty) +} + +type scriptFragmentScraper struct { + scriptScraper + definition ByFragmentDefinition +} + +func (s *scriptFragmentScraper) scrapeByFragment(ctx context.Context, input Input) (ScrapedContent, error) { var inString []byte var err error var ty ScrapeContentType @@ -363,41 +398,10 @@ func (s *scriptScraper) scrapeByFragment(ctx context.Context, input Input) (Scra return nil, err } - return s.scrape(ctx, string(inString), ty) + return s.scrape(ctx, s.definition.Script, string(inString), ty) } -func (s *scriptScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeContentType) (ScrapedContent, error) { - return s.scrape(ctx, `{"url": "`+url+`"}`, ty) -} - -func (s *scriptScraper) scrape(ctx context.Context, input string, ty ScrapeContentType) (ScrapedContent, error) { - switch ty { - case ScrapeContentTypePerformer: - var performer *models.ScrapedPerformer - err := s.runScraperScript(ctx, input, &performer) - return performer, err - case ScrapeContentTypeGallery: - var gallery *models.ScrapedGallery - err := s.runScraperScript(ctx, input, &gallery) - return gallery, err - case ScrapeContentTypeScene: - var scene *models.ScrapedScene - err := s.runScraperScript(ctx, input, &scene) - return scene, err - case ScrapeContentTypeMovie, ScrapeContentTypeGroup: - var movie *models.ScrapedMovie - err := s.runScraperScript(ctx, input, &movie) - return movie, err - case ScrapeContentTypeImage: - var image *models.ScrapedImage - err := s.runScraperScript(ctx, input, &image) - return image, err - } - - return nil, ErrNotSupported -} - -func (s *scriptScraper) scrapeSceneByScene(ctx context.Context, scene *models.Scene) (*models.ScrapedScene, error) { +func (s *scriptFragmentScraper) scrapeSceneByScene(ctx context.Context, scene *models.Scene) (*models.ScrapedScene, error) { inString, err := json.Marshal(sceneInputFromScene(scene)) if err != nil { @@ -406,12 +410,12 @@ func (s *scriptScraper) scrapeSceneByScene(ctx context.Context, scene *models.Sc var ret *models.ScrapedScene - err = s.runScraperScript(ctx, string(inString), &ret) + err = s.runScraperScript(ctx, s.definition.Script, string(inString), &ret) return ret, err } -func (s *scriptScraper) scrapeGalleryByGallery(ctx context.Context, gallery *models.Gallery) (*models.ScrapedGallery, error) { +func (s *scriptFragmentScraper) scrapeGalleryByGallery(ctx context.Context, gallery *models.Gallery) (*models.ScrapedGallery, error) { inString, err := json.Marshal(galleryInputFromGallery(gallery)) if err != nil { @@ -420,12 +424,12 @@ func (s *scriptScraper) scrapeGalleryByGallery(ctx context.Context, gallery *mod var ret *models.ScrapedGallery - err = s.runScraperScript(ctx, string(inString), &ret) + err = s.runScraperScript(ctx, s.definition.Script, string(inString), &ret) return ret, err } -func (s *scriptScraper) scrapeImageByImage(ctx context.Context, image *models.Image) (*models.ScrapedImage, error) { +func (s *scriptFragmentScraper) scrapeImageByImage(ctx context.Context, image *models.Image) (*models.ScrapedImage, error) { inString, err := json.Marshal(imageToUpdateInput(image)) if err != nil { @@ -434,7 +438,7 @@ func (s *scriptScraper) scrapeImageByImage(ctx context.Context, image *models.Im var ret *models.ScrapedImage - err = s.runScraperScript(ctx, string(inString), &ret) + err = s.runScraperScript(ctx, s.definition.Script, string(inString), &ret) return ret, err } diff --git a/pkg/scraper/stash.go b/pkg/scraper/stash.go index 5c5cab9fc..23c4b9063 100644 --- a/pkg/scraper/stash.go +++ b/pkg/scraper/stash.go @@ -14,15 +14,13 @@ import ( ) type stashScraper struct { - scraper scraperTypeConfig - config config + config Definition globalConfig GlobalConfig client *http.Client } -func newStashScraper(scraper scraperTypeConfig, client *http.Client, config config, globalConfig GlobalConfig) *stashScraper { +func newStashScraper(client *http.Client, config Definition, globalConfig GlobalConfig) *stashScraper { return &stashScraper{ - scraper: scraper, config: config, client: client, globalConfig: globalConfig, diff --git a/pkg/scraper/tag.go b/pkg/scraper/tag.go index c26aa855e..14f02e397 100644 --- a/pkg/scraper/tag.go +++ b/pkg/scraper/tag.go @@ -15,7 +15,8 @@ func postProcessTags(ctx context.Context, tqb models.TagQueryer, scrapedTags []* ret = make([]*models.ScrapedTag, 0, len(scrapedTags)) for _, t := range scrapedTags { - err := match.ScrapedTag(ctx, tqb, t) + // Pass empty string for endpoint since this is used by general scrapers, not just stash-box + err := match.ScrapedTag(ctx, tqb, t, "") if err != nil { return nil, err } diff --git a/pkg/scraper/url.go b/pkg/scraper/url.go index b53d7b27f..d036ae68e 100644 --- a/pkg/scraper/url.go +++ b/pkg/scraper/url.go @@ -25,8 +25,8 @@ import ( const scrapeDefaultSleep = time.Second * 2 -func loadURL(ctx context.Context, loadURL string, client *http.Client, scraperConfig config, globalConfig GlobalConfig) (io.Reader, error) { - driverOptions := scraperConfig.DriverOptions +func loadURL(ctx context.Context, loadURL string, client *http.Client, def Definition, globalConfig GlobalConfig) (io.Reader, error) { + driverOptions := def.DriverOptions if driverOptions != nil && driverOptions.UseCDP { // get the page using chrome dp return urlFromCDP(ctx, loadURL, *driverOptions, globalConfig) @@ -37,7 +37,7 @@ func loadURL(ctx context.Context, loadURL string, client *http.Client, scraperCo return nil, err } - jar, err := scraperConfig.jar() + jar, err := def.jar() if err != nil { return nil, fmt.Errorf("error creating cookie jar: %w", err) } @@ -83,7 +83,7 @@ func loadURL(ctx context.Context, loadURL string, client *http.Client, scraperCo } bodyReader := bytes.NewReader(body) - printCookies(jar, scraperConfig, "Jar cookies found for scraper urls") + printCookies(jar, def, "Jar cookies found for scraper urls") return charset.NewReader(bodyReader, resp.Header.Get("Content-Type")) } diff --git a/pkg/scraper/xpath.go b/pkg/scraper/xpath.go index 9993aa3ff..bf70869e8 100644 --- a/pkg/scraper/xpath.go +++ b/pkg/scraper/xpath.go @@ -3,7 +3,6 @@ package scraper import ( "bytes" "context" - "errors" "fmt" "net/http" "net/url" @@ -19,49 +18,36 @@ import ( ) type xpathScraper struct { - scraper scraperTypeConfig - config config + definition Definition globalConfig GlobalConfig client *http.Client } -func newXpathScraper(scraper scraperTypeConfig, client *http.Client, config config, globalConfig GlobalConfig) *xpathScraper { - return &xpathScraper{ - scraper: scraper, - config: config, - globalConfig: globalConfig, - client: client, +func (s *xpathScraper) getXpathScraper(name string) (*mappedScraper, error) { + ret, ok := s.definition.XPathScrapers[name] + if !ok { + return nil, fmt.Errorf("xpath scraper with name %s not found in config", name) } + return &ret, nil } -func (s *xpathScraper) getXpathScraper() *mappedScraper { - return s.config.XPathScrapers[s.scraper.Scraper] +type xpathURLScraper struct { + xpathScraper + definition ByURLDefinition } -func (s *xpathScraper) scrapeURL(ctx context.Context, url string) (*html.Node, *mappedScraper, error) { - scraper := s.getXpathScraper() - - if scraper == nil { - return nil, nil, errors.New("xpath scraper with name " + s.scraper.Scraper + " not found in config") - } - - doc, err := s.loadURL(ctx, url) - - if err != nil { - return nil, nil, err - } - - return doc, scraper, nil -} - -func (s *xpathScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeContentType) (ScrapedContent, error) { - u := replaceURL(url, s.scraper) // allow a URL Replace for performer by URL queries - doc, scraper, err := s.scrapeURL(ctx, u) +func (s *xpathURLScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeContentType) (ScrapedContent, error) { + scraper, err := s.getXpathScraper(s.definition.Scraper) if err != nil { return nil, err } - q := s.getXPathQuery(doc) + doc, err := s.loadURL(ctx, url) + if err != nil { + return nil, err + } + + q := s.getXPathQuery(doc, url) // if these just return the return values from scraper.scrape* functions then // it ends up returning ScrapedContent(nil) rather than nil switch ty { @@ -100,11 +86,15 @@ func (s *xpathScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeCon return nil, ErrNotSupported } -func (s *xpathScraper) scrapeByName(ctx context.Context, name string, ty ScrapeContentType) ([]ScrapedContent, error) { - scraper := s.getXpathScraper() +type xpathNameScraper struct { + xpathScraper + definition ByNameDefinition +} - if scraper == nil { - return nil, fmt.Errorf("%w: name %v", ErrNotFound, s.scraper.Scraper) +func (s *xpathNameScraper) scrapeByName(ctx context.Context, name string, ty ScrapeContentType) ([]ScrapedContent, error) { + scraper, err := s.getXpathScraper(s.definition.Scraper) + if err != nil { + return nil, err } const placeholder = "{}" @@ -112,7 +102,7 @@ func (s *xpathScraper) scrapeByName(ctx context.Context, name string, ty ScrapeC // replace the placeholder string with the URL-escaped name escapedName := url.QueryEscape(name) - url := s.scraper.QueryURL + url := s.definition.QueryURL url = strings.ReplaceAll(url, placeholder, escapedName) doc, err := s.loadURL(ctx, url) @@ -121,7 +111,7 @@ func (s *xpathScraper) scrapeByName(ctx context.Context, name string, ty ScrapeC return nil, err } - q := s.getXPathQuery(doc) + q := s.getXPathQuery(doc, url) q.setType(SearchQuery) var content []ScrapedContent @@ -151,18 +141,22 @@ func (s *xpathScraper) scrapeByName(ctx context.Context, name string, ty ScrapeC return nil, ErrNotSupported } -func (s *xpathScraper) scrapeSceneByScene(ctx context.Context, scene *models.Scene) (*models.ScrapedScene, error) { +type xpathFragmentScraper struct { + xpathScraper + definition ByFragmentDefinition +} + +func (s *xpathFragmentScraper) scrapeSceneByScene(ctx context.Context, scene *models.Scene) (*models.ScrapedScene, error) { // construct the URL queryURL := queryURLParametersFromScene(scene) - if s.scraper.QueryURLReplacements != nil { - queryURL.applyReplacements(s.scraper.QueryURLReplacements) + if s.definition.QueryURLReplacements != nil { + queryURL.applyReplacements(s.definition.QueryURLReplacements) } - url := queryURL.constructURL(s.scraper.QueryURL) + url := queryURL.constructURL(s.definition.QueryURL) - scraper := s.getXpathScraper() - - if scraper == nil { - return nil, errors.New("xpath scraper with name " + s.scraper.Scraper + " not found in config") + scraper, err := s.getXpathScraper(s.definition.Scraper) + if err != nil { + return nil, err } doc, err := s.loadURL(ctx, url) @@ -171,11 +165,11 @@ func (s *xpathScraper) scrapeSceneByScene(ctx context.Context, scene *models.Sce return nil, err } - q := s.getXPathQuery(doc) + q := s.getXPathQuery(doc, url) return scraper.scrapeScene(ctx, q) } -func (s *xpathScraper) scrapeByFragment(ctx context.Context, input Input) (ScrapedContent, error) { +func (s *xpathFragmentScraper) scrapeByFragment(ctx context.Context, input Input) (ScrapedContent, error) { switch { case input.Gallery != nil: return nil, fmt.Errorf("%w: cannot use an xpath scraper as a gallery fragment scraper", ErrNotSupported) @@ -189,15 +183,14 @@ func (s *xpathScraper) scrapeByFragment(ctx context.Context, input Input) (Scrap // construct the URL queryURL := queryURLParametersFromScrapedScene(scene) - if s.scraper.QueryURLReplacements != nil { - queryURL.applyReplacements(s.scraper.QueryURLReplacements) + if s.definition.QueryURLReplacements != nil { + queryURL.applyReplacements(s.definition.QueryURLReplacements) } - url := queryURL.constructURL(s.scraper.QueryURL) + url := queryURL.constructURL(s.definition.QueryURL) - scraper := s.getXpathScraper() - - if scraper == nil { - return nil, errors.New("xpath scraper with name " + s.scraper.Scraper + " not found in config") + scraper, err := s.getXpathScraper(s.definition.Scraper) + if err != nil { + return nil, err } doc, err := s.loadURL(ctx, url) @@ -206,22 +199,21 @@ func (s *xpathScraper) scrapeByFragment(ctx context.Context, input Input) (Scrap return nil, err } - q := s.getXPathQuery(doc) + q := s.getXPathQuery(doc, url) return scraper.scrapeScene(ctx, q) } -func (s *xpathScraper) scrapeGalleryByGallery(ctx context.Context, gallery *models.Gallery) (*models.ScrapedGallery, error) { +func (s *xpathFragmentScraper) scrapeGalleryByGallery(ctx context.Context, gallery *models.Gallery) (*models.ScrapedGallery, error) { // construct the URL queryURL := queryURLParametersFromGallery(gallery) - if s.scraper.QueryURLReplacements != nil { - queryURL.applyReplacements(s.scraper.QueryURLReplacements) + if s.definition.QueryURLReplacements != nil { + queryURL.applyReplacements(s.definition.QueryURLReplacements) } - url := queryURL.constructURL(s.scraper.QueryURL) + url := queryURL.constructURL(s.definition.QueryURL) - scraper := s.getXpathScraper() - - if scraper == nil { - return nil, errors.New("xpath scraper with name " + s.scraper.Scraper + " not found in config") + scraper, err := s.getXpathScraper(s.definition.Scraper) + if err != nil { + return nil, err } doc, err := s.loadURL(ctx, url) @@ -230,22 +222,21 @@ func (s *xpathScraper) scrapeGalleryByGallery(ctx context.Context, gallery *mode return nil, err } - q := s.getXPathQuery(doc) + q := s.getXPathQuery(doc, url) return scraper.scrapeGallery(ctx, q) } -func (s *xpathScraper) scrapeImageByImage(ctx context.Context, image *models.Image) (*models.ScrapedImage, error) { +func (s *xpathFragmentScraper) scrapeImageByImage(ctx context.Context, image *models.Image) (*models.ScrapedImage, error) { // construct the URL queryURL := queryURLParametersFromImage(image) - if s.scraper.QueryURLReplacements != nil { - queryURL.applyReplacements(s.scraper.QueryURLReplacements) + if s.definition.QueryURLReplacements != nil { + queryURL.applyReplacements(s.definition.QueryURLReplacements) } - url := queryURL.constructURL(s.scraper.QueryURL) + url := queryURL.constructURL(s.definition.QueryURL) - scraper := s.getXpathScraper() - - if scraper == nil { - return nil, errors.New("xpath scraper with name " + s.scraper.Scraper + " not found in config") + scraper, err := s.getXpathScraper(s.definition.Scraper) + if err != nil { + return nil, err } doc, err := s.loadURL(ctx, url) @@ -254,19 +245,19 @@ func (s *xpathScraper) scrapeImageByImage(ctx context.Context, image *models.Ima return nil, err } - q := s.getXPathQuery(doc) + q := s.getXPathQuery(doc, url) return scraper.scrapeImage(ctx, q) } func (s *xpathScraper) loadURL(ctx context.Context, url string) (*html.Node, error) { - r, err := loadURL(ctx, url, s.client, s.config, s.globalConfig) + r, err := loadURL(ctx, url, s.client, s.definition, s.globalConfig) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to load URL %q: %w", url, err) } ret, err := html.Parse(r) - if err == nil && s.config.DebugOptions != nil && s.config.DebugOptions.PrintHTML { + if err == nil && s.definition.DebugOptions != nil && s.definition.DebugOptions.PrintHTML { var b bytes.Buffer if err := html.Render(&b, ret); err != nil { logger.Warnf("could not render HTML: %v", err) @@ -277,10 +268,11 @@ func (s *xpathScraper) loadURL(ctx context.Context, url string) (*html.Node, err return ret, err } -func (s *xpathScraper) getXPathQuery(doc *html.Node) *xpathQuery { +func (s *xpathScraper) getXPathQuery(doc *html.Node, url string) *xpathQuery { return &xpathQuery{ doc: doc, scraper: s, + url: url, } } @@ -288,6 +280,7 @@ type xpathQuery struct { doc *html.Node scraper *xpathScraper queryType QueryType + url string } func (q *xpathQuery) getType() QueryType { @@ -298,6 +291,10 @@ func (q *xpathQuery) setType(t QueryType) { q.queryType = t } +func (q *xpathQuery) getURL() string { + return q.url +} + func (q *xpathQuery) runQuery(selector string) ([]string, error) { found, err := htmlquery.QueryAll(q.doc, selector) if err != nil { @@ -346,5 +343,5 @@ func (q *xpathQuery) subScrape(ctx context.Context, value string) mappedQuery { return nil } - return q.scraper.getXPathQuery(doc) + return q.scraper.getXPathQuery(doc, value) } diff --git a/pkg/scraper/xpath_test.go b/pkg/scraper/xpath_test.go index 391f60728..42ee2227b 100644 --- a/pkg/scraper/xpath_test.go +++ b/pkg/scraper/xpath_test.go @@ -674,10 +674,10 @@ func verifyPerformers(t *testing.T, expectedNames []string, expectedURLs []strin } if expectedName != actualName { - t.Errorf("Expected performer name %s, got %s", expectedName, actualName) + t.Errorf("Expected performer name %q, got %q", expectedName, actualName) } if expectedURL != actualURL { - t.Errorf("Expected performer URL %s, got %s", expectedName, actualName) + t.Errorf("Expected performer URL %q, got %q", expectedURL, actualURL) } i++ } @@ -780,7 +780,7 @@ xPathScrapers: Name: //studio ` - c := &config{} + c := &Definition{} err := yaml.Unmarshal([]byte(yamlStr), &c) if err != nil { @@ -892,7 +892,7 @@ xPathScrapers: selector: //span ` - c := &config{} + c := &Definition{} err := yaml.Unmarshal([]byte(yamlStr), &c) if err != nil { @@ -904,12 +904,8 @@ xPathScrapers: client := &http.Client{} ctx := context.Background() - s := newGroupScraper(*c, globalConfig) - us, ok := s.(urlScraper) - if !ok { - t.Error("couldn't convert scraper into url scraper") - } - content, err := us.viaURL(ctx, client, ts.URL, ScrapeContentTypePerformer) + s := scraperFromDefinition(*c, globalConfig) + content, err := s.viaURL(ctx, client, ts.URL, ScrapeContentTypePerformer) if err != nil { t.Errorf("Error scraping performer: %s", err.Error()) diff --git a/pkg/sliceutil/stringslice/string_collections.go b/pkg/sliceutil/stringslice/string_collections.go index f6ea1361c..eff3409e2 100644 --- a/pkg/sliceutil/stringslice/string_collections.go +++ b/pkg/sliceutil/stringslice/string_collections.go @@ -44,3 +44,28 @@ func UniqueFold(s []string) []string { } return ret } + +// UniqueExcludeFold returns a deduplicated slice of strings with the excluded string removed. +// The comparison is case-insensitive. +func UniqueExcludeFold(values []string, exclude string) []string { + seen := make(map[string]struct{}, len(values)) + seen[strings.ToLower(exclude)] = struct{}{} + ret := make([]string, 0, len(values)) + for _, v := range values { + vLower := strings.ToLower(v) + if _, exists := seen[vLower]; exists { + continue + } + seen[vLower] = struct{}{} + ret = append(ret, v) + } + return ret +} + +// TrimSpace trims whitespace from each string in a slice. +func TrimSpace(s []string) []string { + for i, v := range s { + s[i] = strings.TrimSpace(v) + } + return s +} diff --git a/pkg/sqlite/anonymise.go b/pkg/sqlite/anonymise.go index 20926ed25..764f569c0 100644 --- a/pkg/sqlite/anonymise.go +++ b/pkg/sqlite/anonymise.go @@ -102,6 +102,7 @@ func (db *Anonymiser) deleteStashIDs() error { func() error { return db.truncateTable("scene_stash_ids") }, func() error { return db.truncateTable("studio_stash_ids") }, func() error { return db.truncateTable("performer_stash_ids") }, + func() error { return db.truncateTable("tag_stash_ids") }, }) } @@ -619,7 +620,6 @@ func (db *Anonymiser) anonymiseStudios(ctx context.Context) error { query := dialect.From(table).Select( table.Col(idColumn), table.Col("name"), - table.Col("url"), table.Col("details"), ).Where(table.Col(idColumn).Gt(lastID)).Limit(1000) @@ -630,14 +630,12 @@ func (db *Anonymiser) anonymiseStudios(ctx context.Context) error { var ( id int name sql.NullString - url sql.NullString details sql.NullString ) if err := rows.Scan( &id, &name, - &url, &details, ); err != nil { return err @@ -645,7 +643,6 @@ func (db *Anonymiser) anonymiseStudios(ctx context.Context) error { set := goqu.Record{} db.obfuscateNullString(set, "name", name) - db.obfuscateNullString(set, "url", url) db.obfuscateNullString(set, "details", details) if len(set) > 0 { @@ -677,6 +674,10 @@ func (db *Anonymiser) anonymiseStudios(ctx context.Context) error { return err } + if err := db.anonymiseURLs(ctx, goqu.T(studioURLsTable), "studio_id"); err != nil { + return err + } + return nil } diff --git a/pkg/sqlite/criterion_handlers.go b/pkg/sqlite/criterion_handlers.go index 82b9cfc65..1496df71d 100644 --- a/pkg/sqlite/criterion_handlers.go +++ b/pkg/sqlite/criterion_handlers.go @@ -1012,6 +1012,11 @@ func (h *stashIDCriterionHandler) handle(ctx context.Context, f *filterBuilder) return } + // ideally, this handler should just convert to stashIDsCriterionHandler + // but there are some differences in how the existing handler works compared + // to the new code, specifically because this code uses the stringCriterionHandler. + // To minimise potential regressions, we'll keep the existing logic for now. + stashIDRepo := h.stashIDRepository t := stashIDRepo.tableName if h.stashIDTableAs != "" { @@ -1036,11 +1041,59 @@ func (h *stashIDCriterionHandler) handle(ctx context.Context, f *filterBuilder) }, t+".stash_id")(ctx, f) } +type stashIDsCriterionHandler struct { + c *models.StashIDsCriterionInput + stashIDRepository *stashIDRepository + stashIDTableAs string + parentIDCol string +} + +func (h *stashIDsCriterionHandler) handle(ctx context.Context, f *filterBuilder) { + if h.c == nil { + return + } + + stashIDRepo := h.stashIDRepository + t := stashIDRepo.tableName + if h.stashIDTableAs != "" { + t = h.stashIDTableAs + } + + joinClause := fmt.Sprintf("%s.%s = %s", t, stashIDRepo.idColumn, h.parentIDCol) + if h.c.Endpoint != nil && *h.c.Endpoint != "" { + joinClause += fmt.Sprintf(" AND %s.endpoint = '%s'", t, *h.c.Endpoint) + } + + f.addLeftJoin(stashIDRepo.tableName, h.stashIDTableAs, joinClause) + + switch h.c.Modifier { + case models.CriterionModifierIsNull: + f.addWhere(fmt.Sprintf("%s.stash_id IS NULL", t)) + case models.CriterionModifierNotNull: + f.addWhere(fmt.Sprintf("%s.stash_id IS NOT NULL", t)) + case models.CriterionModifierEquals: + var clauses []sqlClause + for _, id := range h.c.StashIDs { + clauses = append(clauses, makeClause(fmt.Sprintf("%s.stash_id = ?", t), id)) + } + f.whereClauses = append(f.whereClauses, orClauses(clauses...)) + case models.CriterionModifierNotEquals: + var clauses []sqlClause + for _, id := range h.c.StashIDs { + clauses = append(clauses, makeClause(fmt.Sprintf("%s.stash_id != ?", t), id)) + } + f.whereClauses = append(f.whereClauses, andClauses(clauses...)) + default: + f.setError(fmt.Errorf("invalid modifier %s for stash IDs criterion", h.c.Modifier)) + } +} + type relatedFilterHandler struct { relatedIDCol string relatedRepo repository relatedHandler criterionHandler joinFn func(f *filterBuilder) + directJoin bool } func (h *relatedFilterHandler) handle(ctx context.Context, f *filterBuilder) { @@ -1054,6 +1107,16 @@ func (h *relatedFilterHandler) handle(ctx context.Context, f *filterBuilder) { return } + if h.joinFn != nil { + h.joinFn(f) + } + + if h.directJoin { + // rerun handler using existing filter builder + h.relatedHandler.handle(ctx, f) + return + } + subQuery := h.relatedRepo.newQuery() selectIDs(&subQuery, subQuery.repository.tableName) if err := subQuery.addFilter(ff); err != nil { @@ -1061,9 +1124,42 @@ func (h *relatedFilterHandler) handle(ctx context.Context, f *filterBuilder) { return } - if h.joinFn != nil { - h.joinFn(f) - } - f.addWhere(fmt.Sprintf("%s IN ("+subQuery.toSQL(false)+")", h.relatedIDCol), subQuery.args...) } + +type phashDistanceCriterionHandler struct { + // assumes that applicable fingerprints table is joined as fingerprints_phash + joinFn func(f *filterBuilder) + criterion *models.PhashDistanceCriterionInput +} + +func (h *phashDistanceCriterionHandler) handle(ctx context.Context, f *filterBuilder) { + phashDistance := h.criterion + if phashDistance == nil { + return + } + + h.joinFn(f) + + value, _ := utils.StringToPhash(phashDistance.Value) + distance := 0 + if phashDistance.Distance != nil { + distance = *phashDistance.Distance + } + + switch { + case phashDistance.Modifier == models.CriterionModifierEquals && distance > 0: + // needed to avoid a type mismatch + f.addWhere("typeof(fingerprints_phash.fingerprint) = 'integer'") + f.addWhere("phash_distance(fingerprints_phash.fingerprint, ?) < ?", value, distance) + case phashDistance.Modifier == models.CriterionModifierNotEquals && distance > 0: + // needed to avoid a type mismatch + f.addWhere("typeof(fingerprints_phash.fingerprint) = 'integer'") + f.addWhere("phash_distance(fingerprints_phash.fingerprint, ?) > ?", value, distance) + default: + intCriterionHandler(&models.IntCriterionInput{ + Value: int(value), + Modifier: phashDistance.Modifier, + }, "fingerprints_phash.fingerprint", nil)(ctx, f) + } +} diff --git a/pkg/sqlite/custom_fields.go b/pkg/sqlite/custom_fields.go index bac6ae5e1..63f85b250 100644 --- a/pkg/sqlite/custom_fields.go +++ b/pkg/sqlite/custom_fields.go @@ -41,18 +41,31 @@ func (s *customFieldsStore) SetCustomFields(ctx context.Context, id int, values case values.Partial != nil: partial = true valMap = values.Partial - default: - return nil } - if err := s.validateCustomFields(valMap); err != nil { + if valMap != nil { + if err := s.validateCustomFields(valMap, values.Remove); err != nil { + return err + } + + if err := s.setCustomFields(ctx, id, valMap, partial); err != nil { + return err + } + } + + if err := s.deleteCustomFields(ctx, id, values.Remove); err != nil { return err } - return s.setCustomFields(ctx, id, valMap, partial) + return nil } -func (s *customFieldsStore) validateCustomFields(values map[string]interface{}) error { +func (s *customFieldsStore) validateCustomFields(values map[string]interface{}, deleteKeys []string) error { + // if values is nil, nothing to validate + if values == nil { + return nil + } + // ensure that custom field names are valid // no leading or trailing whitespace, no empty strings for k := range values { @@ -61,6 +74,13 @@ func (s *customFieldsStore) validateCustomFields(values map[string]interface{}) } } + // ensure delete keys are not also in values + for _, k := range deleteKeys { + if _, ok := values[k]; ok { + return fmt.Errorf("custom field name %q cannot be in both values and delete keys", k) + } + } + return nil } @@ -130,6 +150,22 @@ func (s *customFieldsStore) setCustomFields(ctx context.Context, id int, values return nil } +func (s *customFieldsStore) deleteCustomFields(ctx context.Context, id int, keys []string) error { + if len(keys) == 0 { + return nil + } + + q := dialect.Delete(s.table). + Where(s.fk.Eq(id)). + Where(goqu.I("field").In(keys)) + + if _, err := exec(ctx, q); err != nil { + return fmt.Errorf("deleting custom fields: %w", err) + } + + return nil +} + func (s *customFieldsStore) GetCustomFields(ctx context.Context, id int) (map[string]interface{}, error) { q := dialect.Select("field", "value").From(s.table).Where(s.fk.Eq(id)) diff --git a/pkg/sqlite/custom_fields_test.go b/pkg/sqlite/custom_fields_test.go index ce5c77487..8ee154aec 100644 --- a/pkg/sqlite/custom_fields_test.go +++ b/pkg/sqlite/custom_fields_test.go @@ -64,6 +64,18 @@ func TestSetCustomFields(t *testing.T) { }), false, }, + { + "valid remove", + models.CustomFieldsInput{ + Remove: []string{"real"}, + }, + func() map[string]interface{} { + m := getPerformerCustomFields(performerIdx) + delete(m, "real") + return m + }(), + false, + }, { "leading space full", models.CustomFieldsInput{ @@ -144,16 +156,38 @@ func TestSetCustomFields(t *testing.T) { nil, true, }, + { + "invalid remove full", + models.CustomFieldsInput{ + Full: map[string]interface{}{ + "key": "value", + }, + Remove: []string{"key"}, + }, + nil, + true, + }, + { + "invalid remove partial", + models.CustomFieldsInput{ + Partial: map[string]interface{}{ + "real": float64(4.56), + }, + Remove: []string{"real"}, + }, + nil, + true, + }, } // use performer custom fields store store := db.Performer id := performerIDs[performerIdx] - assert := assert.New(t) - for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { + assert := assert.New(t) + err := store.SetCustomFields(ctx, id, tt.input) if (err != nil) != tt.wantErr { t.Errorf("SetCustomFields() error = %v, wantErr %v", err, tt.wantErr) diff --git a/pkg/sqlite/database.go b/pkg/sqlite/database.go index fce8190d8..a87f6706f 100644 --- a/pkg/sqlite/database.go +++ b/pkg/sqlite/database.go @@ -34,7 +34,7 @@ const ( cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE" ) -var appSchemaVersion uint = 72 +var appSchemaVersion uint = 76 //go:embed migrations/*.sql var migrationsBox embed.FS @@ -350,12 +350,30 @@ func (db *Database) Backup(backupPath string) (err error) { defer thisDB.Close() } - logger.Infof("Backing up database into: %s", backupPath) - _, err = thisDB.Exec(`VACUUM INTO "` + backupPath + `"`) + // if backup path is not in the same directory as the database, + // then backup to the same directory first, then move to the final location. + // This is to prevent errors if the backup directory is over a network share. + dbDir := filepath.Dir(db.dbPath) + moveAfter := filepath.Dir(backupPath) != dbDir + vacuumOut := backupPath + if moveAfter { + vacuumOut = filepath.Join(dbDir, filepath.Base(backupPath)) + } + + logger.Infof("Backing up database into: %s", vacuumOut) + _, err = thisDB.Exec(`VACUUM INTO "` + vacuumOut + `"`) if err != nil { return fmt.Errorf("vacuum failed: %w", err) } + if moveAfter { + logger.Infof("Moving database backup to: %s", backupPath) + err = fsutil.SafeMove(vacuumOut, backupPath) + if err != nil { + return fmt.Errorf("moving database backup failed: %w", err) + } + } + return nil } diff --git a/pkg/sqlite/date.go b/pkg/sqlite/date.go index ec41b612c..522fe7cb0 100644 --- a/pkg/sqlite/date.go +++ b/pkg/sqlite/date.go @@ -5,6 +5,7 @@ import ( "time" "github.com/stashapp/stash/pkg/models" + "gopkg.in/guregu/null.v4" ) const sqliteDateLayout = "2006-01-02" @@ -54,12 +55,12 @@ func (d NullDate) Value() (driver.Value, error) { return d.Date.Format(sqliteDateLayout), nil } -func (d *NullDate) DatePtr() *models.Date { +func (d *NullDate) DatePtr(precision null.Int) *models.Date { if d == nil || !d.Valid { return nil } - return &models.Date{Time: d.Date} + return &models.Date{Time: d.Date, Precision: models.DatePrecision(precision.Int64)} } func NullDateFromDatePtr(d *models.Date) NullDate { @@ -68,3 +69,11 @@ func NullDateFromDatePtr(d *models.Date) NullDate { } return NullDate{Date: d.Time, Valid: true} } + +func datePrecisionFromDatePtr(d *models.Date) null.Int { + if d == nil { + // default to day precision + return null.Int{} + } + return null.IntFrom(int64(d.Precision)) +} diff --git a/pkg/sqlite/file.go b/pkg/sqlite/file.go index ad3442ff7..1be5648b4 100644 --- a/pkg/sqlite/file.go +++ b/pkg/sqlite/file.go @@ -285,7 +285,7 @@ type fileRepositoryType struct { var ( fileRepository = fileRepositoryType{ repository: repository{ - tableName: sceneTable, + tableName: fileTable, idColumn: idColumn, }, scenes: joinRepository{ @@ -625,9 +625,9 @@ func (qb *FileStore) find(ctx context.Context, id models.FileID) (models.File, e } // FindByPath returns the first file that matches the given path. Wildcard characters are supported. -func (qb *FileStore) FindByPath(ctx context.Context, p string) (models.File, error) { +func (qb *FileStore) FindByPath(ctx context.Context, p string, caseSensitive bool) (models.File, error) { - ret, err := qb.FindAllByPath(ctx, p) + ret, err := qb.FindAllByPath(ctx, p, caseSensitive) if err != nil { return nil, err @@ -642,7 +642,7 @@ func (qb *FileStore) FindByPath(ctx context.Context, p string) (models.File, err // FindAllByPath returns all the files that match the given path. // Wildcard characters are supported. -func (qb *FileStore) FindAllByPath(ctx context.Context, p string) ([]models.File, error) { +func (qb *FileStore) FindAllByPath(ctx context.Context, p string, caseSensitive bool) ([]models.File, error) { // separate basename from path basename := filepath.Base(p) dirName := filepath.Dir(p) @@ -657,7 +657,7 @@ func (qb *FileStore) FindAllByPath(ctx context.Context, p string) ([]models.File // like uses case-insensitive matching. Only use like if wildcards are used q := qb.selectDataset().Prepared(true) - if strings.Contains(basename, "%") || strings.Contains(dirName, "%") { + if strings.Contains(basename, "%") || strings.Contains(dirName, "%") || !caseSensitive { q = q.Where( folderTable.Col("path").Like(dirName), table.Col("basename").Like(basename), diff --git a/pkg/sqlite/file_filter.go b/pkg/sqlite/file_filter.go index 60ca01648..12c7ba3d5 100644 --- a/pkg/sqlite/file_filter.go +++ b/pkg/sqlite/file_filter.go @@ -10,6 +10,8 @@ import ( type fileFilterHandler struct { fileFilter *models.FileFilterType + // if true, don't allow use of related filters + isRelated bool } func (qb *fileFilterHandler) validate() error { @@ -22,8 +24,12 @@ func (qb *fileFilterHandler) validate() error { return err } + if qb.isRelated && (fileFilter.ScenesFilter != nil || fileFilter.ImagesFilter != nil || fileFilter.GalleriesFilter != nil) { + return fmt.Errorf("cannot use related filters inside a related filter") + } + if subFilter := fileFilter.SubFilter(); subFilter != nil { - sqb := &fileFilterHandler{fileFilter: subFilter} + sqb := &fileFilterHandler{fileFilter: subFilter, isRelated: qb.isRelated} if err := sqb.validate(); err != nil { return err } @@ -45,7 +51,7 @@ func (qb *fileFilterHandler) handle(ctx context.Context, f *filterBuilder) { sf := fileFilter.SubFilter() if sf != nil { - sub := &fileFilterHandler{sf} + sub := &fileFilterHandler{sf, qb.isRelated} handleSubFilter(ctx, sub, f, fileFilter.OperatorFilter) } diff --git a/pkg/sqlite/file_test.go b/pkg/sqlite/file_test.go index 766ffcc70..8422390c0 100644 --- a/pkg/sqlite/file_test.go +++ b/pkg/sqlite/file_test.go @@ -551,7 +551,7 @@ func Test_FileStore_FindByPath(t *testing.T) { for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) - got, err := qb.FindByPath(ctx, tt.path) + got, err := qb.FindByPath(ctx, tt.path, true) if (err != nil) != tt.wantErr { t.Errorf("FileStore.FindByPath() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/pkg/sqlite/filter.go b/pkg/sqlite/filter.go index 143487af2..fa6759ae6 100644 --- a/pkg/sqlite/filter.go +++ b/pkg/sqlite/filter.go @@ -96,6 +96,9 @@ type join struct { onClause string joinType string args []interface{} + + // if true, indicates this is required for sorting only + sort bool } // equals returns true if the other join alias/table is equal to this one @@ -127,30 +130,45 @@ func (j join) toSQL() string { type joins []join +// addUnique only adds if not already present +// returns true if added +func (j *joins) addUnique(newJoin join) bool { + found := false + for i, jj := range *j { + if jj.equals(newJoin) { + found = true + // if sort is false on the new join, but true on the existing, set the false + if !newJoin.sort && jj.sort { + (*j)[i].sort = false + } + break + } + } + + if !found { + *j = append(*j, newJoin) + } + return !found +} + func (j *joins) add(newJoins ...join) { // only add if not already joined for _, newJoin := range newJoins { - found := false - for _, jj := range *j { - if jj.equals(newJoin) { - found = true - break - } - } - - if !found { - *j = append(*j, newJoin) - } + j.addUnique(newJoin) } } -func (j *joins) toSQL() string { +func (j *joins) toSQL(includeSortPagination bool) string { if len(*j) == 0 { return "" } var ret []string for _, jj := range *j { + // skip sort-only joins if not including sort/pagination + if !includeSortPagination && jj.sort { + continue + } ret = append(ret, jj.toSQL()) } diff --git a/pkg/sqlite/folder.go b/pkg/sqlite/folder.go index 3ac962b8b..f250f7861 100644 --- a/pkg/sqlite/folder.go +++ b/pkg/sqlite/folder.go @@ -292,8 +292,16 @@ func (qb *FolderStore) FindMany(ctx context.Context, ids []models.FolderID) ([]* return folders, nil } -func (qb *FolderStore) FindByPath(ctx context.Context, p string) (*models.Folder, error) { - q := qb.selectDataset().Prepared(true).Where(qb.table().Col("path").Eq(p)) +func (qb *FolderStore) FindByPath(ctx context.Context, p string, caseSensitive bool) (*models.Folder, error) { + // use like for case insensitive search + var criterion exp.BooleanExpression + if caseSensitive { + criterion = qb.table().Col("path").Eq(p) + } else { + criterion = qb.table().Col("path").ILike(p) + } + + q := qb.selectDataset().Prepared(true).Where(criterion) ret, err := qb.get(ctx, q) if err != nil && !errors.Is(err, sql.ErrNoRows) { diff --git a/pkg/sqlite/folder_filter.go b/pkg/sqlite/folder_filter.go index 2fda0d1e3..6b2bd96e9 100644 --- a/pkg/sqlite/folder_filter.go +++ b/pkg/sqlite/folder_filter.go @@ -9,6 +9,8 @@ import ( type folderFilterHandler struct { folderFilter *models.FolderFilterType + table sqlTable + isRelated bool } func (qb *folderFilterHandler) validate() error { @@ -21,8 +23,12 @@ func (qb *folderFilterHandler) validate() error { return err } + if qb.isRelated && (folderFilter.GalleriesFilter != nil) { + return fmt.Errorf("cannot use related filters inside a related filter") + } + if subFilter := folderFilter.SubFilter(); subFilter != nil { - sqb := &folderFilterHandler{folderFilter: subFilter} + sqb := &folderFilterHandler{folderFilter: subFilter, isRelated: qb.isRelated} if err := sqb.validate(); err != nil { return err } @@ -44,7 +50,7 @@ func (qb *folderFilterHandler) handle(ctx context.Context, f *filterBuilder) { sf := folderFilter.SubFilter() if sf != nil { - sub := &folderFilterHandler{sf} + sub := &folderFilterHandler{folderFilter: sf, table: qb.table} handleSubFilter(ctx, sub, f, folderFilter.OperatorFilter) } @@ -52,25 +58,29 @@ func (qb *folderFilterHandler) handle(ctx context.Context, f *filterBuilder) { } func (qb *folderFilterHandler) criterionHandler() criterionHandler { + if qb.table == "" { + qb.table = folderTable + } + folderFilter := qb.folderFilter return compoundHandler{ - stringCriterionHandler(folderFilter.Path, "folders.path"), - ×tampCriterionHandler{folderFilter.ModTime, "folders.mod_time", nil}, + stringCriterionHandler(folderFilter.Path, qb.table.Col("path")), + ×tampCriterionHandler{folderFilter.ModTime, qb.table.Col("mod_time"), nil}, qb.parentFolderCriterionHandler(folderFilter.ParentFolder), qb.zipFileCriterionHandler(folderFilter.ZipFile), qb.galleryCountCriterionHandler(folderFilter.GalleryCount), - ×tampCriterionHandler{folderFilter.CreatedAt, "folders.created_at", nil}, - ×tampCriterionHandler{folderFilter.UpdatedAt, "folders.updated_at", nil}, + ×tampCriterionHandler{folderFilter.CreatedAt, qb.table.Col("created_at"), nil}, + ×tampCriterionHandler{folderFilter.UpdatedAt, qb.table.Col("updated_at"), nil}, &relatedFilterHandler{ - relatedIDCol: "galleries.id", + relatedIDCol: qb.table.Col("id"), relatedRepo: galleryRepository.repository, relatedHandler: &galleryFilterHandler{folderFilter.GalleriesFilter}, joinFn: func(f *filterBuilder) { - folderRepository.galleries.innerJoin(f, "", "folders.id") + folderRepository.galleries.innerJoin(f, "", qb.table.Col("id")) }, }, } @@ -85,7 +95,7 @@ func (qb *folderFilterHandler) zipFileCriterionHandler(criterion *models.MultiCr notClause = "NOT" } - f.addWhere(fmt.Sprintf("folders.zip_file_id IS %s NULL", notClause)) + f.addWhere(fmt.Sprintf("%s.zip_file_id IS %s NULL", qb.table.Name(), notClause)) return } @@ -102,9 +112,9 @@ func (qb *folderFilterHandler) zipFileCriterionHandler(criterion *models.MultiCr havingClause := "" switch criterion.Modifier { case models.CriterionModifierIncludes: - whereClause = "folders.zip_file_id IN " + getInBinding(len(criterion.Value)) + whereClause = fmt.Sprintf("%s.zip_file_id IN %s", qb.table.Name(), getInBinding(len(criterion.Value))) case models.CriterionModifierExcludes: - whereClause = "folders.zip_file_id NOT IN " + getInBinding(len(criterion.Value)) + whereClause = fmt.Sprintf("%s.zip_file_id NOT IN %s", qb.table.Name(), getInBinding(len(criterion.Value))) } f.addWhere(whereClause, args...) @@ -128,8 +138,8 @@ func (qb *folderFilterHandler) parentFolderCriterionHandler(folder *models.Hiera } hh := hierarchicalMultiCriterionHandlerBuilder{ - primaryTable: folderTable, - foreignTable: folderTable, + primaryTable: qb.table.Name(), + foreignTable: qb.table.Name(), foreignFK: "parent_folder_id", parentFK: "parent_folder_id", } diff --git a/pkg/sqlite/folder_test.go b/pkg/sqlite/folder_test.go index 1d948d063..15b2b96b8 100644 --- a/pkg/sqlite/folder_test.go +++ b/pkg/sqlite/folder_test.go @@ -89,7 +89,7 @@ func Test_FolderStore_Create(t *testing.T) { assert.Equal(copy, s) // ensure can find the folder - found, err := qb.FindByPath(ctx, path) + found, err := qb.FindByPath(ctx, path, true) if err != nil { t.Errorf("FolderStore.Find() error = %v", err) } @@ -180,7 +180,7 @@ func Test_FolderStore_Update(t *testing.T) { return } - s, err := qb.FindByPath(ctx, path) + s, err := qb.FindByPath(ctx, path, true) if err != nil { t.Errorf("FolderStore.Find() error = %v", err) } @@ -228,7 +228,7 @@ func Test_FolderStore_FindByPath(t *testing.T) { for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { - got, err := qb.FindByPath(ctx, tt.path) + got, err := qb.FindByPath(ctx, tt.path, true) if (err != nil) != tt.wantErr { t.Errorf("FolderStore.FindByPath() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/pkg/sqlite/gallery.go b/pkg/sqlite/gallery.go index ec9b7ae2e..41729057b 100644 --- a/pkg/sqlite/gallery.go +++ b/pkg/sqlite/gallery.go @@ -30,12 +30,13 @@ const ( ) type galleryRow struct { - ID int `db:"id" goqu:"skipinsert"` - Title zero.String `db:"title"` - Code zero.String `db:"code"` - Date NullDate `db:"date"` - Details zero.String `db:"details"` - Photographer zero.String `db:"photographer"` + ID int `db:"id" goqu:"skipinsert"` + Title zero.String `db:"title"` + Code zero.String `db:"code"` + Date NullDate `db:"date"` + DatePrecision null.Int `db:"date_precision"` + Details zero.String `db:"details"` + Photographer zero.String `db:"photographer"` // expressed as 1-100 Rating null.Int `db:"rating"` Organized bool `db:"organized"` @@ -50,6 +51,7 @@ func (r *galleryRow) fromGallery(o models.Gallery) { r.Title = zero.StringFrom(o.Title) r.Code = zero.StringFrom(o.Code) r.Date = NullDateFromDatePtr(o.Date) + r.DatePrecision = datePrecisionFromDatePtr(o.Date) r.Details = zero.StringFrom(o.Details) r.Photographer = zero.StringFrom(o.Photographer) r.Rating = intFromPtr(o.Rating) @@ -74,7 +76,7 @@ func (r *galleryQueryRow) resolve() *models.Gallery { ID: r.ID, Title: r.Title.String, Code: r.Code.String, - Date: r.Date.DatePtr(), + Date: r.Date.DatePtr(r.DatePrecision), Details: r.Details.String, Photographer: r.Photographer.String, Rating: nullIntPtr(r.Rating), @@ -102,7 +104,7 @@ type galleryRowRecord struct { func (r *galleryRowRecord) fromPartial(o models.GalleryPartial) { r.setNullString("title", o.Title) r.setNullString("code", o.Code) - r.setNullDate("date", o.Date) + r.setNullDate("date", "date_precision", o.Date) r.setNullString("details", o.Details) r.setNullString("photographer", o.Photographer) r.setNullInt("rating", o.Rating) @@ -155,7 +157,7 @@ var ( }, fkColumn: "tag_id", foreignTable: tagTable, - orderBy: "COALESCE(tags.sort_name, tags.name) ASC", + orderBy: tagTableSortSQL, }, images: joinRepository{ repository: repository{ @@ -800,10 +802,12 @@ func (qb *GalleryStore) setGallerySort(query *queryBuilder, findFilter *models.F addFileTable := func() { query.addJoins( join{ + sort: true, table: galleriesFilesTable, onClause: "galleries_files.gallery_id = galleries.id", }, join{ + sort: true, table: fileTable, onClause: "galleries_files.file_id = files.id", }, @@ -813,10 +817,12 @@ func (qb *GalleryStore) setGallerySort(query *queryBuilder, findFilter *models.F addFolderTable := func() { query.addJoins( join{ + sort: true, table: folderTable, onClause: "folders.id = galleries.folder_id", }, join{ + sort: true, table: folderTable, as: "file_folder", onClause: "files.parent_folder_id = file_folder.id", diff --git a/pkg/sqlite/gallery_filter.go b/pkg/sqlite/gallery_filter.go index 18718c511..f05ff7b81 100644 --- a/pkg/sqlite/gallery_filter.go +++ b/pkg/sqlite/gallery_filter.go @@ -146,6 +146,36 @@ func (qb *galleryFilterHandler) criterionHandler() criterionHandler { galleryRepository.tags.innerJoin(f, "gallery_tag", "galleries.id") }, }, + + &relatedFilterHandler{ + relatedIDCol: "files.id", + relatedRepo: fileRepository.repository, + relatedHandler: &fileFilterHandler{ + fileFilter: filter.FilesFilter, + isRelated: true, + }, + joinFn: func(f *filterBuilder) { + galleryRepository.addFilesTable(f) + galleryRepository.addFoldersTable(f) + }, + // don't use a subquery; join directly + directJoin: true, + }, + + &relatedFilterHandler{ + relatedIDCol: "gallery_folder.id", + relatedRepo: folderRepository.repository, + relatedHandler: &folderFilterHandler{ + folderFilter: filter.FoldersFilter, + table: "gallery_folder", + isRelated: true, + }, + joinFn: func(f *filterBuilder) { + f.addLeftJoin(folderTable, "gallery_folder", "galleries.folder_id = gallery_folder.id") + }, + // don't use a subquery; join directly + directJoin: true, + }, } } diff --git a/pkg/sqlite/gallery_test.go b/pkg/sqlite/gallery_test.go index be1edb687..06d7daf17 100644 --- a/pkg/sqlite/gallery_test.go +++ b/pkg/sqlite/gallery_test.go @@ -481,7 +481,7 @@ func Test_galleryQueryBuilder_UpdatePartial(t *testing.T) { CreatedAt: createdAt, UpdatedAt: updatedAt, SceneIDs: models.NewRelatedIDs([]int{sceneIDs[sceneIdxWithGallery]}), - TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithGallery], tagIDs[tagIdx1WithDupName]}), + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithGallery]}), PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithGallery], performerIDs[performerIdx1WithDupName]}), }, false, diff --git a/pkg/sqlite/group.go b/pkg/sqlite/group.go index 7f0ff72ca..b216335b8 100644 --- a/pkg/sqlite/group.go +++ b/pkg/sqlite/group.go @@ -32,11 +32,12 @@ const ( ) type groupRow struct { - ID int `db:"id" goqu:"skipinsert"` - Name zero.String `db:"name"` - Aliases zero.String `db:"aliases"` - Duration null.Int `db:"duration"` - Date NullDate `db:"date"` + ID int `db:"id" goqu:"skipinsert"` + Name zero.String `db:"name"` + Aliases zero.String `db:"aliases"` + Duration null.Int `db:"duration"` + Date NullDate `db:"date"` + DatePrecision null.Int `db:"date_precision"` // expressed as 1-100 Rating null.Int `db:"rating"` StudioID null.Int `db:"studio_id,omitempty"` @@ -56,6 +57,7 @@ func (r *groupRow) fromGroup(o models.Group) { r.Aliases = zero.StringFrom(o.Aliases) r.Duration = intFromPtr(o.Duration) r.Date = NullDateFromDatePtr(o.Date) + r.DatePrecision = datePrecisionFromDatePtr(o.Date) r.Rating = intFromPtr(o.Rating) r.StudioID = intFromPtr(o.StudioID) r.Director = zero.StringFrom(o.Director) @@ -70,7 +72,7 @@ func (r *groupRow) resolve() *models.Group { Name: r.Name.String, Aliases: r.Aliases.String, Duration: nullIntPtr(r.Duration), - Date: r.Date.DatePtr(), + Date: r.Date.DatePtr(r.DatePrecision), Rating: nullIntPtr(r.Rating), StudioID: nullIntPtr(r.StudioID), Director: r.Director.String, @@ -90,7 +92,7 @@ func (r *groupRowRecord) fromPartial(o models.GroupPartial) { r.setNullString("name", o.Name) r.setNullString("aliases", o.Aliases) r.setNullInt("duration", o.Duration) - r.setNullDate("date", o.Date) + r.setNullDate("date", "date_precision", o.Date) r.setNullInt("rating", o.Rating) r.setNullInt("studio_id", o.StudioID) r.setNullString("director", o.Director) @@ -122,7 +124,7 @@ var ( }, fkColumn: tagIDColumn, foreignTable: tagTable, - orderBy: "COALESCE(tags.sort_name, tags.name) ASC", + orderBy: tagTableSortSQL, }, } ) @@ -488,6 +490,7 @@ var groupSortOptions = sortOptions{ "random", "rating", "scenes_count", + "o_counter", "sub_group_order", "tag_count", "updated_at", @@ -517,13 +520,15 @@ func (qb *GroupStore) setGroupSort(query *queryBuilder, findFilter *models.FindF } else { // this will give unexpected results if the query is not filtered by a parent group and // the group has multiple parents and order indexes - query.join(groupRelationsTable, "", "groups.id = groups_relations.sub_id") + query.joinSort(groupRelationsTable, "", "groups.id = groups_relations.sub_id") query.sortAndPagination += getSort("order_index", direction, groupRelationsTable) } case "tag_count": query.sortAndPagination += getCountSort(groupTable, groupsTagsTable, groupIDColumn, direction) case "scenes_count": // generic getSort won't work for this query.sortAndPagination += getCountSort(groupTable, groupsScenesTable, groupIDColumn, direction) + case "o_counter": + query.sortAndPagination += qb.sortByOCounter(direction) default: query.sortAndPagination += getSort(sort, direction, "groups") } @@ -701,3 +706,8 @@ func (qb *GroupStore) FindInAncestors(ctx context.Context, ascestorIDs []int, id return ret, nil } + +func (qb *GroupStore) sortByOCounter(direction string) string { + // need to sum the o_counter from scenes and images + return " ORDER BY (" + selectGroupOCountSQL + ") " + direction +} diff --git a/pkg/sqlite/group_filter.go b/pkg/sqlite/group_filter.go index dcb7bcdfc..f29023785 100644 --- a/pkg/sqlite/group_filter.go +++ b/pkg/sqlite/group_filter.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/stashapp/stash/pkg/models" + "github.com/stashapp/stash/pkg/utils" ) type groupFilterHandler struct { @@ -73,6 +74,7 @@ func (qb *groupFilterHandler) criterionHandler() criterionHandler { qb.performersCriterionHandler(groupFilter.Performers), qb.tagsCriterionHandler(groupFilter.Tags), qb.tagCountCriterionHandler(groupFilter.TagCount), + qb.groupOCounterCriterionHandler(groupFilter.OCounter), &dateCriterionHandler{groupFilter.Date, "groups.date", nil}, groupHierarchyHandler.ParentsCriterionHandler(groupFilter.ContainingGroups), groupHierarchyHandler.ChildrenCriterionHandler(groupFilter.SubGroups), @@ -201,3 +203,37 @@ func (qb *groupFilterHandler) tagCountCriterionHandler(count *models.IntCriterio return h.handler(count) } + +// used for sorting and filtering on group o-count +var selectGroupOCountSQL = utils.StrFormat( + "SELECT SUM(o_counter) "+ + "FROM ("+ + "SELECT COUNT({scenes_o_dates}.{o_date}) as o_counter from {groups_scenes} s "+ + "LEFT JOIN {scenes} ON {scenes}.id = s.{scene_id} "+ + "LEFT JOIN {scenes_o_dates} ON {scenes_o_dates}.{scene_id} = {scenes}.id "+ + "WHERE s.{group_id} = {group}.id "+ + ")", + map[string]interface{}{ + "group": groupTable, + "group_id": groupIDColumn, + "groups_scenes": groupsScenesTable, + "scenes": sceneTable, + "scene_id": sceneIDColumn, + "scenes_o_dates": scenesODatesTable, + "o_date": sceneODateColumn, + }, +) + +func (qb *groupFilterHandler) groupOCounterCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc { + return func(ctx context.Context, f *filterBuilder) { + if count == nil { + return + } + + lhs := "(" + selectGroupOCountSQL + ")" + clause, args := getIntCriterionWhereClause(lhs, *count) + + f.addWhere(clause, args...) + } + +} diff --git a/pkg/sqlite/image.go b/pkg/sqlite/image.go index 840720c50..bcaf3f42f 100644 --- a/pkg/sqlite/image.go +++ b/pkg/sqlite/image.go @@ -34,15 +34,16 @@ type imageRow struct { Title zero.String `db:"title"` Code zero.String `db:"code"` // expressed as 1-100 - Rating null.Int `db:"rating"` - Date NullDate `db:"date"` - Details zero.String `db:"details"` - Photographer zero.String `db:"photographer"` - Organized bool `db:"organized"` - OCounter int `db:"o_counter"` - StudioID null.Int `db:"studio_id,omitempty"` - CreatedAt Timestamp `db:"created_at"` - UpdatedAt Timestamp `db:"updated_at"` + Rating null.Int `db:"rating"` + Date NullDate `db:"date"` + DatePrecision null.Int `db:"date_precision"` + Details zero.String `db:"details"` + Photographer zero.String `db:"photographer"` + Organized bool `db:"organized"` + OCounter int `db:"o_counter"` + StudioID null.Int `db:"studio_id,omitempty"` + CreatedAt Timestamp `db:"created_at"` + UpdatedAt Timestamp `db:"updated_at"` } func (r *imageRow) fromImage(i models.Image) { @@ -51,6 +52,7 @@ func (r *imageRow) fromImage(i models.Image) { r.Code = zero.StringFrom(i.Code) r.Rating = intFromPtr(i.Rating) r.Date = NullDateFromDatePtr(i.Date) + r.DatePrecision = datePrecisionFromDatePtr(i.Date) r.Details = zero.StringFrom(i.Details) r.Photographer = zero.StringFrom(i.Photographer) r.Organized = i.Organized @@ -74,7 +76,7 @@ func (r *imageQueryRow) resolve() *models.Image { Title: r.Title.String, Code: r.Code.String, Rating: nullIntPtr(r.Rating), - Date: r.Date.DatePtr(), + Date: r.Date.DatePtr(r.DatePrecision), Details: r.Details.String, Photographer: r.Photographer.String, Organized: r.Organized, @@ -103,7 +105,7 @@ func (r *imageRowRecord) fromPartial(i models.ImagePartial) { r.setNullString("title", i.Title) r.setNullString("code", i.Code) r.setNullInt("rating", i.Rating) - r.setNullDate("date", i.Date) + r.setNullDate("date", "date_precision", i.Date) r.setNullString("details", i.Details) r.setNullString("photographer", i.Photographer) r.setBool("organized", i.Organized) @@ -177,7 +179,7 @@ var ( }, fkColumn: tagIDColumn, foreignTable: tagTable, - orderBy: "COALESCE(tags.sort_name, tags.name) ASC", + orderBy: tagTableSortSQL, }, } ) @@ -682,6 +684,20 @@ func (qb *ImageStore) OCountByPerformerID(ctx context.Context, performerID int) return ret, nil } +func (qb *ImageStore) OCountByStudioID(ctx context.Context, studioID int) (int, error) { + table := qb.table() + q := dialect.Select(goqu.COALESCE(goqu.SUM("o_counter"), 0)).From(table).Where( + table.Col(studioIDColumn).Eq(studioID), + ) + + var ret int + if err := querySimple(ctx, q, &ret); err != nil { + return 0, err + } + + return ret, nil +} + func (qb *ImageStore) OCount(ctx context.Context) (int, error) { table := qb.table() @@ -926,6 +942,7 @@ var imageSortOptions = sortOptions{ "performer_count", "random", "rating", + "resolution", "tag_count", "title", "updated_at", @@ -951,10 +968,12 @@ func (qb *ImageStore) setImageSortAndPagination(q *queryBuilder, findFilter *mod addFilesJoin := func() { q.addJoins( join{ + sort: true, table: imagesFilesTable, onClause: "images_files.image_id = images.id", }, join{ + sort: true, table: fileTable, onClause: "images_files.file_id = files.id", }, @@ -963,6 +982,7 @@ func (qb *ImageStore) setImageSortAndPagination(q *queryBuilder, findFilter *mod addFolderJoin := func() { q.addJoins(join{ + sort: true, table: folderTable, onClause: "files.parent_folder_id = folders.id", }) @@ -982,6 +1002,14 @@ func (qb *ImageStore) setImageSortAndPagination(q *queryBuilder, findFilter *mod case "mod_time", "filesize": addFilesJoin() sortClause = getSort(sort, direction, "files") + case "resolution": + addFilesJoin() + q.addJoins(join{ + sort: true, + table: imageFileTable, + onClause: "images_files.file_id = image_files.file_id", + }) + sortClause = " ORDER BY MIN(image_files.width, image_files.height) " + direction case "title": addFilesJoin() addFolderJoin() diff --git a/pkg/sqlite/image_filter.go b/pkg/sqlite/image_filter.go index 8f2d5d6b9..b56ade26d 100644 --- a/pkg/sqlite/image_filter.go +++ b/pkg/sqlite/image_filter.go @@ -62,6 +62,15 @@ func (qb *imageFilterHandler) criterionHandler() criterionHandler { stringCriterionHandler(imageFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f) }), + + &phashDistanceCriterionHandler{ + joinFn: func(f *filterBuilder) { + imageRepository.addImagesFilesTable(f) + f.addLeftJoin(fingerprintTable, "fingerprints_phash", "images_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'") + }, + criterion: imageFilter.PhashDistance, + }, + stringCriterionHandler(imageFilter.Title, "images.title"), stringCriterionHandler(imageFilter.Code, "images.code"), stringCriterionHandler(imageFilter.Details, "images.details"), @@ -123,6 +132,21 @@ func (qb *imageFilterHandler) criterionHandler() criterionHandler { imageRepository.tags.innerJoin(f, "image_tag", "images.id") }, }, + + &relatedFilterHandler{ + relatedIDCol: "files.id", + relatedRepo: fileRepository.repository, + relatedHandler: &fileFilterHandler{ + fileFilter: imageFilter.FilesFilter, + isRelated: true, + }, + joinFn: func(f *filterBuilder) { + imageRepository.addFilesTable(f) + imageRepository.addFoldersTable(f) + }, + // don't use a subquery; join directly + directJoin: true, + }, } } diff --git a/pkg/sqlite/migrations/73_studio_urls.up.sql b/pkg/sqlite/migrations/73_studio_urls.up.sql new file mode 100644 index 000000000..c356713c0 --- /dev/null +++ b/pkg/sqlite/migrations/73_studio_urls.up.sql @@ -0,0 +1,24 @@ +CREATE TABLE `studio_urls` ( + `studio_id` integer NOT NULL, + `position` integer NOT NULL, + `url` varchar(255) NOT NULL, + foreign key(`studio_id`) references `studios`(`id`) on delete CASCADE, + PRIMARY KEY(`studio_id`, `position`, `url`) +); + +CREATE INDEX `studio_urls_url` on `studio_urls` (`url`); + +INSERT INTO `studio_urls` + ( + `studio_id`, + `position`, + `url` + ) + SELECT + `id`, + '0', + `url` + FROM `studios` + WHERE `studios`.`url` IS NOT NULL AND `studios`.`url` != ''; + +ALTER TABLE `studios` DROP COLUMN `url`; diff --git a/pkg/sqlite/migrations/74_tag_stash_ids.up.sql b/pkg/sqlite/migrations/74_tag_stash_ids.up.sql new file mode 100644 index 000000000..c281149c7 --- /dev/null +++ b/pkg/sqlite/migrations/74_tag_stash_ids.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE `tag_stash_ids` ( + `tag_id` integer, + `endpoint` varchar(255), + `stash_id` varchar(36), + `updated_at` datetime not null default '1970-01-01T00:00:00Z', + foreign key(`tag_id`) references `tags`(`id`) on delete CASCADE +); \ No newline at end of file diff --git a/pkg/sqlite/migrations/75_date_precision.up.sql b/pkg/sqlite/migrations/75_date_precision.up.sql new file mode 100644 index 000000000..ce35bf170 --- /dev/null +++ b/pkg/sqlite/migrations/75_date_precision.up.sql @@ -0,0 +1,13 @@ +ALTER TABLE "scenes" ADD COLUMN "date_precision" TINYINT; +ALTER TABLE "images" ADD COLUMN "date_precision" TINYINT; +ALTER TABLE "galleries" ADD COLUMN "date_precision" TINYINT; +ALTER TABLE "groups" ADD COLUMN "date_precision" TINYINT; +ALTER TABLE "performers" ADD COLUMN "birthdate_precision" TINYINT; +ALTER TABLE "performers" ADD COLUMN "death_date_precision" TINYINT; + +UPDATE "scenes" SET "date_precision" = 0 WHERE "date" IS NOT NULL; +UPDATE "images" SET "date_precision" = 0 WHERE "date" IS NOT NULL; +UPDATE "galleries" SET "date_precision" = 0 WHERE "date" IS NOT NULL; +UPDATE "groups" SET "date_precision" = 0 WHERE "date" IS NOT NULL; +UPDATE "performers" SET "birthdate_precision" = 0 WHERE "birthdate" IS NOT NULL; +UPDATE "performers" SET "death_date_precision" = 0 WHERE "death_date" IS NOT NULL; diff --git a/pkg/sqlite/migrations/76_studio_custom_fields.up.sql b/pkg/sqlite/migrations/76_studio_custom_fields.up.sql new file mode 100644 index 000000000..81a72d4d4 --- /dev/null +++ b/pkg/sqlite/migrations/76_studio_custom_fields.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `studio_custom_fields` ( + `studio_id` integer NOT NULL, + `field` varchar(64) NOT NULL, + `value` BLOB NOT NULL, + PRIMARY KEY (`studio_id`, `field`), + foreign key(`studio_id`) references `studios`(`id`) on delete CASCADE +); + +CREATE INDEX `index_studio_custom_fields_field_value` ON `studio_custom_fields` (`field`, `value`); diff --git a/pkg/sqlite/migrations/README.md b/pkg/sqlite/migrations/README.md new file mode 100644 index 000000000..f0abb9bc0 --- /dev/null +++ b/pkg/sqlite/migrations/README.md @@ -0,0 +1,7 @@ +# Creating a migration + +1. Create new migration file in the migrations directory with the format `NN_description.up.sql`, where `NN` is the next sequential number. + +2. Update `pkg/sqlite/database.go` to update the `appSchemaVersion` value to the new migration number. + +For migrations requiring complex logic or config file changes, see existing custom migrations for examples. \ No newline at end of file diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index bcb984ffd..bc4461f5f 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -30,32 +30,34 @@ const ( ) type performerRow struct { - ID int `db:"id" goqu:"skipinsert"` - Name null.String `db:"name"` // TODO: make schema non-nullable - Disambigation zero.String `db:"disambiguation"` - Gender zero.String `db:"gender"` - Birthdate NullDate `db:"birthdate"` - Ethnicity zero.String `db:"ethnicity"` - Country zero.String `db:"country"` - EyeColor zero.String `db:"eye_color"` - Height null.Int `db:"height"` - Measurements zero.String `db:"measurements"` - FakeTits zero.String `db:"fake_tits"` - PenisLength null.Float `db:"penis_length"` - Circumcised zero.String `db:"circumcised"` - CareerLength zero.String `db:"career_length"` - Tattoos zero.String `db:"tattoos"` - Piercings zero.String `db:"piercings"` - Favorite bool `db:"favorite"` - CreatedAt Timestamp `db:"created_at"` - UpdatedAt Timestamp `db:"updated_at"` + ID int `db:"id" goqu:"skipinsert"` + Name null.String `db:"name"` // TODO: make schema non-nullable + Disambigation zero.String `db:"disambiguation"` + Gender zero.String `db:"gender"` + Birthdate NullDate `db:"birthdate"` + BirthdatePrecision null.Int `db:"birthdate_precision"` + Ethnicity zero.String `db:"ethnicity"` + Country zero.String `db:"country"` + EyeColor zero.String `db:"eye_color"` + Height null.Int `db:"height"` + Measurements zero.String `db:"measurements"` + FakeTits zero.String `db:"fake_tits"` + PenisLength null.Float `db:"penis_length"` + Circumcised zero.String `db:"circumcised"` + CareerLength zero.String `db:"career_length"` + Tattoos zero.String `db:"tattoos"` + Piercings zero.String `db:"piercings"` + Favorite bool `db:"favorite"` + CreatedAt Timestamp `db:"created_at"` + UpdatedAt Timestamp `db:"updated_at"` // expressed as 1-100 - Rating null.Int `db:"rating"` - Details zero.String `db:"details"` - DeathDate NullDate `db:"death_date"` - HairColor zero.String `db:"hair_color"` - Weight null.Int `db:"weight"` - IgnoreAutoTag bool `db:"ignore_auto_tag"` + Rating null.Int `db:"rating"` + Details zero.String `db:"details"` + DeathDate NullDate `db:"death_date"` + DeathDatePrecision null.Int `db:"death_date_precision"` + HairColor zero.String `db:"hair_color"` + Weight null.Int `db:"weight"` + IgnoreAutoTag bool `db:"ignore_auto_tag"` // not used in resolution or updates ImageBlob zero.String `db:"image_blob"` @@ -69,6 +71,7 @@ func (r *performerRow) fromPerformer(o models.Performer) { r.Gender = zero.StringFrom(o.Gender.String()) } r.Birthdate = NullDateFromDatePtr(o.Birthdate) + r.BirthdatePrecision = datePrecisionFromDatePtr(o.Birthdate) r.Ethnicity = zero.StringFrom(o.Ethnicity) r.Country = zero.StringFrom(o.Country) r.EyeColor = zero.StringFrom(o.EyeColor) @@ -88,6 +91,7 @@ func (r *performerRow) fromPerformer(o models.Performer) { r.Rating = intFromPtr(o.Rating) r.Details = zero.StringFrom(o.Details) r.DeathDate = NullDateFromDatePtr(o.DeathDate) + r.DeathDatePrecision = datePrecisionFromDatePtr(o.DeathDate) r.HairColor = zero.StringFrom(o.HairColor) r.Weight = intFromPtr(o.Weight) r.IgnoreAutoTag = o.IgnoreAutoTag @@ -98,7 +102,7 @@ func (r *performerRow) resolve() *models.Performer { ID: r.ID, Name: r.Name.String, Disambiguation: r.Disambigation.String, - Birthdate: r.Birthdate.DatePtr(), + Birthdate: r.Birthdate.DatePtr(r.BirthdatePrecision), Ethnicity: r.Ethnicity.String, Country: r.Country.String, EyeColor: r.EyeColor.String, @@ -115,7 +119,7 @@ func (r *performerRow) resolve() *models.Performer { // expressed as 1-100 Rating: nullIntPtr(r.Rating), Details: r.Details.String, - DeathDate: r.DeathDate.DatePtr(), + DeathDate: r.DeathDate.DatePtr(r.DeathDatePrecision), HairColor: r.HairColor.String, Weight: nullIntPtr(r.Weight), IgnoreAutoTag: r.IgnoreAutoTag, @@ -142,7 +146,7 @@ func (r *performerRowRecord) fromPartial(o models.PerformerPartial) { r.setString("name", o.Name) r.setNullString("disambiguation", o.Disambiguation) r.setNullString("gender", o.Gender) - r.setNullDate("birthdate", o.Birthdate) + r.setNullDate("birthdate", "birthdate_precision", o.Birthdate) r.setNullString("ethnicity", o.Ethnicity) r.setNullString("country", o.Country) r.setNullString("eye_color", o.EyeColor) @@ -159,7 +163,7 @@ func (r *performerRowRecord) fromPartial(o models.PerformerPartial) { r.setTimestamp("updated_at", o.UpdatedAt) r.setNullInt("rating", o.Rating) r.setNullString("details", o.Details) - r.setNullDate("death_date", o.DeathDate) + r.setNullDate("death_date", "death_date_precision", o.DeathDate) r.setNullString("hair_color", o.HairColor) r.setNullInt("weight", o.Weight) r.setBool("ignore_auto_tag", o.IgnoreAutoTag) @@ -189,7 +193,7 @@ var ( }, fkColumn: tagIDColumn, foreignTable: tagTable, - orderBy: "COALESCE(tags.sort_name, tags.name) ASC", + orderBy: tagTableSortSQL, }, stashIDs: stashIDRepository{ repository{ @@ -702,6 +706,28 @@ func (qb *PerformerStore) sortByLastOAt(direction string) string { return " ORDER BY (" + selectPerformerLastOAtSQL + ") " + direction } +// used for sorting on performer latest scene +var selectPerformerLatestSceneSQL = utils.StrFormat( + "SELECT MAX(date) FROM ("+ + "SELECT {date} FROM {performers_scenes} s "+ + "LEFT JOIN {scenes} ON {scenes}.id = s.{scene_id} "+ + "WHERE s.{performer_id} = {performers}.id"+ + ")", + map[string]interface{}{ + "performer_id": performerIDColumn, + "performers": performerTable, + "performers_scenes": performersScenesTable, + "scenes": sceneTable, + "scene_id": sceneIDColumn, + "date": sceneDateColumn, + }, +) + +func (qb *PerformerStore) sortByLatestScene(direction string) string { + // need to get the latest date from scenes + return " ORDER BY (" + selectPerformerLatestSceneSQL + ") " + direction +} + // used for sorting on performer last view_date var selectPerformerLastPlayedAtSQL = utils.StrFormat( "SELECT MAX(view_date) FROM ("+ @@ -726,6 +752,28 @@ func (qb *PerformerStore) sortByLastPlayedAt(direction string) string { return " ORDER BY (" + selectPerformerLastPlayedAtSQL + ") " + direction } +// used for sorting by total scene duration +var selectPerformerScenesDurationSQL = utils.StrFormat( + "SELECT COALESCE(SUM(video_files.duration), 0) FROM {performers_scenes} s "+ + "LEFT JOIN {scenes} ON {scenes}.id = s.{scene_id} "+ + "LEFT JOIN {scenes_files} ON {scenes_files}.{scene_id} = {scenes}.id "+ + "LEFT JOIN video_files ON video_files.file_id = {scenes_files}.file_id "+ + "WHERE s.{performer_id} = {performers}.id", + map[string]interface{}{ + "performer_id": performerIDColumn, + "performers": performerTable, + "performers_scenes": performersScenesTable, + "scenes": sceneTable, + "scene_id": sceneIDColumn, + "scenes_files": scenesFilesTable, + }, +) + +func (qb *PerformerStore) sortByScenesDuration(direction string) string { + // need to sum duration from all scenes for this performer + return " ORDER BY (" + selectPerformerScenesDurationSQL + ") " + direction +} + var performerSortOptions = sortOptions{ "birthdate", "career_length", @@ -736,6 +784,7 @@ var performerSortOptions = sortOptions{ "images_count", "last_o_at", "last_played_at", + "latest_scene", "measurements", "name", "o_counter", @@ -744,6 +793,7 @@ var performerSortOptions = sortOptions{ "random", "rating", "scenes_count", + "scenes_duration", "tag_count", "updated_at", "weight", @@ -771,6 +821,8 @@ func (qb *PerformerStore) getPerformerSort(findFilter *models.FindFilterType) (s sortQuery += getCountSort(performerTable, performersTagsTable, performerIDColumn, direction) case "scenes_count": sortQuery += getCountSort(performerTable, performersScenesTable, performerIDColumn, direction) + case "scenes_duration": + sortQuery += qb.sortByScenesDuration(direction) case "images_count": sortQuery += getCountSort(performerTable, performersImagesTable, performerIDColumn, direction) case "galleries_count": @@ -783,6 +835,8 @@ func (qb *PerformerStore) getPerformerSort(findFilter *models.FindFilterType) (s sortQuery += qb.sortByLastPlayedAt(direction) case "last_o_at": sortQuery += qb.sortByLastOAt(direction) + case "latest_scene": + sortQuery += qb.sortByLatestScene(direction) default: sortQuery += getSort(sort, direction, "performers") } @@ -864,3 +918,58 @@ func (qb *PerformerStore) FindByStashIDStatus(ctx context.Context, hasStashID bo return ret, nil } + +func (qb *PerformerStore) Merge(ctx context.Context, source []int, destination int) error { + if len(source) == 0 { + return nil + } + + inBinding := getInBinding(len(source)) + + args := []interface{}{destination} + srcArgs := make([]interface{}, len(source)) + for i, id := range source { + if id == destination { + return errors.New("cannot merge where source == destination") + } + srcArgs[i] = id + } + + args = append(args, srcArgs...) + + performerTables := map[string]string{ + performersScenesTable: sceneIDColumn, + performersGalleriesTable: galleryIDColumn, + performersImagesTable: imageIDColumn, + performersTagsTable: tagIDColumn, + } + + args = append(args, destination) + + // for each table, update source performer ids to destination performer id, ignoring duplicates + for table, idColumn := range performerTables { + _, err := dbWrapper.Exec(ctx, `UPDATE OR IGNORE `+table+` +SET performer_id = ? +WHERE performer_id IN `+inBinding+` +AND NOT EXISTS(SELECT 1 FROM `+table+` o WHERE o.`+idColumn+` = `+table+`.`+idColumn+` AND o.performer_id = ?)`, + args..., + ) + if err != nil { + return err + } + + // delete source performer ids from the table where they couldn't be set + if _, err := dbWrapper.Exec(ctx, `DELETE FROM `+table+` WHERE performer_id IN `+inBinding, srcArgs...); err != nil { + return err + } + } + + for _, id := range source { + err := qb.Destroy(ctx, id) + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/sqlite/performer_filter.go b/pkg/sqlite/performer_filter.go index 29bc75a74..401664e33 100644 --- a/pkg/sqlite/performer_filter.go +++ b/pkg/sqlite/performer_filter.go @@ -148,6 +148,12 @@ func (qb *performerFilterHandler) criterionHandler() criterionHandler { stashIDTableAs: "performer_stash_ids", parentIDCol: "performers.id", }, + &stashIDsCriterionHandler{ + c: filter.StashIDsEndpoint, + stashIDRepository: &performerRepository.stashIDs, + stashIDTableAs: "performer_stash_ids", + parentIDCol: "performers.id", + }, qb.aliasCriterionHandler(filter.Aliases), @@ -447,7 +453,7 @@ func (qb *performerFilterHandler) studiosCriterionHandler(studios *models.Hierar return } - if len(studios.Value) == 0 { + if len(studios.Value) == 0 && len(studios.Excludes) == 0 { return } @@ -464,27 +470,54 @@ func (qb *performerFilterHandler) studiosCriterionHandler(studios *models.Hierar return } - const derivedPerformerStudioTable = "performer_studio" - valuesClause, err := getHierarchicalValues(ctx, studios.Value, studioTable, "", "parent_id", "child_id", studios.Depth) - if err != nil { - f.setError(err) - return - } - f.addWith("studio(root_id, item_id) AS (" + valuesClause + ")") + if len(studios.Value) > 0 { + const derivedPerformerStudioTable = "performer_studio" + valuesClause, err := getHierarchicalValues(ctx, studios.Value, studioTable, "", "parent_id", "child_id", studios.Depth) + if err != nil { + f.setError(err) + return + } + f.addWith("studio(root_id, item_id) AS (" + valuesClause + ")") - templStr := `SELECT performer_id FROM {primaryTable} + templStr := `SELECT performer_id FROM {primaryTable} + INNER JOIN {joinTable} ON {primaryTable}.id = {joinTable}.{primaryFK} + INNER JOIN studio ON {primaryTable}.studio_id = studio.item_id` + + var unions []string + for _, c := range formatMaps { + unions = append(unions, utils.StrFormat(templStr, c)) + } + + f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerStudioTable, strings.Join(unions, " UNION "))) + + f.addLeftJoin(derivedPerformerStudioTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerStudioTable)) + f.addWhere(fmt.Sprintf("%s.performer_id IS %s NULL", derivedPerformerStudioTable, clauseCondition)) + } + + // #6412 - handle excludes as well + if len(studios.Excludes) > 0 { + excludeValuesClause, err := getHierarchicalValues(ctx, studios.Excludes, studioTable, "", "parent_id", "child_id", studios.Depth) + if err != nil { + f.setError(err) + return + } + f.addWith("exclude_studio(root_id, item_id) AS (" + excludeValuesClause + ")") + + excludeTemplStr := `SELECT performer_id FROM {primaryTable} INNER JOIN {joinTable} ON {primaryTable}.id = {joinTable}.{primaryFK} - INNER JOIN studio ON {primaryTable}.studio_id = studio.item_id` + INNER JOIN exclude_studio ON {primaryTable}.studio_id = exclude_studio.item_id` - var unions []string - for _, c := range formatMaps { - unions = append(unions, utils.StrFormat(templStr, c)) + var unions []string + for _, c := range formatMaps { + unions = append(unions, utils.StrFormat(excludeTemplStr, c)) + } + + const excludePerformerStudioTable = "performer_studio_exclude" + f.addWith(fmt.Sprintf("%s AS (%s)", excludePerformerStudioTable, strings.Join(unions, " UNION "))) + + f.addLeftJoin(excludePerformerStudioTable, "", fmt.Sprintf("performers.id = %s.performer_id", excludePerformerStudioTable)) + f.addWhere(fmt.Sprintf("%s.performer_id IS NULL", excludePerformerStudioTable)) } - - f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerStudioTable, strings.Join(unions, " UNION "))) - - f.addLeftJoin(derivedPerformerStudioTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerStudioTable)) - f.addWhere(fmt.Sprintf("%s.performer_id IS %s NULL", derivedPerformerStudioTable, clauseCondition)) } } } diff --git a/pkg/sqlite/performer_test.go b/pkg/sqlite/performer_test.go index eb1dfbad2..8d53ca0db 100644 --- a/pkg/sqlite/performer_test.go +++ b/pkg/sqlite/performer_test.go @@ -282,7 +282,7 @@ func Test_PerformerStore_Update(t *testing.T) { Weight: &weight, IgnoreAutoTag: ignoreAutoTag, Aliases: models.NewRelatedStrings(aliases), - TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}), + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithPerformer]}), StashIDs: models.NewRelatedStashIDs([]models.StashID{ { StashID: stashID1, @@ -516,7 +516,7 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { Weight: models.NewOptionalInt(weight), IgnoreAutoTag: models.NewOptionalBool(ignoreAutoTag), TagIDs: &models.UpdateIDs{ - IDs: []int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}, + IDs: []int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithPerformer]}, Mode: models.RelationshipUpdateModeSet, }, StashIDs: &models.UpdateStashIDs{ @@ -563,7 +563,7 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { HairColor: hairColor, Weight: &weight, IgnoreAutoTag: ignoreAutoTag, - TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}), + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithPerformer]}), StashIDs: models.NewRelatedStashIDs([]models.StashID{ { StashID: stashID1, @@ -1069,6 +1069,8 @@ func TestPerformerQuery(t *testing.T) { var ( endpoint = performerStashID(performerIdxWithGallery).Endpoint stashID = performerStashID(performerIdxWithGallery).StashID + stashID2 = performerStashID(performerIdx1WithGallery).StashID + stashIDs = []*string{&stashID, &stashID2} ) tests := []struct { @@ -1133,6 +1135,60 @@ func TestPerformerQuery(t *testing.T) { nil, false, }, + { + "stash ids with endpoint", + nil, + &models.PerformerFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + StashIDs: stashIDs, + Modifier: models.CriterionModifierEquals, + }, + }, + []int{performerIdxWithGallery, performerIdx1WithGallery}, + nil, + false, + }, + { + "exclude stash ids with endpoint", + nil, + &models.PerformerFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + StashIDs: stashIDs, + Modifier: models.CriterionModifierNotEquals, + }, + }, + nil, + []int{performerIdxWithGallery, performerIdx1WithGallery}, + false, + }, + { + "null stash ids with endpoint", + nil, + &models.PerformerFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierIsNull, + }, + }, + nil, + []int{performerIdxWithGallery, performerIdx1WithGallery}, + false, + }, + { + "not null stash ids with endpoint", + nil, + &models.PerformerFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierNotNull, + }, + }, + []int{performerIdxWithGallery, performerIdx1WithGallery}, + nil, + false, + }, { "circumcised (cut)", nil, @@ -1160,6 +1216,98 @@ func TestPerformerQuery(t *testing.T) { []int{performerIdx1WithScene, performerIdxWithScene}, false, }, + { + "include scene studio", + nil, + &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(studioIDs[studioIdxWithScenePerformer])}, + Modifier: models.CriterionModifierIncludes, + }, + }, + []int{performerIdxWithSceneStudio}, + nil, + false, + }, + { + "include image studio", + nil, + &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(studioIDs[studioIdxWithImagePerformer])}, + Modifier: models.CriterionModifierIncludes, + }, + }, + []int{performerIdxWithImageStudio}, + nil, + false, + }, + { + "include gallery studio", + nil, + &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(studioIDs[studioIdxWithGalleryPerformer])}, + Modifier: models.CriterionModifierIncludes, + }, + }, + []int{performerIdxWithGalleryStudio}, + nil, + false, + }, + { + "exclude scene studio", + nil, + &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(studioIDs[studioIdxWithScenePerformer])}, + Modifier: models.CriterionModifierExcludes, + }, + }, + nil, + []int{performerIdxWithSceneStudio}, + false, + }, + { + "exclude image studio", + nil, + &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(studioIDs[studioIdxWithImagePerformer])}, + Modifier: models.CriterionModifierExcludes, + }, + }, + nil, + []int{performerIdxWithImageStudio}, + false, + }, + { + "exclude gallery studio", + nil, + &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(studioIDs[studioIdxWithGalleryPerformer])}, + Modifier: models.CriterionModifierExcludes, + }, + }, + nil, + []int{performerIdxWithGalleryStudio}, + false, + }, + { + "include and exclude scene studio", + nil, + &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(studioIDs[studioIdx1WithTwoScenePerformer])}, + Modifier: models.CriterionModifierIncludes, + Excludes: []string{strconv.Itoa(studioIDs[studioIdx2WithTwoScenePerformer])}, + }, + }, + nil, + []int{performerIdxWithTwoSceneStudio}, + false, + }, } for _, tt := range tests { @@ -2260,7 +2408,7 @@ func TestPerformerQuerySortScenesCount(t *testing.T) { assert.True(t, len(performers) > 0) lastPerformer := performers[len(performers)-1] - assert.Equal(t, performerIDs[performerIdxWithTag], lastPerformer.ID) + assert.Equal(t, performerIDs[performerIdxWithTwoSceneStudio], lastPerformer.ID) return nil }) @@ -2432,6 +2580,146 @@ func TestPerformerStore_FindByStashIDStatus(t *testing.T) { } } +func TestPerformerMerge(t *testing.T) { + tests := []struct { + name string + srcIdxs []int + destIdx int + wantErr bool + }{ + { + name: "merge into self", + srcIdxs: []int{performerIdx1WithDupName}, + destIdx: performerIdx1WithDupName, + wantErr: true, + }, + { + name: "merge multiple", + srcIdxs: []int{ + performerIdx2WithScene, + performerIdxWithTwoScenes, + performerIdx1WithImage, + performerIdxWithTwoImages, + performerIdxWithGallery, + performerIdxWithTwoGalleries, + performerIdxWithTag, + performerIdxWithTwoTags, + }, + destIdx: tagIdxWithPerformer, + wantErr: false, + }, + } + + qb := db.Performer + + for _, tt := range tests { + runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { + assert := assert.New(t) + + // load src tag ids to compare after merge + performerTagIds := make(map[int][]int) + for _, srcIdx := range tt.srcIdxs { + srcPerformer, err := qb.Find(ctx, performerIDs[srcIdx]) + if err != nil { + t.Errorf("Error finding performer: %s", err.Error()) + } + if err := srcPerformer.LoadTagIDs(ctx, qb); err != nil { + t.Errorf("Error loading performer tag IDs: %s", err.Error()) + } + srcTagIDs := srcPerformer.TagIDs.List() + performerTagIds[srcIdx] = srcTagIDs + } + + err := qb.Merge(ctx, indexesToIDs(tagIDs, tt.srcIdxs), tagIDs[tt.destIdx]) + + if (err != nil) != tt.wantErr { + t.Errorf("PerformerStore.Merge() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if err != nil { + return + } + + // ensure source performers are destroyed + for _, srcIdx := range tt.srcIdxs { + p, err := qb.Find(ctx, performerIDs[srcIdx]) + + // not found returns nil performer and nil error + if err != nil { + t.Errorf("Error finding performer: %s", err.Error()) + continue + } + assert.Nil(p) + } + + // ensure items point to new performer + for _, srcIdx := range tt.srcIdxs { + sceneIdxs := scenePerformers.reverseLookup(srcIdx) + for _, sceneIdx := range sceneIdxs { + s, err := db.Scene.Find(ctx, sceneIDs[sceneIdx]) + if err != nil { + t.Errorf("Error finding scene: %s", err.Error()) + } + if err := s.LoadPerformerIDs(ctx, db.Scene); err != nil { + t.Errorf("Error loading scene performer IDs: %s", err.Error()) + } + scenePerformerIDs := s.PerformerIDs.List() + + assert.Contains(scenePerformerIDs, performerIDs[tt.destIdx]) + assert.NotContains(scenePerformerIDs, performerIDs[srcIdx]) + } + + imageIdxs := imagePerformers.reverseLookup(srcIdx) + for _, imageIdx := range imageIdxs { + i, err := db.Image.Find(ctx, imageIDs[imageIdx]) + if err != nil { + t.Errorf("Error finding image: %s", err.Error()) + } + if err := i.LoadPerformerIDs(ctx, db.Image); err != nil { + t.Errorf("Error loading image performer IDs: %s", err.Error()) + } + imagePerformerIDs := i.PerformerIDs.List() + + assert.Contains(imagePerformerIDs, performerIDs[tt.destIdx]) + assert.NotContains(imagePerformerIDs, performerIDs[srcIdx]) + } + + galleryIdxs := galleryPerformers.reverseLookup(srcIdx) + for _, galleryIdx := range galleryIdxs { + g, err := db.Gallery.Find(ctx, galleryIDs[galleryIdx]) + if err != nil { + t.Errorf("Error finding gallery: %s", err.Error()) + } + if err := g.LoadPerformerIDs(ctx, db.Gallery); err != nil { + t.Errorf("Error loading gallery performer IDs: %s", err.Error()) + } + galleryPerformerIDs := g.PerformerIDs.List() + + assert.Contains(galleryPerformerIDs, performerIDs[tt.destIdx]) + assert.NotContains(galleryPerformerIDs, performerIDs[srcIdx]) + } + } + + // ensure tags were merged + destPerformer, err := qb.Find(ctx, performerIDs[tt.destIdx]) + if err != nil { + t.Errorf("Error finding performer: %s", err.Error()) + } + if err := destPerformer.LoadTagIDs(ctx, qb); err != nil { + t.Errorf("Error loading performer tag IDs: %s", err.Error()) + } + destTagIDs := destPerformer.TagIDs.List() + + for _, srcIdx := range tt.srcIdxs { + for _, tagID := range performerTagIds[srcIdx] { + assert.Contains(destTagIDs, tagID) + } + } + }) + } +} + // TODO Update // TODO Destroy // TODO Find diff --git a/pkg/sqlite/query.go b/pkg/sqlite/query.go index 4f4c0c8db..99c1f4e5f 100644 --- a/pkg/sqlite/query.go +++ b/pkg/sqlite/query.go @@ -24,8 +24,8 @@ type queryBuilder struct { sortAndPagination string } -func (qb queryBuilder) body() string { - return fmt.Sprintf("SELECT %s FROM %s%s", strings.Join(qb.columns, ", "), qb.from, qb.joins.toSQL()) +func (qb queryBuilder) body(includeSortPagination bool) string { + return fmt.Sprintf("SELECT %s FROM %s%s", strings.Join(qb.columns, ", "), qb.from, qb.joins.toSQL(includeSortPagination)) } func (qb *queryBuilder) addColumn(column string) { @@ -33,7 +33,7 @@ func (qb *queryBuilder) addColumn(column string) { } func (qb queryBuilder) toSQL(includeSortPagination bool) string { - body := qb.body() + body := qb.body(includeSortPagination) withClause := "" if len(qb.withClauses) > 0 { @@ -59,12 +59,14 @@ func (qb queryBuilder) findIDs(ctx context.Context) ([]int, error) { } func (qb queryBuilder) executeFind(ctx context.Context) ([]int, int, error) { - body := qb.body() + const includeSortPagination = true + body := qb.body(includeSortPagination) return qb.repository.executeFindQuery(ctx, body, qb.args, qb.sortAndPagination, qb.whereClauses, qb.havingClauses, qb.withClauses, qb.recursiveWith) } func (qb queryBuilder) executeCount(ctx context.Context) (int, error) { - body := qb.body() + const includeSortPagination = false + body := qb.body(includeSortPagination) withClause := "" if len(qb.withClauses) > 0 { @@ -131,10 +133,23 @@ func (qb *queryBuilder) join(table, as, onClause string) { qb.joins.add(newJoin) } +func (qb *queryBuilder) joinSort(table, as, onClause string) { + newJoin := join{ + sort: true, + table: table, + as: as, + onClause: onClause, + joinType: "LEFT", + } + + qb.joins.add(newJoin) +} + func (qb *queryBuilder) addJoins(joins ...join) { - qb.joins.add(joins...) for _, j := range joins { - qb.args = append(qb.args, j.args...) + if qb.joins.addUnique(j) { + qb.args = append(qb.args, j.args...) + } } } diff --git a/pkg/sqlite/record.go b/pkg/sqlite/record.go index e60cdc4f7..71622dc60 100644 --- a/pkg/sqlite/record.go +++ b/pkg/sqlite/record.go @@ -100,8 +100,9 @@ func (r *updateRecord) setNullTimestamp(destField string, v models.OptionalTime) } } -func (r *updateRecord) setNullDate(destField string, v models.OptionalDate) { +func (r *updateRecord) setNullDate(destField string, precisionField string, v models.OptionalDate) { if v.Set { r.set(destField, NullDateFromDatePtr(v.Ptr())) + r.set(precisionField, datePrecisionFromDatePtr(v.Ptr())) } } diff --git a/pkg/sqlite/repository.go b/pkg/sqlite/repository.go index ac2954cfb..18d501e3a 100644 --- a/pkg/sqlite/repository.go +++ b/pkg/sqlite/repository.go @@ -96,7 +96,7 @@ func (r *repository) runIdsQuery(ctx context.Context, query string, args []inter } func (r *repository) queryFunc(ctx context.Context, query string, args []interface{}, single bool, f func(rows *sqlx.Rows) error) error { - rows, err := dbWrapper.Queryx(ctx, query, args...) + rows, err := dbWrapper.QueryxContext(ctx, query, args...) if err != nil && !errors.Is(err, sql.ErrNoRows) { return err @@ -119,13 +119,12 @@ func (r *repository) queryFunc(ctx context.Context, query string, args []interfa return nil } +// queryStruct executes a query and scans the result into the provided struct. +// Unlike the other query methods, this will return an error if no rows are found. func (r *repository) queryStruct(ctx context.Context, query string, args []interface{}, out interface{}) error { - if err := r.queryFunc(ctx, query, args, true, func(rows *sqlx.Rows) error { - if err := rows.StructScan(out); err != nil { - return err - } - return nil - }); err != nil { + // changed from queryFunc, since it was not logging the performance correctly, + // since the query doesn't actually execute until Scan is called + if err := dbWrapper.Get(ctx, out, query, args...); err != nil { return fmt.Errorf("executing query: %s [%v]: %w", query, args, err) } diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index c4a46b23c..d92800317 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -26,6 +26,7 @@ const ( sceneTable = "scenes" scenesFilesTable = "scenes_files" sceneIDColumn = "scene_id" + sceneDateColumn = "date" performersScenesTable = "performers_scenes" scenesTagsTable = "scenes_tags" scenesGalleriesTable = "scenes_galleries" @@ -76,12 +77,13 @@ ORDER BY files.size DESC; ` type sceneRow struct { - ID int `db:"id" goqu:"skipinsert"` - Title zero.String `db:"title"` - Code zero.String `db:"code"` - Details zero.String `db:"details"` - Director zero.String `db:"director"` - Date NullDate `db:"date"` + ID int `db:"id" goqu:"skipinsert"` + Title zero.String `db:"title"` + Code zero.String `db:"code"` + Details zero.String `db:"details"` + Director zero.String `db:"director"` + Date NullDate `db:"date"` + DatePrecision null.Int `db:"date_precision"` // expressed as 1-100 Rating null.Int `db:"rating"` Organized bool `db:"organized"` @@ -102,6 +104,7 @@ func (r *sceneRow) fromScene(o models.Scene) { r.Details = zero.StringFrom(o.Details) r.Director = zero.StringFrom(o.Director) r.Date = NullDateFromDatePtr(o.Date) + r.DatePrecision = datePrecisionFromDatePtr(o.Date) r.Rating = intFromPtr(o.Rating) r.Organized = o.Organized r.StudioID = intFromPtr(o.StudioID) @@ -127,7 +130,7 @@ func (r *sceneQueryRow) resolve() *models.Scene { Code: r.Code.String, Details: r.Details.String, Director: r.Director.String, - Date: r.Date.DatePtr(), + Date: r.Date.DatePtr(r.DatePrecision), Rating: nullIntPtr(r.Rating), Organized: r.Organized, StudioID: nullIntPtr(r.StudioID), @@ -159,7 +162,7 @@ func (r *sceneRowRecord) fromPartial(o models.ScenePartial) { r.setNullString("code", o.Code) r.setNullString("details", o.Details) r.setNullString("director", o.Director) - r.setNullDate("date", o.Date) + r.setNullDate("date", "date_precision", o.Date) r.setNullInt("rating", o.Rating) r.setBool("organized", o.Organized) r.setNullInt("studio_id", o.StudioID) @@ -201,7 +204,7 @@ var ( }, fkColumn: tagIDColumn, foreignTable: tagTable, - orderBy: "COALESCE(tags.sort_name, tags.name) ASC", + orderBy: tagTableSortSQL, }, performers: joinRepository{ repository: repository{ @@ -795,6 +798,46 @@ func (qb *SceneStore) OCountByPerformerID(ctx context.Context, performerID int) return ret, nil } +func (qb *SceneStore) OCountByGroupID(ctx context.Context, groupID int) (int, error) { + table := qb.table() + joinTable := scenesGroupsJoinTable + oHistoryTable := goqu.T(scenesODatesTable) + + q := dialect.Select(goqu.COUNT("*")).From(table).InnerJoin( + oHistoryTable, + goqu.On(table.Col(idColumn).Eq(oHistoryTable.Col(sceneIDColumn))), + ).InnerJoin( + joinTable, + goqu.On( + table.Col(idColumn).Eq(joinTable.Col(sceneIDColumn)), + ), + ).Where(joinTable.Col(groupIDColumn).Eq(groupID)) + + var ret int + if err := querySimple(ctx, q, &ret); err != nil { + return 0, err + } + + return ret, nil +} + +func (qb *SceneStore) OCountByStudioID(ctx context.Context, studioID int) (int, error) { + table := qb.table() + oHistoryTable := goqu.T(scenesODatesTable) + + q := dialect.Select(goqu.COUNT("*")).From(table).InnerJoin( + oHistoryTable, + goqu.On(table.Col(idColumn).Eq(oHistoryTable.Col(sceneIDColumn))), + ).Where(table.Col(studioIDColumn).Eq(studioID)) + + var ret int + if err := querySimple(ctx, q, &ret); err != nil { + return 0, err + } + + return ret, nil +} + func (qb *SceneStore) FindByGroupID(ctx context.Context, groupID int) ([]*models.Scene, error) { sq := dialect.From(scenesGroupsJoinTable).Select(scenesGroupsJoinTable.Col(sceneIDColumn)).Where( scenesGroupsJoinTable.Col(groupIDColumn).Eq(groupID), @@ -1096,9 +1139,12 @@ var sceneSortOptions = sortOptions{ "perceptual_similarity", "random", "rating", + "resolution", + "studio", "tag_count", "title", "updated_at", + "performer_age", } func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindFilterType) error { @@ -1115,10 +1161,12 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF addFileTable := func() { query.addJoins( join{ + sort: true, table: scenesFilesTable, onClause: "scenes_files.scene_id = scenes.id", }, join{ + sort: true, table: fileTable, onClause: "scenes_files.file_id = files.id", }, @@ -1129,6 +1177,7 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF addFileTable() query.addJoins( join{ + sort: true, table: videoFileTable, onClause: "video_files.file_id = scenes_files.file_id", }, @@ -1138,6 +1187,7 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF addFolderTable := func() { query.addJoins( join{ + sort: true, table: folderTable, onClause: "files.parent_folder_id = folders.id", }, @@ -1147,10 +1197,10 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF direction := findFilter.GetDirection() switch sort { case "movie_scene_number": - query.join(groupsScenesTable, "", "scenes.id = groups_scenes.scene_id") + query.joinSort(groupsScenesTable, "", "scenes.id = groups_scenes.scene_id") query.sortAndPagination += getSort("scene_index", direction, groupsScenesTable) case "group_scene_number": - query.join(groupsScenesTable, "scene_group", "scenes.id = scene_group.scene_id") + query.joinSort(groupsScenesTable, "scene_group", "scenes.id = scene_group.scene_id") query.sortAndPagination += getSort("scene_index", direction, "scene_group") case "tag_count": query.sortAndPagination += getCountSort(sceneTable, scenesTagsTable, sceneIDColumn, direction) @@ -1168,6 +1218,7 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF addFileTable() query.addJoins( join{ + sort: true, table: fingerprintTable, as: "fingerprints_phash", onClause: "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'", @@ -1187,6 +1238,9 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF sort = "frame_rate" addVideoFileTable() query.sortAndPagination += getSort(sort, direction, videoFileTable) + case "resolution": + addVideoFileTable() + query.sortAndPagination += fmt.Sprintf(" ORDER BY MIN(%s.width, %s.height) %s", videoFileTable, videoFileTable, getSortDirection(direction)) case "filesize": addFileTable() query.sortAndPagination += getSort(sort, direction, fileTable) @@ -1208,6 +1262,32 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF query.sortAndPagination += fmt.Sprintf(" ORDER BY (SELECT MAX(o_date) FROM %s AS sort WHERE sort.%s = %s.id) %s", scenesODatesTable, sceneIDColumn, sceneTable, getSortDirection(direction)) case "o_counter": query.sortAndPagination += getCountSort(sceneTable, scenesODatesTable, sceneIDColumn, direction) + case "performer_age": + // Looking at the youngest performer by default + aggregation := "MIN" + if direction == "DESC" { + // When sorting by performer_'s age DESC, I should consider the oldest performer instead + aggregation = "MAX" + } + fallback := "NULL" + if direction == "ASC" { + // When sorting ascending, NULLs are first by default. Coalescing to the MAX int value supported by sqlite + fallback = "9223372036854775807" + } + query.sortAndPagination += fmt.Sprintf( + " ORDER BY (SELECT COALESCE(%s(JulianDay(scenes.date) - JulianDay(performers.birthdate)), %s) FROM %s as performers INNER JOIN %s AS aggregation WHERE performers.id = aggregation.%s AND aggregation.%s = %s.id) %s", + aggregation, + fallback, + performerTable, + performersScenesTable, + performerIDColumn, + sceneIDColumn, + sceneTable, + getSortDirection(direction), + ) + case "studio": + query.joinSort(studioTable, "", "scenes.studio_id = studios.id") + query.sortAndPagination += getSort("name", direction, studioTable) default: query.sortAndPagination += getSort(sort, direction, "scenes") } diff --git a/pkg/sqlite/scene_filter.go b/pkg/sqlite/scene_filter.go index 2e63dad97..aa0d349df 100644 --- a/pkg/sqlite/scene_filter.go +++ b/pkg/sqlite/scene_filter.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/utils" ) type sceneFilterHandler struct { @@ -83,14 +82,27 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler { criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { if sceneFilter.Phash != nil { // backwards compatibility - qb.phashDistanceCriterionHandler(&models.PhashDistanceCriterionInput{ - Value: sceneFilter.Phash.Value, - Modifier: sceneFilter.Phash.Modifier, - })(ctx, f) + h := phashDistanceCriterionHandler{ + joinFn: func(f *filterBuilder) { + qb.addSceneFilesTable(f) + f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'") + }, + criterion: &models.PhashDistanceCriterionInput{ + Value: sceneFilter.Phash.Value, + Modifier: sceneFilter.Phash.Modifier, + }, + } + h.handle(ctx, f) } }), - qb.phashDistanceCriterionHandler(sceneFilter.PhashDistance), + &phashDistanceCriterionHandler{ + joinFn: func(f *filterBuilder) { + qb.addSceneFilesTable(f) + f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'") + }, + criterion: sceneFilter.PhashDistance, + }, intCriterionHandler(sceneFilter.Rating100, "scenes.rating", nil), qb.oCountCriterionHandler(sceneFilter.OCounter), @@ -114,13 +126,20 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler { stringCriterionHandler(sceneFilter.StashID, "scene_stash_ids.stash_id")(ctx, f) } }), - &stashIDCriterionHandler{ c: sceneFilter.StashIDEndpoint, stashIDRepository: &sceneRepository.stashIDs, stashIDTableAs: "scene_stash_ids", parentIDCol: "scenes.id", }, + &stashIDsCriterionHandler{ + c: sceneFilter.StashIDsEndpoint, + stashIDRepository: &sceneRepository.stashIDs, + stashIDTableAs: "scene_stash_ids", + parentIDCol: "scenes.id", + }, + + qb.stashIDCountCriterionHandler(sceneFilter.StashIDCount), boolCriterionHandler(sceneFilter.Interactive, "video_files.interactive", qb.addVideoFilesTable), intCriterionHandler(sceneFilter.InteractiveSpeed, "video_files.interactive_speed", qb.addVideoFilesTable), @@ -202,6 +221,21 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler { }, }, + &relatedFilterHandler{ + relatedIDCol: "files.id", + relatedRepo: fileRepository.repository, + relatedHandler: &fileFilterHandler{ + fileFilter: sceneFilter.FilesFilter, + isRelated: true, + }, + joinFn: func(f *filterBuilder) { + qb.addFilesTable(f) + qb.addFoldersTable(f) + }, + // don't use a subquery; join directly + directJoin: true, + }, + &relatedFilterHandler{ relatedIDCol: "scene_markers.id", relatedRepo: sceneMarkerRepository.repository, @@ -319,7 +353,7 @@ func (qb *sceneFilterHandler) isMissingCriterionHandler(isMissing *string) crite f.addWhere("galleries_join.scene_id IS NULL") case "studio": f.addWhere("scenes.studio_id IS NULL") - case "movie": + case "movie", "group": sceneRepository.groups.join(f, "groups_join", "scenes.id") f.addWhere("groups_join.scene_id IS NULL") case "performers": @@ -421,6 +455,16 @@ func (qb *sceneFilterHandler) tagCountCriterionHandler(tagCount *models.IntCrite return h.handler(tagCount) } +func (qb *sceneFilterHandler) stashIDCountCriterionHandler(stashIDCount *models.IntCriterionInput) criterionHandlerFunc { + h := countCriterionHandlerBuilder{ + primaryTable: sceneTable, + joinTable: "scene_stash_ids", + primaryFK: sceneIDColumn, + } + + return h.handler(stashIDCount) +} + func (qb *sceneFilterHandler) performersCriterionHandler(performers *models.MultiCriterionInput) criterionHandlerFunc { h := joinedMultiCriterionHandlerBuilder{ primaryTable: sceneTable, @@ -527,42 +571,3 @@ func (qb *sceneFilterHandler) performerTagsCriterionHandler(tags *models.Hierarc joinPrimaryKey: sceneIDColumn, } } - -func (qb *sceneFilterHandler) phashDistanceCriterionHandler(phashDistance *models.PhashDistanceCriterionInput) criterionHandlerFunc { - return func(ctx context.Context, f *filterBuilder) { - if phashDistance != nil { - qb.addSceneFilesTable(f) - f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'") - - value, _ := utils.StringToPhash(phashDistance.Value) - distance := 0 - if phashDistance.Distance != nil { - distance = *phashDistance.Distance - } - - if distance == 0 { - // use the default handler - intCriterionHandler(&models.IntCriterionInput{ - Value: int(value), - Modifier: phashDistance.Modifier, - }, "fingerprints_phash.fingerprint", nil)(ctx, f) - } - - switch { - case phashDistance.Modifier == models.CriterionModifierEquals && distance > 0: - // needed to avoid a type mismatch - f.addWhere("typeof(fingerprints_phash.fingerprint) = 'integer'") - f.addWhere("phash_distance(fingerprints_phash.fingerprint, ?) < ?", value, distance) - case phashDistance.Modifier == models.CriterionModifierNotEquals && distance > 0: - // needed to avoid a type mismatch - f.addWhere("typeof(fingerprints_phash.fingerprint) = 'integer'") - f.addWhere("phash_distance(fingerprints_phash.fingerprint, ?) > ?", value, distance) - default: - intCriterionHandler(&models.IntCriterionInput{ - Value: int(value), - Modifier: phashDistance.Modifier, - }, "fingerprints_phash.fingerprint", nil)(ctx, f) - } - } - } -} diff --git a/pkg/sqlite/scene_marker.go b/pkg/sqlite/scene_marker.go index ed98d0ef7..d47df0e0f 100644 --- a/pkg/sqlite/scene_marker.go +++ b/pkg/sqlite/scene_marker.go @@ -15,7 +15,11 @@ import ( "github.com/stashapp/stash/pkg/models" ) -const sceneMarkerTable = "scene_markers" +const ( + sceneMarkerTable = "scene_markers" + sceneMarkersTagsTable = "scene_markers_tags" + sceneMarkerIDColumn = "scene_marker_id" +) const countSceneMarkersForTagQuery = ` SELECT scene_markers.id FROM scene_markers @@ -101,8 +105,8 @@ var ( }, tags: joinRepository{ repository: repository{ - tableName: "scene_markers_tags", - idColumn: "scene_marker_id", + tableName: sceneMarkersTagsTable, + idColumn: sceneMarkerIDColumn, }, fkColumn: tagIDColumn, }, @@ -157,6 +161,12 @@ func (qb *SceneMarkerStore) UpdatePartial(ctx context.Context, id int, partial m } } + if partial.TagIDs != nil { + if err := sceneMarkersTagsTableMgr.modifyJoins(ctx, id, partial.TagIDs.IDs, partial.TagIDs.Mode); err != nil { + return nil, fmt.Errorf("modifying scene marker tags: %w", err) + } + } + return qb.find(ctx, id) } @@ -382,10 +392,10 @@ func (qb *SceneMarkerStore) setSceneMarkerSort(query *queryBuilder, findFilter * switch sort { case "scenes_updated_at": sort = "updated_at" - query.join(sceneTable, "", "scenes.id = scene_markers.scene_id") + query.joinSort(sceneTable, "", "scenes.id = scene_markers.scene_id") query.sortAndPagination += getSort(sort, direction, sceneTable) case "title": - query.join(tagTable, "", "scene_markers.primary_tag_id = tags.id") + query.joinSort(tagTable, "", "scene_markers.primary_tag_id = tags.id") query.sortAndPagination += " ORDER BY COALESCE(NULLIF(scene_markers.title,''), tags.name) COLLATE NATURAL_CI " + direction case "duration": sort = "(scene_markers.end_seconds - scene_markers.seconds)" diff --git a/pkg/sqlite/scene_test.go b/pkg/sqlite/scene_test.go index 410819a34..ae9ba56cf 100644 --- a/pkg/sqlite/scene_test.go +++ b/pkg/sqlite/scene_test.go @@ -2098,6 +2098,8 @@ func TestSceneQuery(t *testing.T) { var ( endpoint = sceneStashID(sceneIdxWithGallery).Endpoint stashID = sceneStashID(sceneIdxWithGallery).StashID + stashID2 = sceneStashID(sceneIdxWithPerformer).StashID + stashIDs = []*string{&stashID, &stashID2} depth = -1 ) @@ -2203,6 +2205,60 @@ func TestSceneQuery(t *testing.T) { nil, false, }, + { + "stash ids with endpoint", + nil, + &models.SceneFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + StashIDs: stashIDs, + Modifier: models.CriterionModifierEquals, + }, + }, + []int{sceneIdxWithGallery, sceneIdxWithPerformer}, + nil, + false, + }, + { + "exclude stash ids with endpoint", + nil, + &models.SceneFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + StashIDs: stashIDs, + Modifier: models.CriterionModifierNotEquals, + }, + }, + nil, + []int{sceneIdxWithGallery, sceneIdxWithPerformer}, + false, + }, + { + "null stash ids with endpoint", + nil, + &models.SceneFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierIsNull, + }, + }, + nil, + []int{sceneIdxWithGallery, sceneIdxWithPerformer}, + false, + }, + { + "not null stash ids with endpoint", + nil, + &models.SceneFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierNotNull, + }, + }, + []int{sceneIdxWithGallery, sceneIdxWithPerformer}, + nil, + false, + }, { "with studio id 0 including child studios", nil, @@ -2217,6 +2273,32 @@ func TestSceneQuery(t *testing.T) { nil, false, }, + { + "single stash id", + nil, + &models.SceneFilterType{ + StashIDCount: &models.IntCriterionInput{ + Modifier: models.CriterionModifierEquals, + Value: 1, + }, + }, + []int{sceneIdxWithGallery, sceneIdxWithPerformer}, + []int{sceneIdxWithGroup}, + false, + }, + { + "less than one stash id", + nil, + &models.SceneFilterType{ + StashIDCount: &models.IntCriterionInput{ + Modifier: models.CriterionModifierLessThan, + Value: 1, + }, + }, + []int{sceneIdxWithGroup}, + []int{sceneIdxWithGallery, sceneIdxWithPerformer}, + false, + }, } for _, tt := range tests { @@ -2659,6 +2741,21 @@ func verifyString(t *testing.T, value string, criterion models.StringCriterionIn } } +func verifyStringList(t *testing.T, values []string, criterion models.StringCriterionInput) { + t.Helper() + assert := assert.New(t) + switch criterion.Modifier { + case models.CriterionModifierIsNull: + assert.Empty(values) + case models.CriterionModifierNotNull: + assert.NotEmpty(values) + default: + for _, v := range values { + verifyString(t, v, criterion) + } + } +} + func TestSceneQueryRating100(t *testing.T) { const rating = 60 ratingCriterion := models.IntCriterionInput{ @@ -4131,6 +4228,13 @@ func TestSceneQuerySorting(t *testing.T) { sceneIDs[sceneIdx1WithPerformer], -1, }, + { + "performer_age", + "performer_age", + models.SortDirectionEnumDesc, + -1, + -1, + }, } qb := db.Scene diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index a1df897ca..361b5cb79 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -77,6 +77,8 @@ const ( sceneIdxWithPerformerTwoTags sceneIdxWithSpacedName sceneIdxWithStudioPerformer + sceneIdx1WithTwoStudioPerformer + sceneIdx2WithTwoStudioPerformer sceneIdxWithGrandChildStudio sceneIdxMissingPhash sceneIdxWithPerformerParentTag @@ -138,6 +140,7 @@ const ( performerIdxWithSceneStudio performerIdxWithImageStudio performerIdxWithGalleryStudio + performerIdxWithTwoSceneStudio performerIdxWithParentTag // new indexes above // performers with dup names start from the end @@ -257,6 +260,8 @@ const ( studioIdxWithScenePerformer studioIdxWithImagePerformer studioIdxWithGalleryPerformer + studioIdx1WithTwoScenePerformer + studioIdx2WithTwoScenePerformer studioIdxWithTag studioIdx2WithTag studioIdxWithTwoTags @@ -384,16 +389,18 @@ var ( } scenePerformers = linkMap{ - sceneIdxWithPerformer: {performerIdxWithScene}, - sceneIdxWithTwoPerformers: {performerIdx1WithScene, performerIdx2WithScene}, - sceneIdxWithThreePerformers: {performerIdx1WithScene, performerIdx2WithScene, performerIdx3WithScene}, - sceneIdxWithPerformerTag: {performerIdxWithTag}, - sceneIdxWithTwoPerformerTag: {performerIdxWithTag, performerIdx2WithTag}, - sceneIdxWithPerformerTwoTags: {performerIdxWithTwoTags}, - sceneIdx1WithPerformer: {performerIdxWithTwoScenes}, - sceneIdx2WithPerformer: {performerIdxWithTwoScenes}, - sceneIdxWithStudioPerformer: {performerIdxWithSceneStudio}, - sceneIdxWithPerformerParentTag: {performerIdxWithParentTag}, + sceneIdxWithPerformer: {performerIdxWithScene}, + sceneIdxWithTwoPerformers: {performerIdx1WithScene, performerIdx2WithScene}, + sceneIdxWithThreePerformers: {performerIdx1WithScene, performerIdx2WithScene, performerIdx3WithScene}, + sceneIdxWithPerformerTag: {performerIdxWithTag}, + sceneIdxWithTwoPerformerTag: {performerIdxWithTag, performerIdx2WithTag}, + sceneIdxWithPerformerTwoTags: {performerIdxWithTwoTags}, + sceneIdx1WithPerformer: {performerIdxWithTwoScenes}, + sceneIdx2WithPerformer: {performerIdxWithTwoScenes}, + sceneIdxWithStudioPerformer: {performerIdxWithSceneStudio}, + sceneIdx1WithTwoStudioPerformer: {performerIdxWithTwoSceneStudio}, + sceneIdx2WithTwoStudioPerformer: {performerIdxWithTwoSceneStudio}, + sceneIdxWithPerformerParentTag: {performerIdxWithParentTag}, } sceneGalleries = linkMap{ @@ -406,11 +413,13 @@ var ( } sceneStudios = map[int]int{ - sceneIdxWithStudio: studioIdxWithScene, - sceneIdx1WithStudio: studioIdxWithTwoScenes, - sceneIdx2WithStudio: studioIdxWithTwoScenes, - sceneIdxWithStudioPerformer: studioIdxWithScenePerformer, - sceneIdxWithGrandChildStudio: studioIdxWithGrandParent, + sceneIdxWithStudio: studioIdxWithScene, + sceneIdx1WithStudio: studioIdxWithTwoScenes, + sceneIdx2WithStudio: studioIdxWithTwoScenes, + sceneIdxWithStudioPerformer: studioIdxWithScenePerformer, + sceneIdx1WithTwoStudioPerformer: studioIdx1WithTwoScenePerformer, + sceneIdx2WithTwoStudioPerformer: studioIdx2WithTwoScenePerformer, + sceneIdxWithGrandChildStudio: studioIdxWithGrandParent, } ) @@ -1067,10 +1076,17 @@ func getObjectDate(index int) *models.Date { return &ret } +func sceneStashIDs(i int) []models.StashID { + if i%5 == 0 { + return nil + } + return []models.StashID{sceneStashID(i)} +} + func sceneStashID(i int) models.StashID { return models.StashID{ StashID: getSceneStringValue(i, "stashid"), - Endpoint: getSceneStringValue(i, "endpoint"), + Endpoint: getSceneStringValue(0, "endpoint"), UpdatedAt: epochTime, } } @@ -1165,9 +1181,7 @@ func makeScene(i int) *models.Scene { PerformerIDs: models.NewRelatedIDs(pids), TagIDs: models.NewRelatedIDs(tids), Groups: models.NewRelatedGroups(groups), - StashIDs: models.NewRelatedStashIDs([]models.StashID{ - sceneStashID(i), - }), + StashIDs: models.NewRelatedStashIDs(sceneStashIDs(i)), PlayDuration: getScenePlayDuration(i), ResumeTime: getSceneResumeTime(i), } @@ -1538,7 +1552,7 @@ func getIgnoreAutoTag(index int) bool { func performerStashID(i int) models.StashID { return models.StashID{ StashID: getPerformerStringValue(i, "stashid"), - Endpoint: getPerformerStringValue(i, "endpoint"), + Endpoint: getPerformerStringValue(0, "endpoint"), } } @@ -1688,6 +1702,13 @@ func getTagChildCount(id int) int { return 0 } +func tagStashID(i int) models.StashID { + return models.StashID{ + StashID: getTagStringValue(i, "stashid"), + Endpoint: getTagStringValue(0, "endpoint"), + } +} + // createTags creates n tags with plain Name and o tags with camel cased NaMe included func createTags(ctx context.Context, tqb models.TagReaderWriter, n int, o int) error { const namePlain = "Name" @@ -1709,6 +1730,12 @@ func createTags(ctx context.Context, tqb models.TagReaderWriter, n int, o int) e IgnoreAutoTag: getIgnoreAutoTag(i), } + if (index+1)%5 != 0 { + tag.StashIDs = models.NewRelatedStashIDs([]models.StashID{ + tagStashID(i), + }) + } + err := tqb.Create(ctx, &tag) if err != nil { @@ -1738,7 +1765,19 @@ func getStudioNullStringValue(index int, field string) string { return ret.String } -func createStudio(ctx context.Context, sqb *sqlite.StudioStore, name string, parentID *int) (*models.Studio, error) { +func getStudioCustomFields(index int) map[string]interface{} { + if index%5 == 0 { + return nil + } + + return map[string]interface{}{ + "string": getStudioStringValue(index, "custom"), + "int": int64(index % 5), + "real": float64(index) / 10, + } +} + +func createStudio(ctx context.Context, sqb *sqlite.StudioStore, name string, parentID *int, customFields map[string]interface{}) (*models.Studio, error) { studio := models.Studio{ Name: name, } @@ -1747,7 +1786,7 @@ func createStudio(ctx context.Context, sqb *sqlite.StudioStore, name string, par studio.ParentID = parentID } - err := createStudioFromModel(ctx, sqb, &studio) + err := createStudioFromModel(ctx, sqb, &studio, customFields) if err != nil { return nil, err } @@ -1755,8 +1794,11 @@ func createStudio(ctx context.Context, sqb *sqlite.StudioStore, name string, par return &studio, nil } -func createStudioFromModel(ctx context.Context, sqb *sqlite.StudioStore, studio *models.Studio) error { - err := sqb.Create(ctx, studio) +func createStudioFromModel(ctx context.Context, sqb *sqlite.StudioStore, studio *models.Studio, customFields map[string]interface{}) error { + err := sqb.Create(ctx, &models.CreateStudioInput{ + Studio: studio, + CustomFields: customFields, + }) if err != nil { return fmt.Errorf("Error creating studio %v+: %s", studio, err.Error()) @@ -1770,6 +1812,24 @@ func getStudioBoolValue(index int) bool { return index == 1 } +func getStudioEmptyString(index int, field string) string { + v := getPrefixedNullStringValue("studio", index, field) + if !v.Valid { + return "" + } + + return v.String +} + +func getStudioStringList(index int, field string) []string { + v := getStudioEmptyString(index, field) + if v == "" { + return []string{} + } + + return []string{v} +} + // createStudios creates n studios with plain Name and o studios with camel cased NaMe included func createStudios(ctx context.Context, n int, o int) error { sqb := db.Studio @@ -1790,7 +1850,7 @@ func createStudios(ctx context.Context, n int, o int) error { tids := indexesToIDs(tagIDs, studioTags[i]) studio := models.Studio{ Name: name, - URL: getStudioStringValue(index, urlField), + URLs: models.NewRelatedStrings(getStudioStringList(i, urlField)), Favorite: getStudioBoolValue(index), IgnoreAutoTag: getIgnoreAutoTag(i), TagIDs: models.NewRelatedIDs(tids), @@ -1800,7 +1860,7 @@ func createStudios(ctx context.Context, n int, o int) error { alias := getStudioStringValue(i, "Alias") studio.Aliases = models.NewRelatedStrings([]string{alias}) } - err := createStudioFromModel(ctx, sqb, &studio) + err := createStudioFromModel(ctx, sqb, &studio, getStudioCustomFields(i)) if err != nil { return err diff --git a/pkg/sqlite/sql.go b/pkg/sqlite/sql.go index 780d2e988..0b55af8db 100644 --- a/pkg/sqlite/sql.go +++ b/pkg/sqlite/sql.go @@ -137,6 +137,8 @@ func getCountSort(primaryTable, joinTable, primaryFK, direction string) string { return fmt.Sprintf(" ORDER BY (SELECT COUNT(*) FROM %s AS sort WHERE sort.%s = %s.id) %s", joinTable, primaryFK, primaryTable, getSortDirection(direction)) } +// getStringSearchClause returns a sqlClause for searching strings in the provided columns. +// It is used for includes and excludes string criteria. func getStringSearchClause(columns []string, q string, not bool) sqlClause { var likeClauses []string var args []interface{} @@ -362,3 +364,13 @@ func coalesce(column string) string { func like(v string) string { return "%" + v + "%" } + +type sqlTable string + +func (t sqlTable) Name() string { + return string(t) +} + +func (t sqlTable) Col(n string) string { + return fmt.Sprintf("%s.%s", string(t), n) +} diff --git a/pkg/sqlite/stash_id_test.go b/pkg/sqlite/stash_id_test.go index 10949b475..a273c7960 100644 --- a/pkg/sqlite/stash_id_test.go +++ b/pkg/sqlite/stash_id_test.go @@ -27,8 +27,9 @@ func testStashIDReaderWriter(ctx context.Context, t *testing.T, r stashIDReaderW const stashIDStr = "stashID" const endpoint = "endpoint" stashID := models.StashID{ - StashID: stashIDStr, - Endpoint: endpoint, + StashID: stashIDStr, + Endpoint: endpoint, + UpdatedAt: epochTime, } // update stash ids and ensure was updated diff --git a/pkg/sqlite/studio.go b/pkg/sqlite/studio.go index 9217fbcdb..d0c5c220c 100644 --- a/pkg/sqlite/studio.go +++ b/pkg/sqlite/studio.go @@ -15,11 +15,16 @@ import ( "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/studio" + "github.com/stashapp/stash/pkg/utils" ) const ( - studioTable = "studios" - studioIDColumn = "studio_id" + studioTable = "studios" + studioIDColumn = "studio_id" + + studioURLsTable = "studio_urls" + studioURLColumn = "url" + studioAliasesTable = "studio_aliases" studioAliasColumn = "alias" studioParentIDColumn = "parent_id" @@ -31,7 +36,6 @@ const ( type studioRow struct { ID int `db:"id" goqu:"skipinsert"` Name zero.String `db:"name"` - URL zero.String `db:"url"` ParentID null.Int `db:"parent_id,omitempty"` CreatedAt Timestamp `db:"created_at"` UpdatedAt Timestamp `db:"updated_at"` @@ -48,7 +52,6 @@ type studioRow struct { func (r *studioRow) fromStudio(o models.Studio) { r.ID = o.ID r.Name = zero.StringFrom(o.Name) - r.URL = zero.StringFrom(o.URL) r.ParentID = intFromPtr(o.ParentID) r.CreatedAt = Timestamp{Timestamp: o.CreatedAt} r.UpdatedAt = Timestamp{Timestamp: o.UpdatedAt} @@ -62,7 +65,6 @@ func (r *studioRow) resolve() *models.Studio { ret := &models.Studio{ ID: r.ID, Name: r.Name.String, - URL: r.URL.String, ParentID: nullIntPtr(r.ParentID), CreatedAt: r.CreatedAt.Timestamp, UpdatedAt: r.UpdatedAt.Timestamp, @@ -81,7 +83,6 @@ type studioRowRecord struct { func (r *studioRowRecord) fromPartial(o models.StudioPartial) { r.setNullString("name", o.Name) - r.setNullString("url", o.URL) r.setNullInt("parent_id", o.ParentID) r.setTimestamp("created_at", o.CreatedAt) r.setTimestamp("updated_at", o.UpdatedAt) @@ -133,13 +134,14 @@ var ( }, fkColumn: tagIDColumn, foreignTable: tagTable, - orderBy: "COALESCE(tags.sort_name, tags.name) ASC", + orderBy: tagTableSortSQL, }, } ) type StudioStore struct { blobJoinQueryBuilder + customFieldsStore tagRelationshipStore tableMgr *table @@ -151,6 +153,10 @@ func NewStudioStore(blobStore *BlobStore) *StudioStore { blobStore: blobStore, joinTable: studioTable, }, + customFieldsStore: customFieldsStore{ + table: studiosCustomFieldsTable, + fk: studiosCustomFieldsTable.Col(studioIDColumn), + }, tagRelationshipStore: tagRelationshipStore{ idRelationshipStore: idRelationshipStore{ joinTable: studiosTagsTableMgr, @@ -169,11 +175,11 @@ func (qb *StudioStore) selectDataset() *goqu.SelectDataset { return dialect.From(qb.table()).Select(qb.table().All()) } -func (qb *StudioStore) Create(ctx context.Context, newObject *models.Studio) error { +func (qb *StudioStore) Create(ctx context.Context, newObject *models.CreateStudioInput) error { var err error var r studioRow - r.fromStudio(*newObject) + r.fromStudio(*newObject.Studio) id, err := qb.tableMgr.insertID(ctx, r) if err != nil { @@ -181,7 +187,7 @@ func (qb *StudioStore) Create(ctx context.Context, newObject *models.Studio) err } if newObject.Aliases.Loaded() { - if err := studio.EnsureAliasesUnique(ctx, id, newObject.Aliases.List(), qb); err != nil { + if err := studio.ValidateAliases(ctx, id, newObject.Aliases.List(), qb); err != nil { return err } @@ -190,6 +196,13 @@ func (qb *StudioStore) Create(ctx context.Context, newObject *models.Studio) err } } + if newObject.URLs.Loaded() { + const startPos = 0 + if err := studiosURLsTableMgr.insertJoins(ctx, id, startPos, newObject.URLs.List()); err != nil { + return err + } + } + if err := qb.tagRelationshipStore.createRelationships(ctx, id, newObject.TagIDs); err != nil { return err } @@ -200,12 +213,17 @@ func (qb *StudioStore) Create(ctx context.Context, newObject *models.Studio) err } } + const partial = false + if err := qb.setCustomFields(ctx, id, newObject.CustomFields, partial); err != nil { + return err + } + updated, err := qb.find(ctx, id) if err != nil { return fmt.Errorf("finding after create: %w", err) } - *newObject = *updated + *newObject.Studio = *updated return nil } @@ -225,11 +243,13 @@ func (qb *StudioStore) UpdatePartial(ctx context.Context, input models.StudioPar } if input.Aliases != nil { - if err := studio.EnsureAliasesUnique(ctx, input.ID, input.Aliases.Values, qb); err != nil { + if err := studiosAliasesTableMgr.modifyJoins(ctx, input.ID, input.Aliases.Values, input.Aliases.Mode); err != nil { return nil, err } + } - if err := studiosAliasesTableMgr.modifyJoins(ctx, input.ID, input.Aliases.Values, input.Aliases.Mode); err != nil { + if input.URLs != nil { + if err := studiosURLsTableMgr.modifyJoins(ctx, input.ID, input.URLs.Values, input.URLs.Mode); err != nil { return nil, err } } @@ -244,13 +264,17 @@ func (qb *StudioStore) UpdatePartial(ctx context.Context, input models.StudioPar } } - return qb.Find(ctx, input.ID) + if err := qb.SetCustomFields(ctx, input.ID, input.CustomFields); err != nil { + return nil, err + } + + return qb.find(ctx, input.ID) } // This is only used by the Import/Export functionality -func (qb *StudioStore) Update(ctx context.Context, updatedObject *models.Studio) error { +func (qb *StudioStore) Update(ctx context.Context, updatedObject *models.UpdateStudioInput) error { var r studioRow - r.fromStudio(*updatedObject) + r.fromStudio(*updatedObject.Studio) if err := qb.tableMgr.updateByID(ctx, updatedObject.ID, r); err != nil { return err @@ -262,6 +286,12 @@ func (qb *StudioStore) Update(ctx context.Context, updatedObject *models.Studio) } } + if updatedObject.URLs.Loaded() { + if err := studiosURLsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.URLs.List()); err != nil { + return err + } + } + if err := qb.tagRelationshipStore.replaceRelationships(ctx, updatedObject.ID, updatedObject.TagIDs); err != nil { return err } @@ -272,6 +302,10 @@ func (qb *StudioStore) Update(ctx context.Context, updatedObject *models.Studio) } } + if err := qb.SetCustomFields(ctx, updatedObject.ID, updatedObject.CustomFields); err != nil { + return err + } + return nil } @@ -507,7 +541,7 @@ func (qb *StudioStore) QueryForAutoTag(ctx context.Context, words []string) ([]* ret, err := qb.findBySubquery(ctx, sq) if err != nil { - return nil, fmt.Errorf("getting performers for autotag: %w", err) + return nil, fmt.Errorf("getting studios for autotag: %w", err) } return ret, nil @@ -576,14 +610,45 @@ func (qb *StudioStore) QueryCount(ctx context.Context, studioFilter *models.Stud return query.executeCount(ctx) } +func (qb *StudioStore) sortByScenesDuration(direction string) string { + return fmt.Sprintf(` ORDER BY ( + SELECT COALESCE(SUM(video_files.duration), 0) + FROM %s + LEFT JOIN %s ON %s.%s = %s.id + LEFT JOIN video_files ON video_files.file_id = %s.file_id + WHERE %s.%s = %s.id + ) %s`, sceneTable, scenesFilesTable, scenesFilesTable, sceneIDColumn, sceneTable, scenesFilesTable, sceneTable, studioIDColumn, studioTable, getSortDirection(direction)) +} + +// used for sorting on performer latest scene +var selectStudioLatestSceneSQL = utils.StrFormat( + "SELECT MAX(date) FROM ("+ + "SELECT {date} FROM {scenes} s "+ + "WHERE s.{studio_id} = {studios}.id"+ + ")", + map[string]interface{}{ + "scenes": sceneTable, + "studios": studioTable, + "studio_id": studioIDColumn, + "date": sceneDateColumn, + }, +) + +func (qb *StudioStore) sortByLatestScene(direction string) string { + // need to get the latest date from scenes + return " ORDER BY (" + selectStudioLatestSceneSQL + ") " + direction +} + var studioSortOptions = sortOptions{ "child_count", "created_at", "galleries_count", "id", "images_count", + "latest_scene", "name", "scenes_count", + "scenes_duration", "random", "rating", "tag_count", @@ -612,12 +677,16 @@ func (qb *StudioStore) getStudioSort(findFilter *models.FindFilterType) (string, sortQuery += getCountSort(studioTable, studiosTagsTable, studioIDColumn, direction) case "scenes_count": sortQuery += getCountSort(studioTable, sceneTable, studioIDColumn, direction) + case "scenes_duration": + sortQuery += qb.sortByScenesDuration(direction) case "images_count": sortQuery += getCountSort(studioTable, imageTable, studioIDColumn, direction) case "galleries_count": sortQuery += getCountSort(studioTable, galleryTable, studioIDColumn, direction) case "child_count": sortQuery += getCountSort(studioTable, studioTable, studioParentIDColumn, direction) + case "latest_scene": + sortQuery += qb.sortByLatestScene(direction) default: sortQuery += getSort(sort, direction, "studios") } @@ -650,3 +719,7 @@ func (qb *StudioStore) GetStashIDs(ctx context.Context, studioID int) ([]models. func (qb *StudioStore) GetAliases(ctx context.Context, studioID int) ([]string, error) { return studiosAliasesTableMgr.get(ctx, studioID) } + +func (qb *StudioStore) GetURLs(ctx context.Context, studioID int) ([]string, error) { + return studiosURLsTableMgr.get(ctx, studioID) +} diff --git a/pkg/sqlite/studio_filter.go b/pkg/sqlite/studio_filter.go index c514364c4..cd7fc4440 100644 --- a/pkg/sqlite/studio_filter.go +++ b/pkg/sqlite/studio_filter.go @@ -55,7 +55,7 @@ func (qb *studioFilterHandler) criterionHandler() criterionHandler { return compoundHandler{ stringCriterionHandler(studioFilter.Name, studioTable+".name"), stringCriterionHandler(studioFilter.Details, studioTable+".details"), - stringCriterionHandler(studioFilter.URL, studioTable+".url"), + qb.urlsCriterionHandler(studioFilter.URL), intCriterionHandler(studioFilter.Rating100, studioTable+".rating", nil), boolCriterionHandler(studioFilter.Favorite, studioTable+".favorite", nil), boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag", nil), @@ -72,6 +72,12 @@ func (qb *studioFilterHandler) criterionHandler() criterionHandler { stashIDTableAs: "studio_stash_ids", parentIDCol: "studios.id", }, + &stashIDsCriterionHandler{ + c: studioFilter.StashIDsEndpoint, + stashIDRepository: &studioRepository.stashIDs, + stashIDTableAs: "studio_stash_ids", + parentIDCol: "studios.id", + }, qb.isMissingCriterionHandler(studioFilter.IsMissing), qb.tagCountCriterionHandler(studioFilter.TagCount), @@ -111,6 +117,13 @@ func (qb *studioFilterHandler) criterionHandler() criterionHandler { studioRepository.galleries.innerJoin(f, "", "studios.id") }, }, + + &customFieldsFilterHandler{ + table: studiosCustomFieldsTable.GetTable(), + fkCol: studioIDColumn, + c: studioFilter.CustomFields, + idCol: "studios.id", + }, } } @@ -118,6 +131,9 @@ func (qb *studioFilterHandler) isMissingCriterionHandler(isMissing *string) crit return func(ctx context.Context, f *filterBuilder) { if isMissing != nil && *isMissing != "" { switch *isMissing { + case "url": + studiosURLsTableMgr.join(f, "", "studios.id") + f.addWhere("studio_urls.url IS NULL") case "image": f.addWhere("studios.image_blob IS NULL") case "stash_id": @@ -202,6 +218,20 @@ func (qb *studioFilterHandler) aliasCriterionHandler(alias *models.StringCriteri return h.handler(alias) } +func (qb *studioFilterHandler) urlsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc { + h := stringListCriterionHandlerBuilder{ + primaryTable: studioTable, + primaryFK: studioIDColumn, + joinTable: studioURLsTable, + stringColumn: studioURLColumn, + addJoinTable: func(f *filterBuilder) { + studiosURLsTableMgr.join(f, "", "studios.id") + }, + } + + return h.handler(url) +} + func (qb *studioFilterHandler) childCountCriterionHandler(childCount *models.IntCriterionInput) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if childCount != nil { diff --git a/pkg/sqlite/studio_test.go b/pkg/sqlite/studio_test.go index c327a6316..074c77d6f 100644 --- a/pkg/sqlite/studio_test.go +++ b/pkg/sqlite/studio_test.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/stashapp/stash/pkg/models" "github.com/stretchr/testify/assert" @@ -47,6 +48,559 @@ func TestStudioFindByName(t *testing.T) { }) } +func loadStudioRelationships(ctx context.Context, expected models.Studio, actual *models.Studio) error { + if expected.Aliases.Loaded() { + if err := actual.LoadAliases(ctx, db.Studio); err != nil { + return err + } + } + if expected.URLs.Loaded() { + if err := actual.LoadURLs(ctx, db.Studio); err != nil { + return err + } + } + if expected.TagIDs.Loaded() { + if err := actual.LoadTagIDs(ctx, db.Studio); err != nil { + return err + } + } + if expected.StashIDs.Loaded() { + if err := actual.LoadStashIDs(ctx, db.Studio); err != nil { + return err + } + } + + return nil +} + +func Test_StudioStore_Create(t *testing.T) { + var ( + name = "name" + details = "details" + url = "url" + rating = 3 + aliases = []string{"alias1", "alias2"} + ignoreAutoTag = true + favorite = true + endpoint1 = "endpoint1" + endpoint2 = "endpoint2" + stashID1 = "stashid1" + stashID2 = "stashid2" + createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + ) + + tests := []struct { + name string + newObject models.CreateStudioInput + wantErr bool + }{ + { + "full", + models.CreateStudioInput{ + Studio: &models.Studio{ + Name: name, + URLs: models.NewRelatedStrings([]string{url}), + Favorite: favorite, + Rating: &rating, + Details: details, + IgnoreAutoTag: ignoreAutoTag, + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithStudio], tagIDs[tagIdx1WithDupName]}), + Aliases: models.NewRelatedStrings(aliases), + StashIDs: models.NewRelatedStashIDs([]models.StashID{ + { + StashID: stashID1, + Endpoint: endpoint1, + UpdatedAt: epochTime, + }, + { + StashID: stashID2, + Endpoint: endpoint2, + UpdatedAt: epochTime, + }, + }), + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, + CustomFields: testCustomFields, + }, + false, + }, + { + "invalid tag id", + models.CreateStudioInput{ + Studio: &models.Studio{ + Name: name, + TagIDs: models.NewRelatedIDs([]int{invalidID}), + }, + }, + true, + }, + } + + qb := db.Studio + + for _, tt := range tests { + runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { + assert := assert.New(t) + + p := tt.newObject + if err := qb.Create(ctx, &p); (err != nil) != tt.wantErr { + t.Errorf("StudioStore.Create() error = %v, wantErr = %v", err, tt.wantErr) + } + + if tt.wantErr { + assert.Zero(p.ID) + return + } + + assert.NotZero(p.ID) + + copy := *tt.newObject.Studio + copy.ID = p.ID + + // load relationships + if err := loadStudioRelationships(ctx, copy, p.Studio); err != nil { + t.Errorf("loadStudioRelationships() error = %v", err) + return + } + + assert.Equal(copy, *p.Studio) + + // ensure can find the Studio + found, err := qb.Find(ctx, p.ID) + if err != nil { + t.Errorf("StudioStore.Find() error = %v", err) + } + + if !assert.NotNil(found) { + return + } + + // load relationships + if err := loadStudioRelationships(ctx, copy, found); err != nil { + t.Errorf("loadStudioRelationships() error = %v", err) + return + } + assert.Equal(copy, *found) + + // ensure custom fields are set + cf, err := qb.GetCustomFields(ctx, p.ID) + if err != nil { + t.Errorf("StudioStore.GetCustomFields() error = %v", err) + return + } + + assert.Equal(tt.newObject.CustomFields, cf) + + return + }) + } +} + +func Test_StudioStore_Update(t *testing.T) { + var ( + name = "name" + details = "details" + url = "url" + rating = 3 + aliases = []string{"aliasX", "aliasY"} + ignoreAutoTag = true + favorite = true + endpoint1 = "endpoint1" + endpoint2 = "endpoint2" + stashID1 = "stashid1" + stashID2 = "stashid2" + createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + ) + + tests := []struct { + name string + updatedObject models.UpdateStudioInput + wantErr bool + }{ + { + "full", + models.UpdateStudioInput{ + Studio: &models.Studio{ + ID: studioIDs[studioIdxWithGallery], + Name: name, + URLs: models.NewRelatedStrings([]string{url}), + Favorite: favorite, + Rating: &rating, + Details: details, + IgnoreAutoTag: ignoreAutoTag, + Aliases: models.NewRelatedStrings(aliases), + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithStudio]}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{ + { + StashID: stashID1, + Endpoint: endpoint1, + UpdatedAt: epochTime, + }, + { + StashID: stashID2, + Endpoint: endpoint2, + UpdatedAt: epochTime, + }, + }), + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, + }, + false, + }, + { + "clear nullables", + models.UpdateStudioInput{ + Studio: &models.Studio{ + ID: studioIDs[studioIdxWithGallery], + Name: name, // name is mandatory + URLs: models.NewRelatedStrings([]string{}), + Aliases: models.NewRelatedStrings([]string{}), + TagIDs: models.NewRelatedIDs([]int{}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{}), + }, + }, + false, + }, + { + "clear tag ids", + models.UpdateStudioInput{ + Studio: &models.Studio{ + ID: studioIDs[sceneIdxWithTag], + Name: name, // name is mandatory + TagIDs: models.NewRelatedIDs([]int{}), + }, + }, + false, + }, + { + "set custom fields", + models.UpdateStudioInput{ + Studio: &models.Studio{ + ID: studioIDs[studioIdxWithGallery], + Name: name, // name is mandatory + }, + CustomFields: models.CustomFieldsInput{ + Full: testCustomFields, + }, + }, + false, + }, + { + "clear custom fields", + models.UpdateStudioInput{ + Studio: &models.Studio{ + ID: studioIDs[studioIdxWithGallery], + Name: name, // name is mandatory + }, + CustomFields: models.CustomFieldsInput{ + Full: map[string]interface{}{}, + }, + }, + false, + }, + { + "invalid tag id", + models.UpdateStudioInput{ + Studio: &models.Studio{ + ID: studioIDs[sceneIdxWithGallery], + Name: name, // name is mandatory + TagIDs: models.NewRelatedIDs([]int{invalidID}), + }, + }, + true, + }, + } + + qb := db.Studio + for _, tt := range tests { + runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { + assert := assert.New(t) + + copy := *tt.updatedObject.Studio + + if err := qb.Update(ctx, &tt.updatedObject); (err != nil) != tt.wantErr { + t.Errorf("StudioStore.Update() error = %v, wantErr %v", err, tt.wantErr) + } + + if tt.wantErr { + return + } + + s, err := qb.Find(ctx, tt.updatedObject.ID) + if err != nil { + t.Errorf("StudioStore.Find() error = %v", err) + } + + // load relationships + if err := loadStudioRelationships(ctx, copy, s); err != nil { + t.Errorf("loadStudioRelationships() error = %v", err) + return + } + + assert.Equal(copy, *s) + + // ensure custom fields are correct + if tt.updatedObject.CustomFields.Full != nil { + cf, err := qb.GetCustomFields(ctx, tt.updatedObject.ID) + if err != nil { + t.Errorf("StudioStore.GetCustomFields() error = %v", err) + return + } + + assert.Equal(tt.updatedObject.CustomFields.Full, cf) + } + }) + } +} + +func clearStudioPartial() models.StudioPartial { + nullString := models.OptionalString{Set: true, Null: true} + nullInt := models.OptionalInt{Set: true, Null: true} + + // leave mandatory fields + return models.StudioPartial{ + URLs: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet}, + Aliases: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet}, + Rating: nullInt, + Details: nullString, + TagIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, + StashIDs: &models.UpdateStashIDs{Mode: models.RelationshipUpdateModeSet}, + } +} + +func Test_StudioStore_UpdatePartial(t *testing.T) { + var ( + name = "name" + details = "details" + url = "url" + aliases = []string{"aliasX", "aliasY"} + rating = 3 + ignoreAutoTag = true + favorite = true + endpoint1 = "endpoint1" + endpoint2 = "endpoint2" + stashID1 = "stashid1" + stashID2 = "stashid2" + createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + ) + + tests := []struct { + name string + id int + partial models.StudioPartial + want models.Studio + wantErr bool + }{ + { + "full", + studioIDs[studioIdxWithDupName], + models.StudioPartial{ + Name: models.NewOptionalString(name), + URLs: &models.UpdateStrings{ + Values: []string{url}, + Mode: models.RelationshipUpdateModeSet, + }, + Aliases: &models.UpdateStrings{ + Values: aliases, + Mode: models.RelationshipUpdateModeSet, + }, + Favorite: models.NewOptionalBool(favorite), + Rating: models.NewOptionalInt(rating), + Details: models.NewOptionalString(details), + IgnoreAutoTag: models.NewOptionalBool(ignoreAutoTag), + TagIDs: &models.UpdateIDs{ + IDs: []int{tagIDs[tagIdx1WithStudio], tagIDs[tagIdx1WithDupName]}, + Mode: models.RelationshipUpdateModeSet, + }, + StashIDs: &models.UpdateStashIDs{ + StashIDs: []models.StashID{ + { + StashID: stashID1, + Endpoint: endpoint1, + UpdatedAt: epochTime, + }, + { + StashID: stashID2, + Endpoint: endpoint2, + UpdatedAt: epochTime, + }, + }, + Mode: models.RelationshipUpdateModeSet, + }, + CreatedAt: models.NewOptionalTime(createdAt), + UpdatedAt: models.NewOptionalTime(updatedAt), + }, + models.Studio{ + ID: studioIDs[studioIdxWithDupName], + Name: name, + URLs: models.NewRelatedStrings([]string{url}), + Aliases: models.NewRelatedStrings(aliases), + Favorite: favorite, + Rating: &rating, + Details: details, + IgnoreAutoTag: ignoreAutoTag, + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithStudio]}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{ + { + StashID: stashID1, + Endpoint: endpoint1, + UpdatedAt: epochTime, + }, + { + StashID: stashID2, + Endpoint: endpoint2, + UpdatedAt: epochTime, + }, + }), + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, + false, + }, + { + "clear all", + studioIDs[studioIdxWithTwoTags], + clearStudioPartial(), + models.Studio{ + ID: studioIDs[studioIdxWithTwoTags], + Name: getStudioStringValue(studioIdxWithTwoTags, "Name"), + Favorite: getStudioBoolValue(studioIdxWithTwoTags), + Aliases: models.NewRelatedStrings([]string{}), + TagIDs: models.NewRelatedIDs([]int{}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{}), + IgnoreAutoTag: getIgnoreAutoTag(studioIdxWithTwoTags), + }, + false, + }, + { + "invalid id", + invalidID, + models.StudioPartial{Name: models.NewOptionalString(name)}, + models.Studio{}, + true, + }, + } + for _, tt := range tests { + qb := db.Studio + + runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { + assert := assert.New(t) + + tt.partial.ID = tt.id + + got, err := qb.UpdatePartial(ctx, tt.partial) + if (err != nil) != tt.wantErr { + t.Errorf("StudioStore.UpdatePartial() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr { + return + } + + if err := loadStudioRelationships(ctx, tt.want, got); err != nil { + t.Errorf("loadStudioRelationships() error = %v", err) + return + } + + assert.Equal(tt.want, *got) + + s, err := qb.Find(ctx, tt.id) + if err != nil { + t.Errorf("StudioStore.Find() error = %v", err) + } + + // load relationships + if err := loadStudioRelationships(ctx, tt.want, s); err != nil { + t.Errorf("loadStudioRelationships() error = %v", err) + return + } + + assert.Equal(tt.want, *s) + }) + } +} + +func Test_StudioStore_UpdatePartialCustomFields(t *testing.T) { + tests := []struct { + name string + id int + partial models.StudioPartial + expected map[string]interface{} // nil to use the partial + }{ + { + "set custom fields", + studioIDs[studioIdxWithGallery], + models.StudioPartial{ + CustomFields: models.CustomFieldsInput{ + Full: testCustomFields, + }, + }, + nil, + }, + { + "clear custom fields", + studioIDs[studioIdxWithGallery], + models.StudioPartial{ + CustomFields: models.CustomFieldsInput{ + Full: map[string]interface{}{}, + }, + }, + nil, + }, + { + "partial custom fields", + studioIDs[studioIdxWithGallery], + models.StudioPartial{ + CustomFields: models.CustomFieldsInput{ + Partial: map[string]interface{}{ + "string": "bbb", + "new_field": "new", + }, + }, + }, + map[string]interface{}{ + "int": int64(2), + "real": 0.7, + "string": "bbb", + "new_field": "new", + }, + }, + } + for _, tt := range tests { + qb := db.Studio + + runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { + assert := assert.New(t) + + tt.partial.ID = tt.id + + _, err := qb.UpdatePartial(ctx, tt.partial) + if err != nil { + t.Errorf("StudioStore.UpdatePartial() error = %v", err) + return + } + + // ensure custom fields are correct + cf, err := qb.GetCustomFields(ctx, tt.id) + if err != nil { + t.Errorf("StudioStore.GetCustomFields() error = %v", err) + return + } + if tt.expected == nil { + assert.Equal(tt.partial.CustomFields.Full, cf) + } else { + assert.Equal(tt.expected, cf) + } + }) + } +} + func TestStudioQueryNameOr(t *testing.T) { const studio1Idx = 1 const studio2Idx = 2 @@ -107,9 +661,16 @@ func TestStudioQueryNameAndUrl(t *testing.T) { studios := queryStudio(ctx, t, sqb, &studioFilter, nil) - assert.Len(t, studios, 1) + if !assert.Len(t, studios, 1) { + return nil + } + + if err := studios[0].LoadURLs(ctx, db.Studio); err != nil { + t.Errorf("Error loading studio relationships: %v", err) + } + assert.Equal(t, studioName, studios[0].Name) - assert.Equal(t, studioUrl, studios[0].URL) + assert.Equal(t, []string{studioUrl}, studios[0].URLs.List()) return nil }) @@ -145,9 +706,13 @@ func TestStudioQueryNameNotUrl(t *testing.T) { studios := queryStudio(ctx, t, sqb, &studioFilter, nil) for _, studio := range studios { + if err := studio.LoadURLs(ctx, db.Studio); err != nil { + t.Errorf("Error loading studio relationships: %v", err) + } + verifyString(t, studio.Name, nameCriterion) urlCriterion.Modifier = models.CriterionModifierNotEquals - verifyString(t, studio.URL, urlCriterion) + verifyStringList(t, studio.URLs.List(), urlCriterion) } return nil @@ -292,13 +857,13 @@ func TestStudioDestroyParent(t *testing.T) { // create parent and child studios if err := withTxn(func(ctx context.Context) error { - createdParent, err := createStudio(ctx, db.Studio, parentName, nil) + createdParent, err := createStudio(ctx, db.Studio, parentName, nil, nil) if err != nil { return fmt.Errorf("Error creating parent studio: %s", err.Error()) } parentID := createdParent.ID - createdChild, err := createStudio(ctx, db.Studio, childName, &parentID) + createdChild, err := createStudio(ctx, db.Studio, childName, &parentID, nil) if err != nil { return fmt.Errorf("Error creating child studio: %s", err.Error()) } @@ -354,13 +919,13 @@ func TestStudioUpdateClearParent(t *testing.T) { // create parent and child studios if err := withTxn(func(ctx context.Context) error { - createdParent, err := createStudio(ctx, db.Studio, parentName, nil) + createdParent, err := createStudio(ctx, db.Studio, parentName, nil, nil) if err != nil { return fmt.Errorf("Error creating parent studio: %s", err.Error()) } parentID := createdParent.ID - createdChild, err := createStudio(ctx, db.Studio, childName, &parentID) + createdChild, err := createStudio(ctx, db.Studio, childName, &parentID, nil) if err != nil { return fmt.Errorf("Error creating child studio: %s", err.Error()) } @@ -395,7 +960,7 @@ func TestStudioUpdateStudioImage(t *testing.T) { // create studio to test against const name = "TestStudioUpdateStudioImage" - created, err := createStudio(ctx, db.Studio, name, nil) + created, err := createStudio(ctx, db.Studio, name, nil, nil) if err != nil { return fmt.Errorf("Error creating studio: %s", err.Error()) } @@ -559,7 +1124,7 @@ func TestStudioStashIDs(t *testing.T) { // create studio to test against const name = "TestStudioStashIDs" - created, err := createStudio(ctx, db.Studio, name, nil) + created, err := createStudio(ctx, db.Studio, name, nil, nil) if err != nil { return fmt.Errorf("Error creating studio: %s", err.Error()) } @@ -659,7 +1224,11 @@ func TestStudioQueryURL(t *testing.T) { verifyFn := func(ctx context.Context, g *models.Studio) { t.Helper() - verifyString(t, g.URL, urlCriterion) + if err := g.LoadURLs(ctx, db.Studio); err != nil { + t.Errorf("Error loading studio relationships: %v", err) + return + } + verifyStringList(t, g.URLs.List(), urlCriterion) } verifyStudioQuery(t, filter, verifyFn) @@ -967,7 +1536,7 @@ func TestStudioAlias(t *testing.T) { // create studio to test against const name = "TestStudioAlias" - created, err := createStudio(ctx, db.Studio, name, nil) + created, err := createStudio(ctx, db.Studio, name, nil, nil) if err != nil { return fmt.Errorf("Error creating studio: %s", err.Error()) } diff --git a/pkg/sqlite/tables.go b/pkg/sqlite/tables.go index 3b0cbe094..bfc5199fe 100644 --- a/pkg/sqlite/tables.go +++ b/pkg/sqlite/tables.go @@ -28,6 +28,8 @@ var ( scenesGroupsJoinTable = goqu.T(groupsScenesTable) scenesURLsJoinTable = goqu.T(scenesURLsTable) + sceneMarkersTagsJoinTable = goqu.T(sceneMarkersTagsTable) + performersAliasesJoinTable = goqu.T(performersAliasesTable) performersURLsJoinTable = goqu.T(performerURLsTable) performersTagsJoinTable = goqu.T(performersTagsTable) @@ -35,8 +37,10 @@ var ( performersCustomFieldsTable = goqu.T("performer_custom_fields") studiosAliasesJoinTable = goqu.T(studioAliasesTable) + studiosURLsJoinTable = goqu.T(studioURLsTable) studiosTagsJoinTable = goqu.T(studiosTagsTable) studiosStashIDsJoinTable = goqu.T("studio_stash_ids") + studiosCustomFieldsTable = goqu.T("studio_custom_fields") groupsURLsJoinTable = goqu.T(groupURLsTable) groupsTagsJoinTable = goqu.T(groupsTagsTable) @@ -44,6 +48,7 @@ var ( tagsAliasesJoinTable = goqu.T(tagAliasesTable) tagRelationsJoinTable = goqu.T(tagRelationsTable) + tagsStashIDsJoinTable = goqu.T("tag_stash_ids") ) var ( @@ -76,7 +81,7 @@ var ( }, fkColumn: imagesTagsJoinTable.Col(tagIDColumn), foreignTable: tagTableMgr, - orderBy: goqu.COALESCE(tagTableMgr.table.Col("sort_name"), tagTableMgr.table.Col("name")).Asc(), + orderBy: tagTableSort, } imagesPerformersTableMgr = &joinTable{ @@ -116,7 +121,7 @@ var ( }, fkColumn: galleriesTagsJoinTable.Col(tagIDColumn), foreignTable: tagTableMgr, - orderBy: goqu.COALESCE(tagTableMgr.table.Col("sort_name"), tagTableMgr.table.Col("name")).Asc(), + orderBy: tagTableSort, } galleriesPerformersTableMgr = &joinTable{ @@ -160,6 +165,16 @@ var ( idColumn: goqu.T(sceneMarkerTable).Col(idColumn), } + sceneMarkersTagsTableMgr = &joinTable{ + table: table{ + table: sceneMarkersTagsJoinTable, + idColumn: sceneMarkersTagsJoinTable.Col(sceneMarkerIDColumn), + }, + fkColumn: sceneMarkersTagsJoinTable.Col(tagIDColumn), + foreignTable: tagTableMgr, + orderBy: tagTableSort, + } + scenesFilesTableMgr = &relatedFilesTable{ table: table{ table: scenesFilesJoinTable, @@ -174,7 +189,7 @@ var ( }, fkColumn: scenesTagsJoinTable.Col(tagIDColumn), foreignTable: tagTableMgr, - orderBy: goqu.COALESCE(tagTableMgr.table.Col("sort_name"), tagTableMgr.table.Col("name")).Asc(), + orderBy: tagTableSort, } scenesPerformersTableMgr = &joinTable{ @@ -282,7 +297,7 @@ var ( }, fkColumn: performersTagsJoinTable.Col(tagIDColumn), foreignTable: tagTableMgr, - orderBy: goqu.COALESCE(tagTableMgr.table.Col("sort_name"), tagTableMgr.table.Col("name")).Asc(), + orderBy: tagTableSort, } performersStashIDsTableMgr = &stashIDTable{ @@ -307,6 +322,14 @@ var ( stringColumn: studiosAliasesJoinTable.Col(studioAliasColumn), } + studiosURLsTableMgr = &orderedValueTable[string]{ + table: table{ + table: studiosURLsJoinTable, + idColumn: studiosURLsJoinTable.Col(studioIDColumn), + }, + valueColumn: studiosURLsJoinTable.Col(studioURLColumn), + } + studiosTagsTableMgr = &joinTable{ table: table{ table: studiosTagsJoinTable, @@ -314,7 +337,7 @@ var ( }, fkColumn: studiosTagsJoinTable.Col(tagIDColumn), foreignTable: tagTableMgr, - orderBy: goqu.COALESCE(tagTableMgr.table.Col("sort_name"), tagTableMgr.table.Col("name")).Asc(), + orderBy: tagTableSort, } studiosStashIDsTableMgr = &stashIDTable{ @@ -331,6 +354,10 @@ var ( idColumn: goqu.T(tagTable).Col(idColumn), } + // formerly: goqu.COALESCE(tagTableMgr.table.Col("sort_name"), tagTableMgr.table.Col("name")).Asc() + tagTableSort = goqu.L("COALESCE(tags.sort_name, tags.name) COLLATE NATURAL_CI").Asc() + tagTableSortSQL = "COALESCE(tags.sort_name, tags.name) COLLATE NATURAL_CI ASC" + tagsAliasesTableMgr = &stringTable{ table: table{ table: tagsAliasesJoinTable, @@ -346,10 +373,17 @@ var ( }, fkColumn: tagRelationsJoinTable.Col(tagParentIDColumn), foreignTable: tagTableMgr, - orderBy: goqu.COALESCE(tagTableMgr.table.Col("sort_name"), tagTableMgr.table.Col("name")).Asc(), + orderBy: tagTableSort, } tagsChildTagsTableMgr = *tagsParentTagsTableMgr.invert() + + tagsStashIDsTableMgr = &stashIDTable{ + table: table{ + table: tagsStashIDsJoinTable, + idColumn: tagsStashIDsJoinTable.Col(tagIDColumn), + }, + } ) var ( @@ -373,7 +407,7 @@ var ( }, fkColumn: groupsTagsJoinTable.Col(tagIDColumn), foreignTable: tagTableMgr, - orderBy: goqu.COALESCE(tagTableMgr.table.Col("sort_name"), tagTableMgr.table.Col("name")).Asc(), + orderBy: tagTableSort, } groupRelationshipTableMgr = &table{ diff --git a/pkg/sqlite/tag.go b/pkg/sqlite/tag.go index 868e5f92f..b1d773290 100644 --- a/pkg/sqlite/tag.go +++ b/pkg/sqlite/tag.go @@ -101,7 +101,8 @@ func (r *tagRowRecord) fromPartial(o models.TagPartial) { type tagRepositoryType struct { repository - aliases stringRepository + aliases stringRepository + stashIDs stashIDRepository scenes joinRepository images joinRepository @@ -122,6 +123,12 @@ var ( }, stringColumn: tagAliasColumn, }, + stashIDs: stashIDRepository{ + repository{ + tableName: "tag_stash_ids", + idColumn: tagIDColumn, + }, + }, scenes: joinRepository{ repository: repository{ tableName: scenesTagsTable, @@ -208,6 +215,12 @@ func (qb *TagStore) Create(ctx context.Context, newObject *models.Tag) error { } } + if newObject.StashIDs.Loaded() { + if err := tagsStashIDsTableMgr.insertJoins(ctx, id, newObject.StashIDs.List()); err != nil { + return err + } + } + updated, err := qb.find(ctx, id) if err != nil { return fmt.Errorf("finding after create: %w", err) @@ -251,6 +264,12 @@ func (qb *TagStore) UpdatePartial(ctx context.Context, id int, partial models.Ta } } + if partial.StashIDs != nil { + if err := tagsStashIDsTableMgr.modifyJoins(ctx, id, partial.StashIDs.StashIDs, partial.StashIDs.Mode); err != nil { + return nil, err + } + } + return qb.find(ctx, id) } @@ -280,6 +299,12 @@ func (qb *TagStore) Update(ctx context.Context, updatedObject *models.Tag) error } } + if updatedObject.StashIDs.Loaded() { + if err := tagsStashIDsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.StashIDs.List()); err != nil { + return err + } + } + return nil } @@ -518,6 +543,24 @@ func (qb *TagStore) FindByNames(ctx context.Context, names []string, nocase bool return ret, nil } +func (qb *TagStore) FindByStashID(ctx context.Context, stashID models.StashID) ([]*models.Tag, error) { + sq := dialect.From(tagsStashIDsJoinTable).Select(tagsStashIDsJoinTable.Col(tagIDColumn)).Where( + tagsStashIDsJoinTable.Col("stash_id").Eq(stashID.StashID), + tagsStashIDsJoinTable.Col("endpoint").Eq(stashID.Endpoint), + ) + + idsQuery := qb.selectDataset().Where( + qb.table().Col(idColumn).In(sq), + ) + + ret, err := qb.getMany(ctx, idsQuery) + if err != nil { + return nil, fmt.Errorf("getting tags for stash ID %s: %w", stashID.StashID, err) + } + + return ret, nil +} + func (qb *TagStore) GetParentIDs(ctx context.Context, relatedID int) ([]int, error) { return tagsParentTagsTableMgr.get(ctx, relatedID) } @@ -660,9 +703,21 @@ var tagSortOptions = sortOptions{ "random", "scene_markers_count", "scenes_count", + "scenes_duration", "updated_at", } +func (qb *TagStore) sortByScenesDuration(direction string) string { + return fmt.Sprintf(` ORDER BY ( + SELECT COALESCE(SUM(video_files.duration), 0) + FROM %s + LEFT JOIN %s ON %s.id = %s.%s + LEFT JOIN %s ON %s.%s = %s.id + LEFT JOIN video_files ON video_files.file_id = %s.file_id + WHERE %s.%s = %s.id + ) %s`, scenesTagsTable, sceneTable, sceneTable, scenesTagsTable, sceneIDColumn, scenesFilesTable, scenesFilesTable, sceneIDColumn, sceneTable, scenesFilesTable, scenesTagsTable, tagIDColumn, tagTable, getSortDirection(direction)) +} + func (qb *TagStore) getDefaultTagSort() string { return getSort("name", "ASC", "tags") } @@ -689,6 +744,8 @@ func (qb *TagStore) getTagSort(query *queryBuilder, findFilter *models.FindFilte sortQuery += fmt.Sprintf(" ORDER BY COALESCE(tags.sort_name, tags.name) COLLATE NATURAL_CI %s", getSortDirection(direction)) case "scenes_count": sortQuery += getCountSort(tagTable, scenesTagsTable, tagIDColumn, direction) + case "scenes_duration": + sortQuery += qb.sortByScenesDuration(direction) case "scene_markers_count": sortQuery += fmt.Sprintf(" ORDER BY (SELECT COUNT(*) FROM scene_markers_tags WHERE tags.id = scene_markers_tags.tag_id)+(SELECT COUNT(*) FROM scene_markers WHERE tags.id = scene_markers.primary_tag_id) %s", getSortDirection(direction)) case "images_count": @@ -774,6 +831,14 @@ func (qb *TagStore) UpdateAliases(ctx context.Context, tagID int, aliases []stri return tagRepository.aliases.replace(ctx, tagID, aliases) } +func (qb *TagStore) GetStashIDs(ctx context.Context, tagID int) ([]models.StashID, error) { + return tagsStashIDsTableMgr.get(ctx, tagID) +} + +func (qb *TagStore) UpdateStashIDs(ctx context.Context, tagID int, stashIDs []models.StashID) error { + return tagsStashIDsTableMgr.replaceJoins(ctx, tagID, stashIDs) +} + func (qb *TagStore) Merge(ctx context.Context, source []int, destination int) error { if len(source) == 0 { return nil @@ -799,9 +864,12 @@ func (qb *TagStore) Merge(ctx context.Context, source []int, destination int) er imagesTagsTable: imageIDColumn, "performers_tags": "performer_id", "studios_tags": "studio_id", + groupsTagsTable: "group_id", } args = append(args, destination) + + // for each table, update source tag ids to destination tag id, ignoring duplicates for table, idColumn := range tagTables { _, err := dbWrapper.Exec(ctx, `UPDATE OR IGNORE `+table+` SET tag_id = ? @@ -834,6 +902,19 @@ AND NOT EXISTS(SELECT 1 FROM `+table+` o WHERE o.`+idColumn+` = `+table+`.`+idCo return err } + // Merge StashIDs - move all source StashIDs to destination (ignoring duplicates) + _, err = dbWrapper.Exec(ctx, `UPDATE OR IGNORE `+"tag_stash_ids"+` +SET tag_id = ? +WHERE tag_id IN `+inBinding, args...) + if err != nil { + return err + } + + // Delete remaining source StashIDs that couldn't be moved (duplicates) + if _, err := dbWrapper.Exec(ctx, `DELETE FROM tag_stash_ids WHERE tag_id IN `+inBinding, srcArgs...); err != nil { + return err + } + for _, id := range source { err = qb.Destroy(ctx, id) if err != nil { diff --git a/pkg/sqlite/tag_filter.go b/pkg/sqlite/tag_filter.go index f55834f7e..dadc351ee 100644 --- a/pkg/sqlite/tag_filter.go +++ b/pkg/sqlite/tag_filter.go @@ -84,6 +84,20 @@ func (qb *tagFilterHandler) criterionHandler() criterionHandler { tagHierarchyHandler.ChildrenCriterionHandler(tagFilter.Children), tagHierarchyHandler.ParentCountCriterionHandler(tagFilter.ParentCount), tagHierarchyHandler.ChildCountCriterionHandler(tagFilter.ChildCount), + + &stashIDCriterionHandler{ + c: tagFilter.StashIDEndpoint, + stashIDRepository: &tagRepository.stashIDs, + stashIDTableAs: "tag_stash_ids", + parentIDCol: "tags.id", + }, + &stashIDsCriterionHandler{ + c: tagFilter.StashIDsEndpoint, + stashIDRepository: &tagRepository.stashIDs, + stashIDTableAs: "tag_stash_ids", + parentIDCol: "tags.id", + }, + ×tampCriterionHandler{tagFilter.CreatedAt, "tags.created_at", nil}, ×tampCriterionHandler{tagFilter.UpdatedAt, "tags.updated_at", nil}, diff --git a/pkg/sqlite/tag_test.go b/pkg/sqlite/tag_test.go index 5359be785..f1bac19b2 100644 --- a/pkg/sqlite/tag_test.go +++ b/pkg/sqlite/tag_test.go @@ -343,6 +343,165 @@ func queryTags(ctx context.Context, t *testing.T, qb models.TagReader, tagFilter return tags } +func tagsToIDs(i []*models.Tag) []int { + ret := make([]int, len(i)) + for i, v := range i { + ret[i] = v.ID + } + + return ret +} + +func TestTagQuery(t *testing.T) { + var ( + endpoint = tagStashID(tagIdxWithPerformer).Endpoint + stashID = tagStashID(tagIdxWithPerformer).StashID + stashID2 = tagStashID(tagIdx1WithPerformer).StashID + stashIDs = []*string{&stashID, &stashID2} + ) + + tests := []struct { + name string + findFilter *models.FindFilterType + filter *models.TagFilterType + includeIdxs []int + excludeIdxs []int + wantErr bool + }{ + { + "stash id with endpoint", + nil, + &models.TagFilterType{ + StashIDEndpoint: &models.StashIDCriterionInput{ + Endpoint: &endpoint, + StashID: &stashID, + Modifier: models.CriterionModifierEquals, + }, + }, + []int{tagIdxWithPerformer}, + nil, + false, + }, + { + "exclude stash id with endpoint", + nil, + &models.TagFilterType{ + StashIDEndpoint: &models.StashIDCriterionInput{ + Endpoint: &endpoint, + StashID: &stashID, + Modifier: models.CriterionModifierNotEquals, + }, + }, + nil, + []int{tagIdxWithPerformer}, + false, + }, + { + "null stash id with endpoint", + nil, + &models.TagFilterType{ + StashIDEndpoint: &models.StashIDCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierIsNull, + }, + }, + nil, + []int{tagIdxWithPerformer}, + false, + }, + { + "not null stash id with endpoint", + nil, + &models.TagFilterType{ + StashIDEndpoint: &models.StashIDCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierNotNull, + }, + }, + []int{tagIdxWithPerformer}, + nil, + false, + }, + { + "stash ids with endpoint", + nil, + &models.TagFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + StashIDs: stashIDs, + Modifier: models.CriterionModifierEquals, + }, + }, + []int{tagIdxWithPerformer, tagIdx1WithPerformer}, + nil, + false, + }, + { + "exclude stash ids with endpoint", + nil, + &models.TagFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + StashIDs: stashIDs, + Modifier: models.CriterionModifierNotEquals, + }, + }, + nil, + []int{tagIdxWithPerformer, tagIdx1WithPerformer}, + false, + }, + { + "null stash ids with endpoint", + nil, + &models.TagFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierIsNull, + }, + }, + nil, + []int{tagIdxWithPerformer, tagIdx1WithPerformer}, + false, + }, + { + "not null stash ids with endpoint", + nil, + &models.TagFilterType{ + StashIDsEndpoint: &models.StashIDsCriterionInput{ + Endpoint: &endpoint, + Modifier: models.CriterionModifierNotNull, + }, + }, + []int{tagIdxWithPerformer, tagIdx1WithPerformer}, + nil, + false, + }, + } + + for _, tt := range tests { + runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { + assert := assert.New(t) + + tags, _, err := db.Tag.Query(ctx, tt.filter, tt.findFilter) + if (err != nil) != tt.wantErr { + t.Errorf("PerformerStore.Query() error = %v, wantErr %v", err, tt.wantErr) + return + } + + ids := tagsToIDs(tags) + include := indexesToIDs(tagIDs, tt.includeIdxs) + exclude := indexesToIDs(tagIDs, tt.excludeIdxs) + + for _, i := range include { + assert.Contains(ids, i) + } + for _, e := range exclude { + assert.NotContains(ids, e) + } + }) + } +} + func TestTagQueryIsMissingImage(t *testing.T) { withTxn(func(ctx context.Context) error { qb := db.Tag @@ -900,6 +1059,66 @@ func TestTagUpdateAlias(t *testing.T) { } } +func TestTagStashIDs(t *testing.T) { + if err := withTxn(func(ctx context.Context) error { + qb := db.Tag + + // create tag to test against + const name = "TestTagStashIDs" + tag := models.Tag{ + Name: name, + } + err := qb.Create(ctx, &tag) + if err != nil { + return fmt.Errorf("Error creating tag: %s", err.Error()) + } + + testStashIDReaderWriter(ctx, t, qb, tag.ID) + + return nil + }); err != nil { + t.Error(err.Error()) + } +} + +func TestTagFindByStashID(t *testing.T) { + withTxn(func(ctx context.Context) error { + qb := db.Tag + + // create tag to test against + const name = "TestTagFindByStashID" + const stashID = "stashid" + const endpoint = "endpoint" + tag := models.Tag{ + Name: name, + StashIDs: models.NewRelatedStashIDs([]models.StashID{{StashID: stashID, Endpoint: endpoint}}), + } + err := qb.Create(ctx, &tag) + if err != nil { + return fmt.Errorf("Error creating tag: %s", err.Error()) + } + + // find by stash ID + tags, err := qb.FindByStashID(ctx, models.StashID{StashID: stashID, Endpoint: endpoint}) + if err != nil { + return fmt.Errorf("Error finding by stash ID: %s", err.Error()) + } + + assert.Len(t, tags, 1) + assert.Equal(t, tag.ID, tags[0].ID) + + // find by non-existent stash ID + tags, err = qb.FindByStashID(ctx, models.StashID{StashID: "nonexistent", Endpoint: endpoint}) + if err != nil { + return fmt.Errorf("Error finding by stash ID: %s", err.Error()) + } + + assert.Len(t, tags, 0) + + return nil + }) +} + func TestTagMerge(t *testing.T) { assert := assert.New(t) @@ -931,6 +1150,8 @@ func TestTagMerge(t *testing.T) { tagIdxWithGallery, tagIdx1WithGallery, tagIdx2WithGallery, + tagIdx1WithGroup, + tagIdx2WithGroup, } var srcIDs []int for _, idx := range srcIdxs { @@ -1024,6 +1245,18 @@ func TestTagMerge(t *testing.T) { assert.Contains(studioTagIDs, destID) + // ensure group points to new tag + group, err := db.Group.Find(ctx, groupIDs[groupIdxWithTwoTags]) + if err != nil { + return err + } + if err := group.LoadTagIDs(ctx, db.Group); err != nil { + return err + } + groupTagIDs := group.TagIDs.List() + + assert.Contains(groupTagIDs, destID) + return nil }); err != nil { t.Error(err.Error()) diff --git a/pkg/sqlite/tx.go b/pkg/sqlite/tx.go index a2e272aa9..b6701dc81 100644 --- a/pkg/sqlite/tx.go +++ b/pkg/sqlite/tx.go @@ -16,8 +16,8 @@ const ( type dbReader interface { Get(dest interface{}, query string, args ...interface{}) error - Select(dest interface{}, query string, args ...interface{}) error - Queryx(query string, args ...interface{}) (*sqlx.Rows, error) + GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error + SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) } @@ -54,7 +54,7 @@ func (*dbWrapperType) Get(ctx context.Context, dest interface{}, query string, a } start := time.Now() - err = tx.Get(dest, query, args...) + err = tx.GetContext(ctx, dest, query, args...) logSQL(start, query, args...) return sqlError(err, query, args...) @@ -67,7 +67,7 @@ func (*dbWrapperType) Select(ctx context.Context, dest interface{}, query string } start := time.Now() - err = tx.Select(dest, query, args...) + err = tx.SelectContext(ctx, dest, query, args...) logSQL(start, query, args...) return sqlError(err, query, args...) @@ -80,23 +80,14 @@ func (*dbWrapperType) Queryx(ctx context.Context, query string, args ...interfac } start := time.Now() - ret, err := tx.Queryx(query, args...) + ret, err := tx.QueryxContext(ctx, query, args...) logSQL(start, query, args...) return ret, sqlError(err, query, args...) } func (*dbWrapperType) QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) { - tx, err := getDBReader(ctx) - if err != nil { - return nil, sqlError(err, query, args...) - } - - start := time.Now() - ret, err := tx.QueryxContext(ctx, query, args...) - logSQL(start, query, args...) - - return ret, sqlError(err, query, args...) + return dbWrapper.Queryx(ctx, query, args...) } func (*dbWrapperType) NamedExec(ctx context.Context, query string, arg interface{}) (sql.Result, error) { @@ -106,7 +97,7 @@ func (*dbWrapperType) NamedExec(ctx context.Context, query string, arg interface } start := time.Now() - ret, err := tx.NamedExec(query, arg) + ret, err := tx.NamedExecContext(ctx, query, arg) logSQL(start, query, arg) return ret, sqlError(err, query, arg) @@ -119,7 +110,7 @@ func (*dbWrapperType) Exec(ctx context.Context, query string, args ...interface{ } start := time.Now() - ret, err := tx.Exec(query, args...) + ret, err := tx.ExecContext(ctx, query, args...) logSQL(start, query, args...) return ret, sqlError(err, query, args...) diff --git a/pkg/stashbox/graphql/generated_client.go b/pkg/stashbox/graphql/generated_client.go index f0224900f..640a1c893 100644 --- a/pkg/stashbox/graphql/generated_client.go +++ b/pkg/stashbox/graphql/generated_client.go @@ -17,6 +17,8 @@ type StashBoxGraphQLClient interface { FindPerformerByID(ctx context.Context, id string, interceptors ...clientv2.RequestInterceptor) (*FindPerformerByID, error) FindSceneByID(ctx context.Context, id string, interceptors ...clientv2.RequestInterceptor) (*FindSceneByID, error) FindStudio(ctx context.Context, id *string, name *string, interceptors ...clientv2.RequestInterceptor) (*FindStudio, error) + FindTag(ctx context.Context, id *string, name *string, interceptors ...clientv2.RequestInterceptor) (*FindTag, error) + QueryTags(ctx context.Context, input TagQueryInput, interceptors ...clientv2.RequestInterceptor) (*QueryTags, error) SubmitFingerprint(ctx context.Context, input FingerprintSubmission, interceptors ...clientv2.RequestInterceptor) (*SubmitFingerprint, error) Me(ctx context.Context, interceptors ...clientv2.RequestInterceptor) (*Me, error) SubmitSceneDraft(ctx context.Context, input SceneDraftInput, interceptors ...clientv2.RequestInterceptor) (*SubmitSceneDraft, error) @@ -82,11 +84,12 @@ func (t *ImageFragment) GetHeight() int { } type StudioFragment struct { - Name string "json:\"name\" graphql:\"name\"" - ID string "json:\"id\" graphql:\"id\"" - Urls []*URLFragment "json:\"urls\" graphql:\"urls\"" - Parent *StudioFragment_Parent "json:\"parent,omitempty\" graphql:\"parent\"" - Images []*ImageFragment "json:\"images\" graphql:\"images\"" + Name string "json:\"name\" graphql:\"name\"" + ID string "json:\"id\" graphql:\"id\"" + Aliases []string "json:\"aliases\" graphql:\"aliases\"" + Urls []*URLFragment "json:\"urls\" graphql:\"urls\"" + Parent *StudioFragment_Parent "json:\"parent,omitempty\" graphql:\"parent\"" + Images []*ImageFragment "json:\"images\" graphql:\"images\"" } func (t *StudioFragment) GetName() string { @@ -101,6 +104,12 @@ func (t *StudioFragment) GetID() string { } return t.ID } +func (t *StudioFragment) GetAliases() []string { + if t == nil { + t = &StudioFragment{} + } + return t.Aliases +} func (t *StudioFragment) GetUrls() []*URLFragment { if t == nil { t = &StudioFragment{} @@ -635,6 +644,24 @@ func (t *FindStudio_FindStudio_StudioFragment_Parent) GetName() string { return t.Name } +type QueryTags_QueryTags struct { + Count int "json:\"count\" graphql:\"count\"" + Tags []*TagFragment "json:\"tags\" graphql:\"tags\"" +} + +func (t *QueryTags_QueryTags) GetCount() int { + if t == nil { + t = &QueryTags_QueryTags{} + } + return t.Count +} +func (t *QueryTags_QueryTags) GetTags() []*TagFragment { + if t == nil { + t = &QueryTags_QueryTags{} + } + return t.Tags +} + type Me_Me struct { Name string "json:\"name\" graphql:\"name\"" } @@ -756,6 +783,28 @@ func (t *FindStudio) GetFindStudio() *StudioFragment { return t.FindStudio } +type FindTag struct { + FindTag *TagFragment "json:\"findTag,omitempty\" graphql:\"findTag\"" +} + +func (t *FindTag) GetFindTag() *TagFragment { + if t == nil { + t = &FindTag{} + } + return t.FindTag +} + +type QueryTags struct { + QueryTags QueryTags_QueryTags "json:\"queryTags\" graphql:\"queryTags\"" +} + +func (t *QueryTags) GetQueryTags() *QueryTags_QueryTags { + if t == nil { + t = &QueryTags{} + } + return &t.QueryTags +} + type SubmitFingerprint struct { SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\"" } @@ -845,6 +894,7 @@ fragment ImageFragment on Image { fragment StudioFragment on Studio { name id + aliases urls { ... URLFragment } @@ -980,6 +1030,7 @@ fragment ImageFragment on Image { fragment StudioFragment on Studio { name id + aliases urls { ... URLFragment } @@ -1115,6 +1166,7 @@ fragment ImageFragment on Image { fragment StudioFragment on Studio { name id + aliases urls { ... URLFragment } @@ -1250,6 +1302,7 @@ fragment ImageFragment on Image { fragment StudioFragment on Studio { name id + aliases urls { ... URLFragment } @@ -1543,6 +1596,7 @@ fragment ImageFragment on Image { fragment StudioFragment on Studio { name id + aliases urls { ... URLFragment } @@ -1641,6 +1695,7 @@ const FindStudioDocument = `query FindStudio ($id: ID, $name: String) { fragment StudioFragment on Studio { name id + aliases urls { ... URLFragment } @@ -1682,6 +1737,66 @@ func (c *Client) FindStudio(ctx context.Context, id *string, name *string, inter return &res, nil } +const FindTagDocument = `query FindTag ($id: ID, $name: String) { + findTag(id: $id, name: $name) { + ... TagFragment + } +} +fragment TagFragment on Tag { + name + id +} +` + +func (c *Client) FindTag(ctx context.Context, id *string, name *string, interceptors ...clientv2.RequestInterceptor) (*FindTag, error) { + vars := map[string]any{ + "id": id, + "name": name, + } + + var res FindTag + if err := c.Client.Post(ctx, "FindTag", FindTagDocument, &res, vars, interceptors...); err != nil { + if c.Client.ParseDataWhenErrors { + return &res, err + } + + return nil, err + } + + return &res, nil +} + +const QueryTagsDocument = `query QueryTags ($input: TagQueryInput!) { + queryTags(input: $input) { + count + tags { + ... TagFragment + } + } +} +fragment TagFragment on Tag { + name + id +} +` + +func (c *Client) QueryTags(ctx context.Context, input TagQueryInput, interceptors ...clientv2.RequestInterceptor) (*QueryTags, error) { + vars := map[string]any{ + "input": input, + } + + var res QueryTags + if err := c.Client.Post(ctx, "QueryTags", QueryTagsDocument, &res, vars, interceptors...); err != nil { + if c.Client.ParseDataWhenErrors { + return &res, err + } + + return nil, err + } + + return &res, nil +} + const SubmitFingerprintDocument = `mutation SubmitFingerprint ($input: FingerprintSubmission!) { submitFingerprint(input: $input) } @@ -1783,6 +1898,8 @@ var DocumentOperationNames = map[string]string{ FindPerformerByIDDocument: "FindPerformerByID", FindSceneByIDDocument: "FindSceneByID", FindStudioDocument: "FindStudio", + FindTagDocument: "FindTag", + QueryTagsDocument: "QueryTags", SubmitFingerprintDocument: "SubmitFingerprint", MeDocument: "Me", SubmitSceneDraftDocument: "SubmitSceneDraft", diff --git a/pkg/stashbox/performer.go b/pkg/stashbox/performer.go index 56d7b109e..38824eba1 100644 --- a/pkg/stashbox/performer.go +++ b/pkg/stashbox/performer.go @@ -125,8 +125,8 @@ func translateGender(gender *graphql.GenderEnum) *string { return nil } -func formatMeasurements(m graphql.MeasurementsFragment) *string { - if m.BandSize != nil && m.CupSize != nil && m.Hip != nil && m.Waist != nil { +func formatMeasurements(m *graphql.MeasurementsFragment) *string { + if m != nil && m.BandSize != nil && m.CupSize != nil && m.Hip != nil && m.Waist != nil { ret := fmt.Sprintf("%d%s-%d-%d", *m.BandSize, *m.CupSize, *m.Waist, *m.Hip) return &ret } @@ -209,7 +209,7 @@ func performerFragmentToScrapedPerformer(p graphql.PerformerFragment) *models.Sc Name: &p.Name, Disambiguation: p.Disambiguation, Country: p.Country, - Measurements: formatMeasurements(*p.Measurements), + Measurements: formatMeasurements(p.Measurements), CareerLength: formatCareerLength(p.CareerStartYear, p.CareerEndYear), Tattoos: formatBodyModifications(p.Tattoos), Piercings: formatBodyModifications(p.Piercings), diff --git a/pkg/stashbox/scene.go b/pkg/stashbox/scene.go index 33d427091..64c4defa2 100644 --- a/pkg/stashbox/scene.go +++ b/pkg/stashbox/scene.go @@ -205,7 +205,8 @@ func (c Client) sceneFragmentToScrapedScene(ctx context.Context, s *graphql.Scen for _, t := range s.Tags { st := &models.ScrapedTag{ - Name: t.Name, + Name: t.Name, + RemoteSiteID: &t.ID, } ss.Tags = append(ss.Tags, st) } @@ -242,8 +243,9 @@ type SceneDraft struct { Performers []*models.Performer // StashIDs must be loaded Studio *models.Studio - Tags []*models.Tag - Cover []byte + // StashIDs must be loaded + Tags []*models.Tag + Cover []byte } func (c Client) SubmitSceneDraft(ctx context.Context, d SceneDraft) (*string, error) { @@ -347,7 +349,17 @@ func newSceneDraftInput(d SceneDraft, endpoint string) graphql.SceneDraftInput { var tags []*graphql.DraftEntityInput sceneTags := d.Tags for _, tag := range sceneTags { - tags = append(tags, &graphql.DraftEntityInput{Name: tag.Name}) + tagDraft := graphql.DraftEntityInput{Name: tag.Name} + + stashIDs := tag.StashIDs.List() + for _, stashID := range stashIDs { + if stashID.Endpoint == endpoint { + tagDraft.ID = &stashID.StashID + break + } + } + + tags = append(tags, &tagDraft) } draft.Tags = tags diff --git a/pkg/stashbox/studio.go b/pkg/stashbox/studio.go index b424ac6fa..8934972f2 100644 --- a/pkg/stashbox/studio.go +++ b/pkg/stashbox/studio.go @@ -2,6 +2,7 @@ package stashbox import ( "context" + "strings" "github.com/google/uuid" "github.com/stashapp/stash/pkg/models" @@ -63,13 +64,19 @@ func studioFragmentToScrapedStudio(s graphql.StudioFragment) *models.ScrapedStud images = append(images, image.URL) } + aliases := strings.Join(s.Aliases, ", ") + st := &models.ScrapedStudio{ Name: s.Name, - URL: findURL(s.Urls, "HOME"), + Aliases: &aliases, Images: images, RemoteSiteID: &s.ID, } + for _, u := range s.Urls { + st.URLs = append(st.URLs, u.URL) + } + if len(st.Images) > 0 { st.Image = &st.Images[0] } diff --git a/pkg/stashbox/tag.go b/pkg/stashbox/tag.go new file mode 100644 index 000000000..df2ecbcc0 --- /dev/null +++ b/pkg/stashbox/tag.go @@ -0,0 +1,67 @@ +package stashbox + +import ( + "context" + + "github.com/google/uuid" + "github.com/stashapp/stash/pkg/models" + "github.com/stashapp/stash/pkg/stashbox/graphql" +) + +// QueryTag searches for tags by name or ID. +// If query is a valid UUID, it searches by ID (returns single result). +// Otherwise, it searches by name (returns multiple results). +func (c Client) QueryTag(ctx context.Context, query string) ([]*models.ScrapedTag, error) { + _, err := uuid.Parse(query) + if err == nil { + // Query is a UUID, use findTag for exact match + return c.findTagByID(ctx, query) + } + // Otherwise search by name + return c.queryTagsByName(ctx, query) +} + +func (c Client) findTagByID(ctx context.Context, id string) ([]*models.ScrapedTag, error) { + tag, err := c.client.FindTag(ctx, &id, nil) + if err != nil { + return nil, err + } + + if tag.FindTag == nil { + return nil, nil + } + + return []*models.ScrapedTag{{ + Name: tag.FindTag.Name, + RemoteSiteID: &tag.FindTag.ID, + }}, nil +} + +func (c Client) queryTagsByName(ctx context.Context, name string) ([]*models.ScrapedTag, error) { + input := graphql.TagQueryInput{ + Name: &name, + Page: 1, + PerPage: 25, + Direction: graphql.SortDirectionEnumAsc, + Sort: graphql.TagSortEnumName, + } + + result, err := c.client.QueryTags(ctx, input) + if err != nil { + return nil, err + } + + if result.QueryTags.Tags == nil { + return nil, nil + } + + var ret []*models.ScrapedTag + for _, t := range result.QueryTags.Tags { + ret = append(ret, &models.ScrapedTag{ + Name: t.Name, + RemoteSiteID: &t.ID, + }) + } + + return ret, nil +} diff --git a/pkg/studio/export.go b/pkg/studio/export.go index 483058c10..1440c3cdd 100644 --- a/pkg/studio/export.go +++ b/pkg/studio/export.go @@ -14,6 +14,7 @@ import ( type FinderImageStashIDGetter interface { models.StudioGetter models.AliasLoader + models.URLLoader models.StashIDLoader GetImage(ctx context.Context, studioID int) ([]byte, error) } @@ -22,7 +23,6 @@ type FinderImageStashIDGetter interface { func ToJSON(ctx context.Context, reader FinderImageStashIDGetter, studio *models.Studio) (*jsonschema.Studio, error) { newStudioJSON := jsonschema.Studio{ Name: studio.Name, - URL: studio.URL, Details: studio.Details, Favorite: studio.Favorite, IgnoreAutoTag: studio.IgnoreAutoTag, @@ -50,6 +50,11 @@ func ToJSON(ctx context.Context, reader FinderImageStashIDGetter, studio *models } newStudioJSON.Aliases = studio.Aliases.List() + if err := studio.LoadURLs(ctx, reader); err != nil { + return nil, fmt.Errorf("loading studio URLs: %w", err) + } + newStudioJSON.URLs = studio.URLs.List() + if err := studio.LoadStashIDs(ctx, reader); err != nil { return nil, fmt.Errorf("loading studio stash ids: %w", err) } diff --git a/pkg/studio/export_test.go b/pkg/studio/export_test.go index 0e42141ec..c333c0ad5 100644 --- a/pkg/studio/export_test.go +++ b/pkg/studio/export_test.go @@ -60,7 +60,7 @@ func createFullStudio(id int, parentID int) models.Studio { ret := models.Studio{ ID: id, Name: studioName, - URL: url, + URLs: models.NewRelatedStrings([]string{url}), Details: details, Favorite: true, CreatedAt: createTime, @@ -84,6 +84,7 @@ func createEmptyStudio(id int) models.Studio { ID: id, CreatedAt: createTime, UpdatedAt: updateTime, + URLs: models.NewRelatedStrings([]string{}), Aliases: models.NewRelatedStrings([]string{}), TagIDs: models.NewRelatedIDs([]int{}), StashIDs: models.NewRelatedStashIDs([]models.StashID{}), @@ -93,7 +94,7 @@ func createEmptyStudio(id int) models.Studio { func createFullJSONStudio(parentStudio, image string, aliases []string) *jsonschema.Studio { return &jsonschema.Studio{ Name: studioName, - URL: url, + URLs: []string{url}, Details: details, Favorite: true, CreatedAt: json.JSONTime{ @@ -120,6 +121,7 @@ func createEmptyJSONStudio() *jsonschema.Studio { Time: updateTime, }, Aliases: []string{}, + URLs: []string{}, StashIDs: []models.StashID{}, } } diff --git a/pkg/studio/import.go b/pkg/studio/import.go index 3aaceb093..d5284ce02 100644 --- a/pkg/studio/import.go +++ b/pkg/studio/import.go @@ -153,7 +153,7 @@ func (i *Importer) populateParentStudio(ctx context.Context) error { } func (i *Importer) createParentStudio(ctx context.Context, name string) (int, error) { - newStudio := models.NewStudio() + newStudio := models.NewCreateStudioInput() newStudio.Name = name err := i.ReaderWriter.Create(ctx, &newStudio) @@ -194,7 +194,7 @@ func (i *Importer) FindExistingID(ctx context.Context) (*int, error) { } func (i *Importer) Create(ctx context.Context) (*int, error) { - err := i.ReaderWriter.Create(ctx, &i.studio) + err := i.ReaderWriter.Create(ctx, &models.CreateStudioInput{Studio: &i.studio}) if err != nil { return nil, fmt.Errorf("error creating studio: %v", err) } @@ -206,7 +206,7 @@ func (i *Importer) Create(ctx context.Context) (*int, error) { func (i *Importer) Update(ctx context.Context, id int) error { studio := i.studio studio.ID = id - err := i.ReaderWriter.Update(ctx, &studio) + err := i.ReaderWriter.Update(ctx, &models.UpdateStudioInput{Studio: &studio}) if err != nil { return fmt.Errorf("error updating existing studio: %v", err) } @@ -217,7 +217,6 @@ func (i *Importer) Update(ctx context.Context, id int) error { func studioJSONtoStudio(studioJSON jsonschema.Studio) models.Studio { newStudio := models.Studio{ Name: studioJSON.Name, - URL: studioJSON.URL, Aliases: models.NewRelatedStrings(studioJSON.Aliases), Details: studioJSON.Details, Favorite: studioJSON.Favorite, @@ -229,6 +228,19 @@ func studioJSONtoStudio(studioJSON jsonschema.Studio) models.Studio { StashIDs: models.NewRelatedStashIDs(studioJSON.StashIDs), } + if len(studioJSON.URLs) > 0 { + newStudio.URLs = models.NewRelatedStrings(studioJSON.URLs) + } else { + urls := []string{} + if studioJSON.URL != "" { + urls = append(urls, studioJSON.URL) + } + + if len(urls) > 0 { + newStudio.URLs = models.NewRelatedStrings(urls) + } + } + if studioJSON.Rating != 0 { newStudio.Rating = &studioJSON.Rating } diff --git a/pkg/studio/import_test.go b/pkg/studio/import_test.go index 882b8ca56..6648ebe0d 100644 --- a/pkg/studio/import_test.go +++ b/pkg/studio/import_test.go @@ -206,9 +206,9 @@ func TestImporterPreImportWithMissingParent(t *testing.T) { } db.Studio.On("FindByName", testCtx, missingParentStudioName, false).Return(nil, nil).Times(3) - db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.Studio")).Run(func(args mock.Arguments) { - s := args.Get(1).(*models.Studio) - s.ID = existingStudioID + db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.CreateStudioInput")).Run(func(args mock.Arguments) { + s := args.Get(1).(*models.CreateStudioInput) + s.Studio.ID = existingStudioID }).Return(nil) err := i.PreImport(testCtx) @@ -240,7 +240,7 @@ func TestImporterPreImportWithMissingParentCreateErr(t *testing.T) { } db.Studio.On("FindByName", testCtx, missingParentStudioName, false).Return(nil, nil).Once() - db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.Studio")).Return(errors.New("Create error")) + db.Studio.On("Create", testCtx, mock.AnythingOfType("*models.CreateStudioInput")).Return(errors.New("Create error")) err := i.PreImport(testCtx) assert.NotNil(t, err) @@ -327,11 +327,11 @@ func TestCreate(t *testing.T) { } errCreate := errors.New("Create error") - db.Studio.On("Create", testCtx, &studio).Run(func(args mock.Arguments) { - s := args.Get(1).(*models.Studio) + db.Studio.On("Create", testCtx, &models.CreateStudioInput{Studio: &studio}).Run(func(args mock.Arguments) { + s := args.Get(1).(*models.CreateStudioInput) s.ID = studioID }).Return(nil).Once() - db.Studio.On("Create", testCtx, &studioErr).Return(errCreate).Once() + db.Studio.On("Create", testCtx, &models.CreateStudioInput{Studio: &studioErr}).Return(errCreate).Once() id, err := i.Create(testCtx) assert.Equal(t, studioID, *id) @@ -366,7 +366,7 @@ func TestUpdate(t *testing.T) { // id needs to be set for the mock input studio.ID = studioID - db.Studio.On("Update", testCtx, &studio).Return(nil).Once() + db.Studio.On("Update", testCtx, &models.UpdateStudioInput{Studio: &studio}).Return(nil).Once() err := i.Update(testCtx, studioID) assert.Nil(t, err) @@ -375,7 +375,7 @@ func TestUpdate(t *testing.T) { // need to set id separately studioErr.ID = errImageID - db.Studio.On("Update", testCtx, &studioErr).Return(errUpdate).Once() + db.Studio.On("Update", testCtx, &models.UpdateStudioInput{Studio: &studioErr}).Return(errUpdate).Once() err = i.Update(testCtx, errImageID) assert.NotNil(t, err) diff --git a/pkg/studio/validate.go b/pkg/studio/validate.go index 8a8676351..526400066 100644 --- a/pkg/studio/validate.go +++ b/pkg/studio/validate.go @@ -10,6 +10,7 @@ import ( var ( ErrNameMissing = errors.New("studio name must not be blank") + ErrEmptyAlias = errors.New("studio alias must not be an empty string") ErrStudioOwnAncestor = errors.New("studio cannot be an ancestor of itself") ) @@ -61,9 +62,12 @@ func EnsureStudioNameUnique(ctx context.Context, id int, name string, qb models. return nil } -func EnsureAliasesUnique(ctx context.Context, id int, aliases []string, qb models.StudioQueryer) error { +func ValidateAliases(ctx context.Context, id int, aliases []string, qb models.StudioQueryer) error { for _, a := range aliases { - if err := EnsureStudioNameUnique(ctx, id, a, qb); err != nil { + if err := validateName(ctx, id, a, qb); err != nil { + if errors.Is(err, ErrNameMissing) { + return ErrEmptyAlias + } return err } } @@ -71,13 +75,13 @@ func EnsureAliasesUnique(ctx context.Context, id int, aliases []string, qb model return nil } -func ValidateCreate(ctx context.Context, studio models.Studio, qb models.StudioQueryer) error { +func ValidateCreate(ctx context.Context, studio models.CreateStudioInput, qb models.StudioQueryer) error { if err := validateName(ctx, 0, studio.Name, qb); err != nil { return err } if studio.Aliases.Loaded() && len(studio.Aliases.List()) > 0 { - if err := EnsureAliasesUnique(ctx, 0, studio.Aliases.List(), qb); err != nil { + if err := ValidateAliases(ctx, 0, studio.Aliases.List(), qb); err != nil { return err } } @@ -131,7 +135,8 @@ func ValidateModify(ctx context.Context, s models.StudioPartial, qb ValidateModi } effectiveAliases := s.Aliases.Apply(existing.Aliases.List()) - if err := EnsureAliasesUnique(ctx, s.ID, effectiveAliases, qb); err != nil { + + if err := ValidateAliases(ctx, s.ID, effectiveAliases, qb); err != nil { return err } } diff --git a/pkg/studio/validate_test.go b/pkg/studio/validate_test.go index 6562dc5ca..b196ba3c3 100644 --- a/pkg/studio/validate_test.go +++ b/pkg/studio/validate_test.go @@ -102,3 +102,72 @@ func TestValidateUpdateName(t *testing.T) { }) } } + +func TestValidateUpdateAliases(t *testing.T) { + db := mocks.NewDatabase() + + const ( + name1 = "name 1" + name2 = "name 2" + alias1 = "alias 1" + newAlias = "new alias" + ) + + existing1 := models.Studio{ + ID: 1, + Name: name1, + } + existing2 := models.Studio{ + ID: 2, + Name: name2, + } + + pp := 1 + findFilter := &models.FindFilterType{ + PerPage: &pp, + } + + aliasFilter := func(n string) *models.StudioFilterType { + return &models.StudioFilterType{ + Aliases: &models.StringCriterionInput{ + Value: n, + Modifier: models.CriterionModifierEquals, + }, + } + } + + // name1 matches existing1 name - ok + db.Studio.On("Query", testCtx, nameFilter(alias1), findFilter).Return(nil, 0, nil) + db.Studio.On("Query", testCtx, aliasFilter(alias1), findFilter).Return(nil, 0, nil) + + // name2 matches existing2 name - error + db.Studio.On("Query", testCtx, nameFilter(name2), findFilter).Return([]*models.Studio{&existing2}, 1, nil) + + // alias matches existing alias - error + db.Studio.On("Query", testCtx, nameFilter(newAlias), findFilter).Return(nil, 0, nil) + db.Studio.On("Query", testCtx, aliasFilter(newAlias), findFilter).Return([]*models.Studio{&existing2}, 1, nil) + + // valid alias + db.Studio.On("Query", testCtx, nameFilter("valid"), findFilter).Return(nil, 0, nil) + db.Studio.On("Query", testCtx, aliasFilter("valid"), findFilter).Return(nil, 0, nil) + + tests := []struct { + tName string + studio models.Studio + aliases []string + want error + }{ + {"valid alias", existing1, []string{alias1}, nil}, + {"alias duplicates other name", existing1, []string{name2}, &NameExistsError{name2}}, + {"alias duplicates other alias", existing1, []string{newAlias}, &NameUsedByAliasError{newAlias, existing2.Name}}, + {"valid new alias", existing1, []string{"valid"}, nil}, + {"empty alias", existing1, []string{""}, ErrEmptyAlias}, + } + + for _, tt := range tests { + t.Run(tt.tName, func(t *testing.T) { + got := ValidateAliases(testCtx, tt.studio.ID, tt.aliases, db.Studio) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/tag/export.go b/pkg/tag/export.go index bd1573341..b07418667 100644 --- a/pkg/tag/export.go +++ b/pkg/tag/export.go @@ -16,6 +16,7 @@ type FinderAliasImageGetter interface { GetAliases(ctx context.Context, studioID int) ([]string, error) GetImage(ctx context.Context, tagID int) ([]byte, error) FindByChildTagID(ctx context.Context, childID int) ([]*models.Tag, error) + models.StashIDLoader } // ToJSON converts a Tag object into its JSON equivalent. @@ -37,6 +38,15 @@ func ToJSON(ctx context.Context, reader FinderAliasImageGetter, tag *models.Tag) newTagJSON.Aliases = aliases + if err := tag.LoadStashIDs(ctx, reader); err != nil { + return nil, fmt.Errorf("loading tag stash ids: %w", err) + } + + stashIDs := tag.StashIDs.List() + if len(stashIDs) > 0 { + newTagJSON.StashIDs = stashIDs + } + image, err := reader.GetImage(ctx, tag.ID) if err != nil { logger.Errorf("Error getting tag image: %v", err) diff --git a/pkg/tag/export_test.go b/pkg/tag/export_test.go index 6c008c170..84e082f30 100644 --- a/pkg/tag/export_test.go +++ b/pkg/tag/export_test.go @@ -126,6 +126,13 @@ func TestToJSON(t *testing.T) { db.Tag.On("GetAliases", testCtx, withParentsID).Return(nil, nil).Once() db.Tag.On("GetAliases", testCtx, errParentsID).Return(nil, nil).Once() + db.Tag.On("GetStashIDs", testCtx, tagID).Return(nil, nil).Once() + db.Tag.On("GetStashIDs", testCtx, noImageID).Return(nil, nil).Once() + db.Tag.On("GetStashIDs", testCtx, errImageID).Return(nil, nil).Once() + // errAliasID test fails before GetStashIDs is called, so no mock needed + db.Tag.On("GetStashIDs", testCtx, withParentsID).Return(nil, nil).Once() + db.Tag.On("GetStashIDs", testCtx, errParentsID).Return(nil, nil).Once() + db.Tag.On("GetImage", testCtx, tagID).Return(imageBytes, nil).Once() db.Tag.On("GetImage", testCtx, noImageID).Return(nil, nil).Once() db.Tag.On("GetImage", testCtx, errImageID).Return(nil, imageErr).Once() diff --git a/pkg/tag/import.go b/pkg/tag/import.go index 21203afb0..53b741886 100644 --- a/pkg/tag/import.go +++ b/pkg/tag/import.go @@ -42,6 +42,7 @@ func (i *Importer) PreImport(ctx context.Context) error { Description: i.Input.Description, Favorite: i.Input.Favorite, IgnoreAutoTag: i.Input.IgnoreAutoTag, + StashIDs: models.NewRelatedStashIDs(i.Input.StashIDs), CreatedAt: i.Input.CreatedAt.GetTime(), UpdatedAt: i.Input.UpdatedAt.GetTime(), } diff --git a/pkg/tag/validate.go b/pkg/tag/validate.go index 966cec945..abc260b5e 100644 --- a/pkg/tag/validate.go +++ b/pkg/tag/validate.go @@ -69,7 +69,9 @@ func ValidateUpdate(ctx context.Context, id int, partial models.TagPartial, qb m return err } - if err := EnsureAliasesUnique(ctx, id, partial.Aliases.Apply(existing.Aliases.List()), qb); err != nil { + newAliases := partial.Aliases.Apply(existing.Aliases.List()) + + if err := EnsureAliasesUnique(ctx, id, newAliases, qb); err != nil { return err } } diff --git a/pkg/tag/validate_test.go b/pkg/tag/validate_test.go new file mode 100644 index 000000000..539086a6d --- /dev/null +++ b/pkg/tag/validate_test.go @@ -0,0 +1,86 @@ +package tag + +import ( + "testing" + + "github.com/stashapp/stash/pkg/models" + "github.com/stashapp/stash/pkg/models/mocks" + "github.com/stretchr/testify/assert" +) + +func nameFilter(n string) *models.TagFilterType { + return &models.TagFilterType{ + Name: &models.StringCriterionInput{ + Value: n, + Modifier: models.CriterionModifierEquals, + }, + } +} + +func aliasFilter(n string) *models.TagFilterType { + return &models.TagFilterType{ + Aliases: &models.StringCriterionInput{ + Value: n, + Modifier: models.CriterionModifierEquals, + }, + } +} + +func TestEnsureAliasesUnique(t *testing.T) { + db := mocks.NewDatabase() + + const ( + name1 = "name 1" + name2 = "name 2" + alias1 = "alias 1" + newAlias = "new alias" + ) + + existing2 := models.Tag{ + ID: 2, + Name: name2, + } + + pp := 1 + findFilter := &models.FindFilterType{ + PerPage: &pp, + } + + // name1 matches existing1 name - ok + // EnsureAliasesUnique calls EnsureTagNameUnique. + // EnsureTagNameUnique calls ByName then ByAlias. + + // Case 1: valid alias + // ByName "alias 1" -> nil + // ByAlias "alias 1" -> nil + db.Tag.On("Query", testCtx, nameFilter(alias1), findFilter).Return(nil, 0, nil) + db.Tag.On("Query", testCtx, aliasFilter(alias1), findFilter).Return(nil, 0, nil) + + // Case 2: alias duplicates existing2 name + // ByName "name 2" -> existing2 + db.Tag.On("Query", testCtx, nameFilter(name2), findFilter).Return([]*models.Tag{&existing2}, 1, nil) + + // Case 3: alias duplicates existing2 alias + // ByName "new alias" -> nil + // ByAlias "new alias" -> existing2 + db.Tag.On("Query", testCtx, nameFilter(newAlias), findFilter).Return(nil, 0, nil) + db.Tag.On("Query", testCtx, aliasFilter(newAlias), findFilter).Return([]*models.Tag{&existing2}, 1, nil) + + tests := []struct { + tName string + id int + aliases []string + want error + }{ + {"valid alias", 1, []string{alias1}, nil}, + {"alias duplicates other name", 1, []string{name2}, &NameExistsError{name2}}, + {"alias duplicates other alias", 1, []string{newAlias}, &NameUsedByAliasError{newAlias, existing2.Name}}, + } + + for _, tt := range tests { + t.Run(tt.tName, func(t *testing.T) { + got := EnsureAliasesUnique(testCtx, tt.id, tt.aliases, db.Tag) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/utils/context.go b/pkg/utils/context.go deleted file mode 100644 index 2a3862b5d..000000000 --- a/pkg/utils/context.go +++ /dev/null @@ -1,22 +0,0 @@ -package utils - -import ( - "context" - "time" -) - -type ValueOnlyContext struct { - context.Context -} - -func (ValueOnlyContext) Deadline() (deadline time.Time, ok bool) { - return -} - -func (ValueOnlyContext) Done() <-chan struct{} { - return nil -} - -func (ValueOnlyContext) Err() error { - return nil -} diff --git a/pkg/utils/date_test.go b/pkg/utils/date_test.go new file mode 100644 index 000000000..ae077c21e --- /dev/null +++ b/pkg/utils/date_test.go @@ -0,0 +1,43 @@ +package utils + +import ( + "testing" +) + +func TestParseDateStringAsTime(t *testing.T) { + tests := []struct { + name string + input string + expectError bool + }{ + // Full date formats (existing support) + {"RFC3339", "2014-01-02T15:04:05Z", false}, + {"Date only", "2014-01-02", false}, + {"Date with time", "2014-01-02 15:04:05", false}, + + // Invalid formats + {"Invalid format", "not-a-date", true}, + {"Empty string", "", true}, + {"Year-Month", "2006-08", true}, + {"Year only", "2014", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ParseDateStringAsTime(tt.input) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error for input %q, but got none", tt.input) + } + } else { + if err != nil { + t.Errorf("Unexpected error for input %q: %v", tt.input, err) + } + if result.IsZero() { + t.Errorf("Expected non-zero time for input %q", tt.input) + } + } + }) + } +} diff --git a/ui/login/login.html b/ui/login/login.html index 32787fc91..62b8ffdc8 100644 --- a/ui/login/login.html +++ b/ui/login/login.html @@ -44,7 +44,8 @@ xhr.onerror = function() { document.getElementsByClassName("login-error")[0].innerHTML = localeStrings.internal_error; }; - xhr.send("username=" + username + "&password=" + password + "&returnURL=" + returnURL); + var body = "username=" + encodeURIComponent(username) + "&password=" + encodeURIComponent(password) + "&returnURL=" + encodeURIComponent(returnURL); + xhr.send(body); } diff --git a/ui/v2.5/.prettierignore b/ui/v2.5/.prettierignore index 5ef8feb04..003d319d2 100644 --- a/ui/v2.5/.prettierignore +++ b/ui/v2.5/.prettierignore @@ -2,8 +2,8 @@ # dependencies /node_modules -/.pnp -.pnp.js +pnpm-lock.yaml +pnpm-workspace.yaml # locales src/locales/**/*.json diff --git a/ui/v2.5/README.md b/ui/v2.5/README.md index bb215a23c..ece6502bf 100755 --- a/ui/v2.5/README.md +++ b/ui/v2.5/README.md @@ -4,7 +4,7 @@ This project was bootstrapped with [Create React App](https://github.com/faceboo In the project directory, you can run: -### `yarn start` +### `npm run start` Runs the app in the development mode.
Open [http://localhost:3000](http://localhost:3000) to view it in the browser. @@ -12,12 +12,12 @@ Open [http://localhost:3000](http://localhost:3000) to view it in the browser. The page will reload if you make edits.
You will also see any lint errors in the console. -### `yarn test` +### `npm run test` Launches the test runner in the interactive watch mode.
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. -### `yarn build` +### `npm run build` Builds the app for production to the `build` folder.
It correctly bundles React in production mode and optimizes the build for the best performance. @@ -27,13 +27,13 @@ Your app is ready to be deployed! See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. -### `yarn format` +### `npm run format` Formats the whitespace of all typescript and scss code with prettier, to ease editing and ensure a common code style. Should ideally be run before all frontend PRs. -### `yarn eject` +### `npm run eject` **Note: this is a one-way operation. Once you `eject`, you can’t go back!** @@ -69,6 +69,6 @@ This section has moved here: https://facebook.github.io/create-react-app/docs/ad This section has moved here: https://facebook.github.io/create-react-app/docs/deployment -### `yarn build` fails to minify +### `npm run build` fails to minify This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify diff --git a/ui/v2.5/graphql/data/config.graphql b/ui/v2.5/graphql/data/config.graphql index c0bcda821..08dcf5d3b 100644 --- a/ui/v2.5/graphql/data/config.graphql +++ b/ui/v2.5/graphql/data/config.graphql @@ -6,6 +6,7 @@ fragment ConfigGeneralData on ConfigGeneralResult { } databasePath backupDirectoryPath + deleteTrashPath generatedPath metadataPath scrapersPath @@ -37,6 +38,7 @@ fragment ConfigGeneralData on ConfigGeneralResult { logOut logLevel logAccess + logFileMaxSize createGalleriesFromFolders galleryCoverRegex videoExtensions @@ -71,6 +73,7 @@ fragment ConfigGeneralData on ConfigGeneralResult { } fragment ConfigInterfaceData on ConfigInterfaceResult { + sfwContentMode menuItems soundOnPreview wallShowTitle @@ -89,6 +92,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult { javascriptEnabled customLocales customLocalesEnabled + disableCustomizations language imageLightbox { slideshowDelay @@ -97,12 +101,14 @@ fragment ConfigInterfaceData on ConfigInterfaceResult { resetZoomOnNav scrollMode scrollAttemptsBeforeChange + disableAnimation } disableDropdownCreate { performer tag studio movie + gallery } handyKey funscriptOffset diff --git a/ui/v2.5/graphql/data/gallery.graphql b/ui/v2.5/graphql/data/gallery.graphql index c41f3e2b2..89f3ed44c 100644 --- a/ui/v2.5/graphql/data/gallery.graphql +++ b/ui/v2.5/graphql/data/gallery.graphql @@ -22,7 +22,7 @@ fragment GalleryData on Gallery { folder { ...FolderData } - + image_count chapters { ...GalleryChapterData } diff --git a/ui/v2.5/graphql/data/group.graphql b/ui/v2.5/graphql/data/group.graphql index 41114f5aa..440c420da 100644 --- a/ui/v2.5/graphql/data/group.graphql +++ b/ui/v2.5/graphql/data/group.graphql @@ -1,3 +1,4 @@ +# Full fragment for detail views - includes recursive counts fragment GroupData on Group { id name @@ -32,6 +33,48 @@ fragment GroupData on Group { performer_count_all: performer_count(depth: -1) sub_group_count sub_group_count_all: sub_group_count(depth: -1) + o_counter + + scenes { + id + title + } +} + +# Lightweight fragment for list views - excludes expensive recursive counts +# The _all fields (depth: -1) cause 10+ second queries on large databases +fragment ListGroupData on Group { + id + name + aliases + duration + date + rating100 + director + + studio { + ...SlimStudioData + } + + tags { + ...SlimTagData + } + + containing_groups { + group { + ...SlimGroupData + } + description + } + + synopsis + urls + front_image_path + back_image_path + scene_count + performer_count + sub_group_count + o_counter scenes { id diff --git a/ui/v2.5/graphql/data/scrapers.graphql b/ui/v2.5/graphql/data/scrapers.graphql index b2fe0603a..4a0f588a4 100644 --- a/ui/v2.5/graphql/data/scrapers.graphql +++ b/ui/v2.5/graphql/data/scrapers.graphql @@ -1,15 +1,25 @@ fragment ScrapedStudioData on ScrapedStudio { stored_id name - url + urls parent { stored_id name - url + urls image + details + aliases + tags { + ...ScrapedSceneTagData + } remote_site_id } image + details + aliases + tags { + ...ScrapedSceneTagData + } remote_site_id } @@ -76,7 +86,7 @@ fragment ScrapedScenePerformerData on ScrapedPerformer { fragment ScrapedGroupStudioData on ScrapedStudio { stored_id name - url + urls } fragment ScrapedGroupData on ScrapedGroup { @@ -123,21 +133,32 @@ fragment ScrapedSceneGroupData on ScrapedGroup { fragment ScrapedSceneStudioData on ScrapedStudio { stored_id name - url + urls parent { stored_id name - url + urls image + details + aliases + tags { + ...ScrapedSceneTagData + } remote_site_id } image + details + aliases + tags { + ...ScrapedSceneTagData + } remote_site_id } fragment ScrapedSceneTagData on ScrapedTag { stored_id name + remote_site_id } fragment ScrapedSceneData on ScrapedScene { diff --git a/ui/v2.5/graphql/data/studio-slim.graphql b/ui/v2.5/graphql/data/studio-slim.graphql index cf101bd04..c48f7d93e 100644 --- a/ui/v2.5/graphql/data/studio-slim.graphql +++ b/ui/v2.5/graphql/data/studio-slim.graphql @@ -17,4 +17,5 @@ fragment SlimStudioData on Studio { id name } + o_counter } diff --git a/ui/v2.5/graphql/data/studio.graphql b/ui/v2.5/graphql/data/studio.graphql index 25e776755..aabec7a9b 100644 --- a/ui/v2.5/graphql/data/studio.graphql +++ b/ui/v2.5/graphql/data/studio.graphql @@ -2,10 +2,12 @@ fragment StudioData on Studio { id name url + urls parent_studio { id name url + urls image_path } child_studios { @@ -37,6 +39,7 @@ fragment StudioData on Studio { tags { ...SlimTagData } + o_counter } fragment SelectStudioData on Studio { diff --git a/ui/v2.5/graphql/data/tag-slim.graphql b/ui/v2.5/graphql/data/tag-slim.graphql index 3c498539b..3aca74f16 100644 --- a/ui/v2.5/graphql/data/tag-slim.graphql +++ b/ui/v2.5/graphql/data/tag-slim.graphql @@ -6,4 +6,10 @@ fragment SlimTagData on Tag { image_path parent_count child_count + + stash_ids { + endpoint + stash_id + updated_at + } } diff --git a/ui/v2.5/graphql/data/tag.graphql b/ui/v2.5/graphql/data/tag.graphql index 5eae173ea..e640af0c9 100644 --- a/ui/v2.5/graphql/data/tag.graphql +++ b/ui/v2.5/graphql/data/tag.graphql @@ -6,6 +6,11 @@ fragment TagData on Tag { aliases ignore_auto_tag favorite + stash_ids { + endpoint + stash_id + updated_at + } image_path scene_count scene_count_all: scene_count(depth: -1) @@ -45,4 +50,43 @@ fragment SelectTagData on Tag { name sort_name } + + stash_ids { + endpoint + stash_id + updated_at + } +} + +# Optimized fragment for tag list page - excludes expensive recursive *_count_all fields +fragment TagListData on Tag { + id + name + sort_name + description + aliases + ignore_auto_tag + favorite + stash_ids { + endpoint + stash_id + updated_at + } + image_path + # Direct counts only - no recursive depth queries + scene_count + scene_marker_count + image_count + gallery_count + performer_count + studio_count + group_count + + parents { + ...SlimTagData + } + + children { + ...SlimTagData + } } diff --git a/ui/v2.5/graphql/mutations/performer.graphql b/ui/v2.5/graphql/mutations/performer.graphql index a4fa341ed..2082281fc 100644 --- a/ui/v2.5/graphql/mutations/performer.graphql +++ b/ui/v2.5/graphql/mutations/performer.graphql @@ -23,3 +23,9 @@ mutation PerformerDestroy($id: ID!) { mutation PerformersDestroy($ids: [ID!]!) { performersDestroy(ids: $ids) } + +mutation PerformerMerge($input: PerformerMergeInput!) { + performerMerge(input: $input) { + id + } +} diff --git a/ui/v2.5/graphql/mutations/scene-marker.graphql b/ui/v2.5/graphql/mutations/scene-marker.graphql index 766e318fc..a2162f799 100644 --- a/ui/v2.5/graphql/mutations/scene-marker.graphql +++ b/ui/v2.5/graphql/mutations/scene-marker.graphql @@ -44,6 +44,12 @@ mutation SceneMarkerUpdate( } } +mutation BulkSceneMarkerUpdate($input: BulkSceneMarkerUpdateInput!) { + bulkSceneMarkerUpdate(input: $input) { + ...SceneMarkerData + } +} + mutation SceneMarkerDestroy($id: ID!) { sceneMarkerDestroy(id: $id) } diff --git a/ui/v2.5/graphql/mutations/studio.graphql b/ui/v2.5/graphql/mutations/studio.graphql index 6d1944dc1..679d75f6d 100644 --- a/ui/v2.5/graphql/mutations/studio.graphql +++ b/ui/v2.5/graphql/mutations/studio.graphql @@ -10,6 +10,12 @@ mutation StudioUpdate($input: StudioUpdateInput!) { } } +mutation BulkStudioUpdate($input: BulkStudioUpdateInput!) { + bulkStudioUpdate(input: $input) { + ...StudioData + } +} + mutation StudioDestroy($id: ID!) { studioDestroy(input: { id: $id }) } diff --git a/ui/v2.5/graphql/queries/image.graphql b/ui/v2.5/graphql/queries/image.graphql index ee96d00d2..d2c6cdac8 100644 --- a/ui/v2.5/graphql/queries/image.graphql +++ b/ui/v2.5/graphql/queries/image.graphql @@ -9,14 +9,27 @@ query FindImages( image_ids: $image_ids ) { count - megapixels - filesize images { ...SlimImageData } } } +query FindImagesMetadata( + $filter: FindFilterType + $image_filter: ImageFilterType + $image_ids: [Int!] +) { + findImages( + filter: $filter + image_filter: $image_filter + image_ids: $image_ids + ) { + megapixels + filesize + } +} + query FindImage($id: ID!, $checksum: String) { findImage(id: $id, checksum: $checksum) { ...ImageData diff --git a/ui/v2.5/graphql/queries/movie.graphql b/ui/v2.5/graphql/queries/movie.graphql index ad47e908d..2b2af7510 100644 --- a/ui/v2.5/graphql/queries/movie.graphql +++ b/ui/v2.5/graphql/queries/movie.graphql @@ -2,7 +2,7 @@ query FindGroups($filter: FindFilterType, $group_filter: GroupFilterType) { findGroups(filter: $filter, group_filter: $group_filter) { count groups { - ...GroupData + ...ListGroupData } } } diff --git a/ui/v2.5/graphql/queries/scrapers/scrapers.graphql b/ui/v2.5/graphql/queries/scrapers/scrapers.graphql index 8137fe054..4ddfbd91b 100644 --- a/ui/v2.5/graphql/queries/scrapers/scrapers.graphql +++ b/ui/v2.5/graphql/queries/scrapers/scrapers.graphql @@ -62,6 +62,15 @@ query ScrapeSingleStudio( } } +query ScrapeSingleTag( + $source: ScraperSourceInput! + $input: ScrapeSingleTagInput! +) { + scrapeSingleTag(source: $source, input: $input) { + ...ScrapedSceneTagData + } +} + query ScrapeSinglePerformer( $source: ScraperSourceInput! $input: ScrapeSinglePerformerInput! diff --git a/ui/v2.5/graphql/queries/tag.graphql b/ui/v2.5/graphql/queries/tag.graphql index ab1fc62f8..e0b20ee02 100644 --- a/ui/v2.5/graphql/queries/tag.graphql +++ b/ui/v2.5/graphql/queries/tag.graphql @@ -25,3 +25,13 @@ query FindTagsForSelect( } } } + +# Optimized query for tag list page - uses TagListData fragment without recursive counts +query FindTagsForList($filter: FindFilterType, $tag_filter: TagFilterType) { + findTags(filter: $filter, tag_filter: $tag_filter) { + count + tags { + ...TagListData + } + } +} diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index c129b6f5c..e024a0053 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -1,14 +1,14 @@ { "name": "stash", - "version": "0.1.0", "private": true, "homepage": "./", + "type": "module", "scripts": { "start": "vite", "build": "vite build", - "build-ci": "yarn run validate && yarn run build", - "validate": "yarn run lint && yarn run check && yarn run format-check", - "lint": "yarn run lint:js && yarn run lint:css", + "build-ci": "npm run validate && npm run build", + "validate": "npm run lint && npm run check && npm run format-check", + "lint": "npm run lint:js && npm run lint:css", "lint:css": "stylelint --cache \"src/**/*.scss\"", "lint:js": "eslint --cache src/", "check": "tsc --noEmit", @@ -27,14 +27,15 @@ "@formatjs/intl-locale": "^3.0.11", "@formatjs/intl-numberformat": "^8.3.3", "@formatjs/intl-pluralrules": "^5.1.8", - "@fortawesome/fontawesome-svg-core": "^6.3.0", - "@fortawesome/free-brands-svg-icons": "^6.3.0", - "@fortawesome/free-regular-svg-icons": "^6.3.0", - "@fortawesome/free-solid-svg-icons": "^6.3.0", - "@fortawesome/react-fontawesome": "^0.2.0", + "@fortawesome/fontawesome-svg-core": "^7.1.0", + "@fortawesome/free-brands-svg-icons": "^7.1.0", + "@fortawesome/free-regular-svg-icons": "^7.1.0", + "@fortawesome/free-solid-svg-icons": "^7.1.0", + "@fortawesome/react-fontawesome": "^0.2.6", "@react-hook/resize-observer": "^1.2.6", "@silvermine/videojs-airplay": "^1.2.0", "@silvermine/videojs-chromecast": "^1.4.1", + "@types/react-router-dom": "^5.3.3", "apollo-upload-client": "^18.0.1", "base64-blob": "^1.4.1", "bootstrap": "^4.6.2", @@ -48,9 +49,8 @@ "graphql-tag": "^2.12.6", "graphql-ws": "^5.14.3", "i18n-iso-countries": "^7.5.0", - "intersection-observer": "^0.12.2", "localforage": "^1.10.0", - "lodash-es": "^4.17.21", + "lodash-es": "^4.17.23", "moment": "^2.30.1", "mousetrap": "^1.6.5", "mousetrap-pause": "^1.0.0", @@ -92,6 +92,7 @@ "@graphql-codegen/typescript-react-apollo": "^4.1.0", "@types/apollo-upload-client": "^18.0.0", "@types/crypto-js": "^4.2.2", + "@types/dom-screen-wake-lock": "^1.0.3", "@types/lodash-es": "^4.17.6", "@types/mousetrap": "^1.6.11", "@types/node": "^18.13.0", @@ -108,8 +109,8 @@ "@types/videojs-seek-buttons": "^2.1.0", "@typescript-eslint/eslint-plugin": "^5.52.0", "@typescript-eslint/parser": "^5.52.0", - "@vitejs/plugin-legacy": "^4.0.1", - "@vitejs/plugin-react": "^3.1.0", + "@vitejs/plugin-legacy": "^5.4.3", + "@vitejs/plugin-react": "^5.1.0", "eslint": "^8.34.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^17.0.0", @@ -128,7 +129,7 @@ "terser": "^5.9.0", "ts-node": "^10.9.1", "typescript": "~4.8.4", - "vite": "^4.5.14", + "vite": "^5.4.21", "vite-plugin-compression": "^0.5.1", "vite-tsconfig-paths": "^4.0.5" } diff --git a/ui/v2.5/pnpm-lock.yaml b/ui/v2.5/pnpm-lock.yaml new file mode 100644 index 000000000..02033c41f --- /dev/null +++ b/ui/v2.5/pnpm-lock.yaml @@ -0,0 +1,11385 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@ant-design/react-slick': + specifier: ^1.0.0 + version: 1.1.2(react@17.0.2) + '@apollo/client': + specifier: ^3.8.10 + version: 3.14.0(@types/react@17.0.89)(graphql-ws@5.16.2(graphql@16.11.0))(graphql@16.11.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@formatjs/intl-getcanonicallocales': + specifier: ^2.0.5 + version: 2.5.6 + '@formatjs/intl-locale': + specifier: ^3.0.11 + version: 3.4.6 + '@formatjs/intl-numberformat': + specifier: ^8.3.3 + version: 8.15.6 + '@formatjs/intl-pluralrules': + specifier: ^5.1.8 + version: 5.4.6 + '@fortawesome/fontawesome-svg-core': + specifier: ^7.1.0 + version: 7.1.0 + '@fortawesome/free-brands-svg-icons': + specifier: ^7.1.0 + version: 7.1.0 + '@fortawesome/free-regular-svg-icons': + specifier: ^7.1.0 + version: 7.1.0 + '@fortawesome/free-solid-svg-icons': + specifier: ^7.1.0 + version: 7.1.0 + '@fortawesome/react-fontawesome': + specifier: ^0.2.6 + version: 0.2.6(@fortawesome/fontawesome-svg-core@7.1.0)(react@17.0.2) + '@react-hook/resize-observer': + specifier: ^1.2.6 + version: 1.2.6(react@17.0.2) + '@silvermine/videojs-airplay': + specifier: ^1.2.0 + version: 1.3.0(video.js@7.21.7) + '@silvermine/videojs-chromecast': + specifier: ^1.4.1 + version: 1.5.0(video.js@7.21.7) + '@types/react-router-dom': + specifier: ^5.3.3 + version: 5.3.3 + apollo-upload-client: + specifier: ^18.0.1 + version: 18.0.1(@apollo/client@3.14.0(@types/react@17.0.89)(graphql-ws@5.16.2(graphql@16.11.0))(graphql@16.11.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(graphql@16.11.0) + base64-blob: + specifier: ^1.4.1 + version: 1.4.1 + bootstrap: + specifier: ^4.6.2 + version: 4.6.2(jquery@3.7.1)(popper.js@1.16.1) + classnames: + specifier: ^2.3.2 + version: 2.5.1 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + event-target-polyfill: + specifier: ^0.0.4 + version: 0.0.4 + flag-icons: + specifier: ^6.6.6 + version: 6.15.0 + flexbin: + specifier: ^0.2.0 + version: 0.2.0 + formik: + specifier: ^2.4.5 + version: 2.4.6(@types/react@17.0.89)(react@17.0.2) + graphql: + specifier: ^16.8.1 + version: 16.11.0 + graphql-tag: + specifier: ^2.12.6 + version: 2.12.6(graphql@16.11.0) + graphql-ws: + specifier: ^5.14.3 + version: 5.16.2(graphql@16.11.0) + i18n-iso-countries: + specifier: ^7.5.0 + version: 7.14.0 + localforage: + specifier: ^1.10.0 + version: 1.10.0 + lodash-es: + specifier: ^4.17.23 + version: 4.17.23 + moment: + specifier: ^2.30.1 + version: 2.30.1 + mousetrap: + specifier: ^1.6.5 + version: 1.6.5 + mousetrap-pause: + specifier: ^1.0.0 + version: 1.0.0 + normalize-url: + specifier: ^4.5.1 + version: 4.5.1 + react: + specifier: ^17.0.2 + version: 17.0.2 + react-bootstrap: + specifier: ^1.6.6 + version: 1.6.8(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + react-datepicker: + specifier: ^4.10.0 + version: 4.25.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + react-dom: + specifier: ^17.0.2 + version: 17.0.2(react@17.0.2) + react-helmet: + specifier: ^6.1.0 + version: 6.1.0(react@17.0.2) + react-intl: + specifier: ^6.2.8 + version: 6.8.9(react@17.0.2)(typescript@4.8.4) + react-photo-gallery: + specifier: ^8.0.0 + version: 8.0.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + react-remark: + specifier: ^2.1.0 + version: 2.1.0(react@17.0.2) + react-router-bootstrap: + specifier: ^0.25.0 + version: 0.25.0(react-router-dom@5.3.4(react@17.0.2))(react@17.0.2) + react-router-dom: + specifier: ^5.3.4 + version: 5.3.4(react@17.0.2) + react-router-hash-link: + specifier: ^2.4.3 + version: 2.4.3(react-router-dom@5.3.4(react@17.0.2))(react@17.0.2) + react-select: + specifier: ^5.7.0 + version: 5.10.2(@types/react@17.0.89)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + remark-gfm: + specifier: ^1.0.0 + version: 1.0.0 + resize-observer-polyfill: + specifier: ^1.5.1 + version: 1.5.1 + slick-carousel: + specifier: ^1.8.1 + version: 1.8.1(jquery@3.7.1) + string.prototype.replaceall: + specifier: ^1.0.7 + version: 1.0.11 + thehandy: + specifier: ^1.0.3 + version: 1.1.0 + ua-parser-js: + specifier: ^1.0.34 + version: 1.0.41 + universal-cookie: + specifier: ^4.0.4 + version: 4.0.4 + video.js: + specifier: ^7.21.3 + version: 7.21.7 + videojs-abloop: + specifier: ^1.2.0 + version: 1.2.0 + videojs-contrib-dash: + specifier: ^5.1.1 + version: 5.1.1 + videojs-mobile-ui: + specifier: ^0.8.0 + version: 0.8.0(video.js@7.21.7) + videojs-seek-buttons: + specifier: ^3.0.1 + version: 3.0.1(video.js@7.21.7) + videojs-vr: + specifier: 1.8.0 + version: 1.8.0 + videojs-vtt.js: + specifier: ^0.15.4 + version: 0.15.5 + yup: + specifier: ^1.3.2 + version: 1.7.1 + devDependencies: + '@babel/core': + specifier: ^7.20.12 + version: 7.28.4 + '@graphql-codegen/cli': + specifier: ^5.0.0 + version: 5.0.7(@parcel/watcher@2.5.1)(@types/node@18.19.130)(graphql@16.11.0)(typescript@4.8.4) + '@graphql-codegen/time': + specifier: ^5.0.0 + version: 5.0.1(graphql@16.11.0) + '@graphql-codegen/typescript': + specifier: ^4.0.1 + version: 4.1.6(graphql@16.11.0) + '@graphql-codegen/typescript-operations': + specifier: ^4.0.1 + version: 4.6.1(graphql@16.11.0) + '@graphql-codegen/typescript-react-apollo': + specifier: ^4.1.0 + version: 4.3.3(graphql@16.11.0) + '@types/apollo-upload-client': + specifier: ^18.0.0 + version: 18.0.1(@types/react@17.0.89)(graphql-ws@5.16.2(graphql@16.11.0))(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@types/crypto-js': + specifier: ^4.2.2 + version: 4.2.2 + '@types/dom-screen-wake-lock': + specifier: ^1.0.3 + version: 1.0.3 + '@types/lodash-es': + specifier: ^4.17.6 + version: 4.17.12 + '@types/mousetrap': + specifier: ^1.6.11 + version: 1.6.15 + '@types/node': + specifier: ^18.13.0 + version: 18.19.130 + '@types/react': + specifier: ^17.0.53 + version: 17.0.89 + '@types/react-datepicker': + specifier: ^4.10.0 + version: 4.19.6(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@types/react-dom': + specifier: ^17.0.19 + version: 17.0.26(@types/react@17.0.89) + '@types/react-helmet': + specifier: ^6.1.6 + version: 6.1.11 + '@types/react-router-bootstrap': + specifier: ^0.24.5 + version: 0.24.5 + '@types/react-router-hash-link': + specifier: ^2.4.5 + version: 2.4.9 + '@types/three': + specifier: ^0.154.0 + version: 0.154.0 + '@types/ua-parser-js': + specifier: ^0.7.36 + version: 0.7.39 + '@types/video.js': + specifier: ^7.3.51 + version: 7.3.58 + '@types/videojs-mobile-ui': + specifier: ^0.8.0 + version: 0.8.3 + '@types/videojs-seek-buttons': + specifier: ^2.1.0 + version: 2.1.3 + '@typescript-eslint/eslint-plugin': + specifier: ^5.52.0 + version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1)(typescript@4.8.4) + '@typescript-eslint/parser': + specifier: ^5.52.0 + version: 5.62.0(eslint@8.57.1)(typescript@4.8.4) + '@vitejs/plugin-legacy': + specifier: ^5.4.3 + version: 5.4.3(terser@5.44.0)(vite@5.4.21(@types/node@18.19.130)(sass@1.93.2)(terser@5.44.0)) + '@vitejs/plugin-react': + specifier: ^5.1.0 + version: 5.1.0(vite@5.4.21(@types/node@18.19.130)(sass@1.93.2)(terser@5.44.0)) + eslint: + specifier: ^8.34.0 + version: 8.57.1 + eslint-config-airbnb: + specifier: ^19.0.4 + version: 19.0.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1))(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint@8.57.1) + eslint-config-airbnb-typescript: + specifier: ^17.0.0 + version: 17.1.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1)(typescript@4.8.4))(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1))(eslint@8.57.1) + eslint-config-prettier: + specifier: ^8.6.0 + version: 8.10.2(eslint@8.57.1) + eslint-plugin-import: + specifier: ^2.27.5 + version: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1) + eslint-plugin-jsx-a11y: + specifier: ^6.7.1 + version: 6.10.2(eslint@8.57.1) + eslint-plugin-react: + specifier: ^7.32.2 + version: 7.37.5(eslint@8.57.1) + eslint-plugin-react-hooks: + specifier: ^4.6.0 + version: 4.6.2(eslint@8.57.1) + extract-react-intl-messages: + specifier: ^4.1.1 + version: 4.1.1 + postcss: + specifier: ^8.4.31 + version: 8.5.6 + postcss-scss: + specifier: ^4.0.6 + version: 4.0.9(postcss@8.5.6) + prettier: + specifier: ^2.8.4 + version: 2.8.8 + sass: + specifier: ^1.58.1 + version: 1.93.2 + stylelint: + specifier: ^15.10.1 + version: 15.11.0(typescript@4.8.4) + stylelint-order: + specifier: ^6.0.2 + version: 6.0.4(stylelint@15.11.0(typescript@4.8.4)) + terser: + specifier: ^5.9.0 + version: 5.44.0 + ts-node: + specifier: ^10.9.1 + version: 10.9.2(@types/node@18.19.130)(typescript@4.8.4) + typescript: + specifier: ~4.8.4 + version: 4.8.4 + vite: + specifier: ^5.4.21 + version: 5.4.21(@types/node@18.19.130)(sass@1.93.2)(terser@5.44.0) + vite-plugin-compression: + specifier: ^0.5.1 + version: 0.5.1(vite@5.4.21(@types/node@18.19.130)(sass@1.93.2)(terser@5.44.0)) + vite-tsconfig-paths: + specifier: ^4.0.5 + version: 4.3.2(typescript@4.8.4)(vite@5.4.21(@types/node@18.19.130)(sass@1.93.2)(terser@5.44.0)) + +packages: + + '@ant-design/react-slick@1.1.2': + resolution: {integrity: sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==} + peerDependencies: + react: '>=16.9.0' + + '@apollo/client@3.14.0': + resolution: {integrity: sha512-0YQKKRIxiMlIou+SekQqdCo0ZTHxOcES+K8vKB53cIDpwABNR0P0yRzPgsbgcj3zRJniD93S/ontsnZsCLZrxQ==} + peerDependencies: + graphql: ^15.0.0 || ^16.0.0 + graphql-ws: ^5.5.5 || ^6.0.3 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc + subscriptions-transport-ws: ^0.9.0 || ^0.11.0 + peerDependenciesMeta: + graphql-ws: + optional: true + react: + optional: true + react-dom: + optional: true + subscriptions-transport-ws: + optional: true + + '@ardatan/relay-compiler@12.0.0': + resolution: {integrity: sha512-9anThAaj1dQr6IGmzBMcfzOQKTa5artjuPmw8NYK/fiGEMjADbSguBY2FMDykt+QhilR3wc9VA/3yVju7JHg7Q==} + hasBin: true + peerDependencies: + graphql: '*' + + '@ardatan/relay-compiler@12.0.3': + resolution: {integrity: sha512-mBDFOGvAoVlWaWqs3hm1AciGHSQE1rqFc/liZTyYz/Oek9yZdT5H26pH2zAFuEiTiBVPPyMuqf5VjOFPI2DGsQ==} + hasBin: true + peerDependencies: + graphql: '*' + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.3': + resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.27.1': + resolution: {integrity: sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.5': + resolution: {integrity: sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.27.1': + resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.28.3': + resolution: {integrity: sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': + resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1': + resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1': + resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1': + resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3': + resolution: {integrity: sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-class-properties@7.18.6': + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-object-rest-spread@7.20.7': + resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-flow@7.27.1': + resolution: {integrity: sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.27.1': + resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.28.0': + resolution: {integrity: sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.27.1': + resolution: {integrity: sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.27.1': + resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.28.4': + resolution: {integrity: sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.27.1': + resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.28.3': + resolution: {integrity: sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.28.4': + resolution: {integrity: sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.27.1': + resolution: {integrity: sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.28.0': + resolution: {integrity: sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.27.1': + resolution: {integrity: sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.27.1': + resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1': + resolution: {integrity: sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.27.1': + resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-explicit-resource-management@7.28.0': + resolution: {integrity: sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.27.1': + resolution: {integrity: sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.27.1': + resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-flow-strip-types@7.27.1': + resolution: {integrity: sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.27.1': + resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.27.1': + resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.27.1': + resolution: {integrity: sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.27.1': + resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.27.1': + resolution: {integrity: sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.27.1': + resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.27.1': + resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.27.1': + resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.27.1': + resolution: {integrity: sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.27.1': + resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1': + resolution: {integrity: sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.27.1': + resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1': + resolution: {integrity: sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.27.1': + resolution: {integrity: sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.28.4': + resolution: {integrity: sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.27.1': + resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.27.1': + resolution: {integrity: sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.27.1': + resolution: {integrity: sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.27.7': + resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.27.1': + resolution: {integrity: sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.27.1': + resolution: {integrity: sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.27.1': + resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-display-name@7.28.0': + resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.27.1': + resolution: {integrity: sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.28.4': + resolution: {integrity: sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.27.1': + resolution: {integrity: sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.27.1': + resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.27.1': + resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.27.1': + resolution: {integrity: sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.27.1': + resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.27.1': + resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.27.1': + resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.27.1': + resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.27.1': + resolution: {integrity: sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.27.1': + resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.27.1': + resolution: {integrity: sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.28.3': + resolution: {integrity: sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@csstools/css-parser-algorithms@2.7.1': + resolution: {integrity: sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-tokenizer': ^2.4.1 + + '@csstools/css-tokenizer@2.4.1': + resolution: {integrity: sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==} + engines: {node: ^14 || ^16 || >=18} + + '@csstools/media-query-list-parser@2.1.13': + resolution: {integrity: sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-parser-algorithms': ^2.7.1 + '@csstools/css-tokenizer': ^2.4.1 + + '@csstools/selector-specificity@3.1.1': + resolution: {integrity: sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss-selector-parser: ^6.0.13 + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@envelop/core@5.3.2': + resolution: {integrity: sha512-06Mu7fmyKzk09P2i2kHpGfItqLLgCq7uO5/nX4fc/iHMplWPNuAx4iYR+WXUQoFHDnP6EUbceQNQ5iyeMz9f3g==} + engines: {node: '>=18.0.0'} + + '@envelop/instrumentation@1.0.0': + resolution: {integrity: sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==} + engines: {node: '>=18.0.0'} + + '@envelop/types@5.2.1': + resolution: {integrity: sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==} + engines: {node: '>=18.0.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@fastify/busboy@3.2.0': + resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@formatjs/ecma402-abstract@1.18.3': + resolution: {integrity: sha512-J961RbhyjHWeCIv+iOceNxpoZ/qomJOs5lH+rUJCeKNa59gME4KC0LJVMeWODjHsnv/hTH8Hvd6sevzcAzjuaQ==} + + '@formatjs/ecma402-abstract@1.4.0': + resolution: {integrity: sha512-Mv027hcLFjE45K8UJ8PjRpdDGfR0aManEFj1KzoN8zXNveHGEygpZGfFf/FTTMl+QEVSrPAUlyxaCApvmv47AQ==} + + '@formatjs/ecma402-abstract@1.5.0': + resolution: {integrity: sha512-wXv36yo+mfWllweN0Fq7sUs7PUiNopn7I0JpLTe3hGu6ZMR4CV7LqK1llhB18pndwpKoafQKb1et2DCJAOW20Q==} + + '@formatjs/ecma402-abstract@2.2.4': + resolution: {integrity: sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==} + + '@formatjs/ecma402-abstract@2.3.6': + resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} + + '@formatjs/fast-memoize@2.2.3': + resolution: {integrity: sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA==} + + '@formatjs/fast-memoize@2.2.7': + resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + + '@formatjs/icu-messageformat-parser@2.9.4': + resolution: {integrity: sha512-Tbvp5a9IWuxUcpWNIW6GlMQYEc4rwNHR259uUFoKWNN1jM9obf9Ul0e+7r7MvFOBNcN+13K7NuKCKqQiAn1QEg==} + + '@formatjs/icu-skeleton-parser@1.8.8': + resolution: {integrity: sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==} + + '@formatjs/intl-displaynames@6.8.5': + resolution: {integrity: sha512-85b+GdAKCsleS6cqVxf/Aw/uBd+20EM0wDpgaxzHo3RIR3bxF4xCJqH/Grbzx8CXurTgDDZHPdPdwJC+May41w==} + + '@formatjs/intl-enumerator@1.4.6': + resolution: {integrity: sha512-O2YMcE3SuBy4jL8r6YNq/8hvFrQ92QGLawdmzFbOi8D1r3VOfEMr8ifnOMp3zt8XemfTLrma+aF6yRCVeEbVLw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + '@formatjs/intl-getcanonicallocales@2.3.0': + resolution: {integrity: sha512-BOXbLwqQ7nKua/l7tKqDLRN84WupDXFDhGJQMFvsMVA2dKuOdRaWTxWpL3cJ7qPkoNw11Jf+Xpj4OSPBBvW0eQ==} + + '@formatjs/intl-getcanonicallocales@2.5.6': + resolution: {integrity: sha512-CnBbc4St61RL06gDXlCZG08Gt41uiySgsdZNBExh8/c0FBONJCrAlQ9FsyalUdq9ze0nCeknJtMmO8JnB9xHgQ==} + + '@formatjs/intl-listformat@7.7.5': + resolution: {integrity: sha512-Wzes10SMNeYgnxYiKsda4rnHP3Q3II4XT2tZyOgnH5fWuHDtIkceuWlRQNsvrI3uiwP4hLqp2XdQTCsfkhXulg==} + + '@formatjs/intl-locale@3.4.6': + resolution: {integrity: sha512-2TI0sBmIBhtM/BI/ePWuQhoqmMWveeKF4bUphs9YLHmFf4XmmlpWKzbPV8jR/fTK/KFidEuZsF+IgbOAL/OVGQ==} + + '@formatjs/intl-localematcher@0.5.4': + resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==} + + '@formatjs/intl-localematcher@0.5.8': + resolution: {integrity: sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==} + + '@formatjs/intl-localematcher@0.6.2': + resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} + + '@formatjs/intl-numberformat@5.7.6': + resolution: {integrity: sha512-ZlZfYtvbVHYZY5OG3RXizoCwxKxEKOrzEe2YOw9wbzoxF3PmFn0SAgojCFGLyNXkkR6xVxlylhbuOPf1dkIVNg==} + + '@formatjs/intl-numberformat@8.15.6': + resolution: {integrity: sha512-htynTNKm5WOnbR521tNSMkWzX3yO6Z77qjOxvRujh5/A/UBKeoNElyuKCJltizdx3X33QNWQZC4aWjLKcccyeQ==} + + '@formatjs/intl-pluralrules@5.4.6': + resolution: {integrity: sha512-2HlOq+c7KsSps829SJ3B5987coX5mzKx9NbPcNwQ07eq8FBHgB3HfMoxt5HvLsdk4oQwCjAEnocbtd+wVwZ2Kg==} + + '@formatjs/intl@2.10.15': + resolution: {integrity: sha512-i6+xVqT+6KCz7nBfk4ybMXmbKO36tKvbMKtgFz9KV+8idYFyFbfwKooYk8kGjyA5+T5f1kEPQM5IDLXucTAQ9g==} + peerDependencies: + typescript: ^4.7 || 5 + peerDependenciesMeta: + typescript: + optional: true + + '@formatjs/ts-transformer@2.13.0': + resolution: {integrity: sha512-mu7sHXZk1NWZrQ3eUqugpSYo8x5/tXkrI4uIbFqCEC0eNgQaIcoKgVeDFgDAcgG+cEme2atAUYSFF+DFWC4org==} + peerDependencies: + ts-jest: ^26.4.0 + peerDependenciesMeta: + ts-jest: + optional: true + + '@fortawesome/fontawesome-common-types@7.1.0': + resolution: {integrity: sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==} + engines: {node: '>=6'} + + '@fortawesome/fontawesome-svg-core@7.1.0': + resolution: {integrity: sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==} + engines: {node: '>=6'} + + '@fortawesome/free-brands-svg-icons@7.1.0': + resolution: {integrity: sha512-9byUd9bgNfthsZAjBl6GxOu1VPHgBuRUP9juI7ZoM98h8xNPTCTagfwUFyYscdZq4Hr7gD1azMfM9s5tIWKZZA==} + engines: {node: '>=6'} + + '@fortawesome/free-regular-svg-icons@7.1.0': + resolution: {integrity: sha512-0e2fdEyB4AR+e6kU4yxwA/MonnYcw/CsMEP9lH82ORFi9svA6/RhDyhxIv5mlJaldmaHLLYVTb+3iEr+PDSZuQ==} + engines: {node: '>=6'} + + '@fortawesome/free-solid-svg-icons@7.1.0': + resolution: {integrity: sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA==} + engines: {node: '>=6'} + + '@fortawesome/react-fontawesome@0.2.6': + resolution: {integrity: sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg==} + deprecated: v0.2.x is no longer supported. Unless you are still using FontAwesome 5, please update to v3.1.1 or greater. + peerDependencies: + '@fortawesome/fontawesome-svg-core': ~1 || ~6 || ~7 + react: ^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@graphql-codegen/add@5.0.3': + resolution: {integrity: sha512-SxXPmramkth8XtBlAHu4H4jYcYXM/o3p01+psU+0NADQowA8jtYkK6MW5rV6T+CxkEaNZItfSmZRPgIuypcqnA==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/cli@5.0.7': + resolution: {integrity: sha512-h/sxYvSaWtxZxo8GtaA8SvcHTyViaaPd7dweF/hmRDpaQU1o3iU3EZxlcJ+oLTunU0tSMFsnrIXm/mhXxI11Cw==} + engines: {node: '>=16'} + hasBin: true + peerDependencies: + '@parcel/watcher': ^2.1.0 + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + '@parcel/watcher': + optional: true + + '@graphql-codegen/client-preset@4.8.3': + resolution: {integrity: sha512-QpEsPSO9fnRxA6Z66AmBuGcwHjZ6dYSxYo5ycMlYgSPzAbyG8gn/kWljofjJfWqSY+T/lRn+r8IXTH14ml24vQ==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-sock: ^1.0.0 + peerDependenciesMeta: + graphql-sock: + optional: true + + '@graphql-codegen/core@4.0.2': + resolution: {integrity: sha512-IZbpkhwVqgizcjNiaVzNAzm/xbWT6YnGgeOLwVjm4KbJn3V2jchVtuzHH09G5/WkkLSk2wgbXNdwjM41JxO6Eg==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/gql-tag-operations@4.0.17': + resolution: {integrity: sha512-2pnvPdIG6W9OuxkrEZ6hvZd142+O3B13lvhrZ48yyEBh2ujtmKokw0eTwDHtlXUqjVS0I3q7+HB2y12G/m69CA==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/plugin-helpers@3.1.2': + resolution: {integrity: sha512-emOQiHyIliVOIjKVKdsI5MXj312zmRDwmHpyUTZMjfpvxq/UVAHUJIVdVf+lnjjrI+LXBTgMlTWTgHQfmICxjg==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/plugin-helpers@5.1.1': + resolution: {integrity: sha512-28GHODK2HY1NhdyRcPP3sCz0Kqxyfiz7boIZ8qIxFYmpLYnlDgiYok5fhFLVSZihyOpCs4Fa37gVHf/Q4I2FEg==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/schema-ast@4.1.0': + resolution: {integrity: sha512-kZVn0z+th9SvqxfKYgztA6PM7mhnSZaj4fiuBWvMTqA+QqQ9BBed6Pz41KuD/jr0gJtnlr2A4++/0VlpVbCTmQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/time@5.0.1': + resolution: {integrity: sha512-iHgS8+CmRofse8NRzKAkFKYVUzlZ5qYPpzjOA1Y+Fi8u4p90gOA5KjMqvMbAEcX1AWtR5cVtN2CJ2aENc36q8w==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/typed-document-node@5.1.2': + resolution: {integrity: sha512-jaxfViDqFRbNQmfKwUY8hDyjnLTw2Z7DhGutxoOiiAI0gE/LfPe0LYaVFKVmVOOD7M3bWxoWfu4slrkbWbUbEw==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/typescript-operations@4.6.1': + resolution: {integrity: sha512-k92laxhih7s0WZ8j5WMIbgKwhe64C0As6x+PdcvgZFMudDJ7rPJ/hFqJ9DCRxNjXoHmSjnr6VUuQZq4lT1RzCA==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-sock: ^1.0.0 + peerDependenciesMeta: + graphql-sock: + optional: true + + '@graphql-codegen/typescript-react-apollo@4.3.3': + resolution: {integrity: sha512-ecuzzqoZEHCtlxaEXL1LQTrfzVYwNNtbVUBHc/KQDfkJIQZon+dG5ZXOoJ4BpbRA2L99yTx+TZc2VkpOVfSypw==} + engines: {node: '>= 16.0.0'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/typescript@4.1.6': + resolution: {integrity: sha512-vpw3sfwf9A7S+kIUjyFxuvrywGxd4lmwmyYnnDVjVE4kSQ6Td3DpqaPTy8aNQ6O96vFoi/bxbZS2BW49PwSUUA==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/visitor-plugin-common@2.13.8': + resolution: {integrity: sha512-IQWu99YV4wt8hGxIbBQPtqRuaWZhkQRG2IZKbMoSvh0vGeWb3dB0n0hSgKaOOxDY+tljtOf9MTcUYvJslQucMQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/visitor-plugin-common@5.8.0': + resolution: {integrity: sha512-lC1E1Kmuzi3WZUlYlqB4fP6+CvbKH9J+haU1iWmgsBx5/sO2ROeXJG4Dmt8gP03bI2BwjiwV5WxCEMlyeuzLnA==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-hive/signal@1.0.0': + resolution: {integrity: sha512-RiwLMc89lTjvyLEivZ/qxAC5nBHoS2CtsWFSOsN35sxG9zoo5Z+JsFHM8MlvmO9yt+MJNIyC5MLE1rsbOphlag==} + engines: {node: '>=18.0.0'} + + '@graphql-tools/apollo-engine-loader@8.0.22': + resolution: {integrity: sha512-ssD2wNxeOTRcUEkuGcp0KfZAGstL9YLTe/y3erTDZtOs2wL1TJESw8NVAp+3oUHPeHKBZQB4Z6RFEbPgMdT2wA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/batch-execute@9.0.19': + resolution: {integrity: sha512-VGamgY4PLzSx48IHPoblRw0oTaBa7S26RpZXt0Y4NN90ytoE0LutlpB2484RbkfcTjv9wa64QD474+YP1kEgGA==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/code-file-loader@8.1.22': + resolution: {integrity: sha512-FSka29kqFkfFmw36CwoQ+4iyhchxfEzPbXOi37lCEjWLHudGaPkXc3RyB9LdmBxx3g3GHEu43a5n5W8gfcrMdA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/delegate@10.2.23': + resolution: {integrity: sha512-xrPtl7f1LxS+B6o+W7ueuQh67CwRkfl+UKJncaslnqYdkxKmNBB4wnzVcW8ZsRdwbsla/v43PtwAvSlzxCzq2w==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/documents@1.0.1': + resolution: {integrity: sha512-aweoMH15wNJ8g7b2r4C4WRuJxZ0ca8HtNO54rkye/3duxTkW4fGBEutCx03jCIr5+a1l+4vFJNP859QnAVBVCA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-common@0.0.4': + resolution: {integrity: sha512-SEH/OWR+sHbknqZyROCFHcRrbZeUAyjCsgpVWCRjqjqRbiJiXq6TxNIIOmpXgkrXWW/2Ev4Wms6YSGJXjdCs6Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-common@0.0.6': + resolution: {integrity: sha512-JAH/R1zf77CSkpYATIJw+eOJwsbWocdDjY+avY7G+P5HCXxwQjAjWVkJI1QJBQYjPQDVxwf1fmTZlIN3VOadow==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-graphql-ws@2.0.7': + resolution: {integrity: sha512-J27za7sKF6RjhmvSOwOQFeNhNHyP4f4niqPnerJmq73OtLx9Y2PGOhkXOEB0PjhvPJceuttkD2O1yMgEkTGs3Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-http@1.3.3': + resolution: {integrity: sha512-LIy+l08/Ivl8f8sMiHW2ebyck59JzyzO/yF9SFS4NH6MJZUezA1xThUXCDIKhHiD56h/gPojbkpcFvM2CbNE7A==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-legacy-ws@1.1.19': + resolution: {integrity: sha512-bEbv/SlEdhWQD0WZLUX1kOenEdVZk1yYtilrAWjRUgfHRZoEkY9s+oiqOxnth3z68wC2MWYx7ykkS5hhDamixg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor@1.4.9': + resolution: {integrity: sha512-SAUlDT70JAvXeqV87gGzvDzUGofn39nvaVcVhNf12Dt+GfWHtNNO/RCn/Ea4VJaSLGzraUd41ObnN3i80EBU7w==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/git-loader@8.0.26': + resolution: {integrity: sha512-0g+9eng8DaT4ZmZvUmPgjLTgesUa6M8xrDjNBltRldZkB055rOeUgJiKmL6u8PjzI5VxkkVsn0wtAHXhDI2UXQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/github-loader@8.0.22': + resolution: {integrity: sha512-uQ4JNcNPsyMkTIgzeSbsoT9hogLjYrZooLUYd173l5eUGUi49EAcsGdiBCKaKfEjanv410FE8hjaHr7fjSRkJw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/graphql-file-loader@8.1.2': + resolution: {integrity: sha512-VB6ttpwkqCu0KsA1/Wmev4qsu05Qfw49kgVSKkPjuyDQfVaqtr9ewEQRkX5CqnqHGEeLl6sOlNGEMM5fCVMWGQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/graphql-tag-pluck@8.3.21': + resolution: {integrity: sha512-TJhELNvR1tmghXMi6HVKp/Swxbx1rcSp/zdkuJZT0DCM3vOY11FXY6NW3aoxumcuYDNN3jqXcCPKstYGFPi5GQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/import@7.1.2': + resolution: {integrity: sha512-+tlNQbLEqAA4LdWoLwM1tckx95lo8WIKd8vhj99b9rLwN/KfLwHWzdS3jnUFK7+99vmHmN1oE5v5zmqJz0MTKw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/json-file-loader@8.0.20': + resolution: {integrity: sha512-5v6W+ZLBBML5SgntuBDLsYoqUvwfNboAwL6BwPHi3z/hH1f8BS9/0+MCW9OGY712g7E4pc3y9KqS67mWF753eA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/load@8.1.2': + resolution: {integrity: sha512-WhDPv25/jRND+0uripofMX0IEwo6mrv+tJg6HifRmDu8USCD7nZhufT0PP7lIcuutqjIQFyogqT70BQsy6wOgw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/merge@9.1.1': + resolution: {integrity: sha512-BJ5/7Y7GOhTuvzzO5tSBFL4NGr7PVqTJY3KeIDlVTT8YLcTXtBR+hlrC3uyEym7Ragn+zyWdHeJ9ev+nRX1X2w==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/optimize@1.4.0': + resolution: {integrity: sha512-dJs/2XvZp+wgHH8T5J2TqptT9/6uVzIYvA6uFACha+ufvdMBedkfR4b4GbT8jAKLRARiqRTxy3dctnwkTM2tdw==} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/optimize@2.0.0': + resolution: {integrity: sha512-nhdT+CRGDZ+bk68ic+Jw1OZ99YCDIKYA5AlVAnBHJvMawSx9YQqQAIj4refNc1/LRieGiuWvhbG3jvPVYho0Dg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/prisma-loader@8.0.17': + resolution: {integrity: sha512-fnuTLeQhqRbA156pAyzJYN0KxCjKYRU5bz1q/SKOwElSnAU4k7/G1kyVsWLh7fneY78LoMNH5n+KlFV8iQlnyg==} + engines: {node: '>=16.0.0'} + deprecated: 'This package was intended to be used with an older versions of Prisma.\nThe newer versions of Prisma has a different approach to GraphQL integration.\nTherefore, this package is no longer needed and has been deprecated and removed.\nLearn more: https://www.prisma.io/graphql' + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/relay-operation-optimizer@6.5.18': + resolution: {integrity: sha512-mc5VPyTeV+LwiM+DNvoDQfPqwQYhPV/cl5jOBjTgSniyaq8/86aODfMkrE2OduhQ5E00hqrkuL2Fdrgk0w1QJg==} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/relay-operation-optimizer@7.0.21': + resolution: {integrity: sha512-vMdU0+XfeBh9RCwPqRsr3A05hPA3MsahFn/7OAwXzMySA5EVnSH5R4poWNs3h1a0yT0tDPLhxORhK7qJdSWj2A==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/schema@10.0.25': + resolution: {integrity: sha512-/PqE8US8kdQ7lB9M5+jlW8AyVjRGCKU7TSktuW3WNKSKmDO0MK1wakvb5gGdyT49MjAIb4a3LWxIpwo5VygZuw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/url-loader@8.0.33': + resolution: {integrity: sha512-Fu626qcNHcqAj8uYd7QRarcJn5XZ863kmxsg1sm0fyjyfBJnsvC7ddFt6Hayz5kxVKfsnjxiDfPMXanvsQVBKw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/utils@10.9.1': + resolution: {integrity: sha512-B1wwkXk9UvU7LCBkPs8513WxOQ2H8Fo5p8HR1+Id9WmYE5+bd51vqN+MbrqvWczHCH2gwkREgHJN88tE0n1FCw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/utils@9.2.1': + resolution: {integrity: sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/wrap@10.1.4': + resolution: {integrity: sha512-7pyNKqXProRjlSdqOtrbnFRMQAVamCmEREilOXtZujxY6kYit3tvWWSjUrcIOheltTffoRh7EQSjpy2JDCzasg==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-typed-document-node/core@3.2.0': + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@inquirer/external-editor@1.0.2': + resolution: {integrity: sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + + '@mapbox/hast-util-table-cell-style@0.2.1': + resolution: {integrity: sha512-LyQz4XJIdCdY/+temIhD/Ed0x/p4GAOUycpFSEK2Ads1CPKZy6b7V/2ROEtQiLLQ8soIs0xe/QAoR6kwpyW/yw==} + engines: {node: '>=12'} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@react-hook/latest@1.0.3': + resolution: {integrity: sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==} + peerDependencies: + react: '>=16.8' + + '@react-hook/passive-layout-effect@1.2.1': + resolution: {integrity: sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==} + peerDependencies: + react: '>=16.8' + + '@react-hook/resize-observer@1.2.6': + resolution: {integrity: sha512-DlBXtLSW0DqYYTW3Ft1/GQFZlTdKY5VAFIC4+km6IK5NiPPDFchGbEJm1j6pSgMqPRHbUQgHJX7RaR76ic1LWA==} + peerDependencies: + react: '>=16.8' + + '@repeaterjs/repeater@3.0.6': + resolution: {integrity: sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==} + + '@restart/context@2.1.4': + resolution: {integrity: sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==} + peerDependencies: + react: '>=16.3.2' + + '@restart/hooks@0.4.16': + resolution: {integrity: sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==} + peerDependencies: + react: '>=16.8.0' + + '@rolldown/pluginutils@1.0.0-beta.43': + resolution: {integrity: sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==} + + '@rollup/rollup-android-arm-eabi@4.53.1': + resolution: {integrity: sha512-bxZtughE4VNVJlL1RdoSE545kc4JxL7op57KKoi59/gwuU5rV6jLWFXXc8jwgFoT6vtj+ZjO+Z2C5nrY0Cl6wA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.53.1': + resolution: {integrity: sha512-44a1hreb02cAAfAKmZfXVercPFaDjqXCK+iKeVOlJ9ltvnO6QqsBHgKVPTu+MJHSLLeMEUbeG2qiDYgbFPU48g==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.53.1': + resolution: {integrity: sha512-usmzIgD0rf1syoOZ2WZvy8YpXK5G1V3btm3QZddoGSa6mOgfXWkkv+642bfUUldomgrbiLQGrPryb7DXLovPWQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.53.1': + resolution: {integrity: sha512-is3r/k4vig2Gt8mKtTlzzyaSQ+hd87kDxiN3uDSDwggJLUV56Umli6OoL+/YZa/KvtdrdyNfMKHzL/P4siOOmg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.53.1': + resolution: {integrity: sha512-QJ1ksgp/bDJkZB4daldVmHaEQkG4r8PUXitCOC2WRmRaSaHx5RwPoI3DHVfXKwDkB+Sk6auFI/+JHacTekPRSw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.53.1': + resolution: {integrity: sha512-J6ma5xgAzvqsnU6a0+jgGX/gvoGokqpkx6zY4cWizRrm0ffhHDpJKQgC8dtDb3+MqfZDIqs64REbfHDMzxLMqQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.53.1': + resolution: {integrity: sha512-JzWRR41o2U3/KMNKRuZNsDUAcAVUYhsPuMlx5RUldw0E4lvSIXFUwejtYz1HJXohUmqs/M6BBJAUBzKXZVddbg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.53.1': + resolution: {integrity: sha512-L8kRIrnfMrEoHLHtHn+4uYA52fiLDEDyezgxZtGUTiII/yb04Krq+vk3P2Try+Vya9LeCE9ZHU8CXD6J9EhzHQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.53.1': + resolution: {integrity: sha512-ysAc0MFRV+WtQ8li8hi3EoFi7us6d1UzaS/+Dp7FYZfg3NdDljGMoVyiIp6Ucz7uhlYDBZ/zt6XI0YEZbUO11Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.53.1': + resolution: {integrity: sha512-UV6l9MJpDbDZZ/fJvqNcvO1PcivGEf1AvKuTcHoLjVZVFeAMygnamCTDikCVMRnA+qJe+B3pSbgX2+lBMqgBhA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.53.1': + resolution: {integrity: sha512-UDUtelEprkA85g95Q+nj3Xf0M4hHa4DiJ+3P3h4BuGliY4NReYYqwlc0Y8ICLjN4+uIgCEvaygYlpf0hUj90Yg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.53.1': + resolution: {integrity: sha512-vrRn+BYhEtNOte/zbc2wAUQReJXxEx2URfTol6OEfY2zFEUK92pkFBSXRylDM7aHi+YqEPJt9/ABYzmcrS4SgQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.53.1': + resolution: {integrity: sha512-gto/1CxHyi4A7YqZZNznQYrVlPSaodOBPKM+6xcDSCMVZN/Fzb4K+AIkNz/1yAYz9h3Ng+e2fY9H6bgawVq17w==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.53.1': + resolution: {integrity: sha512-KZ6Vx7jAw3aLNjFR8eYVcQVdFa/cvBzDNRFM3z7XhNNunWjA03eUrEwJYPk0G8V7Gs08IThFKcAPS4WY/ybIrQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.53.1': + resolution: {integrity: sha512-HvEixy2s/rWNgpwyKpXJcHmE7om1M89hxBTBi9Fs6zVuLU4gOrEMQNbNsN/tBVIMbLyysz/iwNiGtMOpLAOlvA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.53.1': + resolution: {integrity: sha512-E/n8x2MSjAQgjj9IixO4UeEUeqXLtiA7pyoXCFYLuXpBA/t2hnbIdxHfA7kK9BFsYAoNU4st1rHYdldl8dTqGA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.53.1': + resolution: {integrity: sha512-IhJ087PbLOQXCN6Ui/3FUkI9pWNZe/Z7rEIVOzMsOs1/HSAECCvSZ7PkIbkNqL/AZn6WbZvnoVZw/qwqYMo4/w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.53.1': + resolution: {integrity: sha512-0++oPNgLJHBblreu0SFM7b3mAsBJBTY0Ksrmu9N6ZVrPiTkRgda52mWR7TKhHAsUb9noCjFvAw9l6ZO1yzaVbA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.53.1': + resolution: {integrity: sha512-VJXivz61c5uVdbmitLkDlbcTk9Or43YC2QVLRkqp86QoeFSqI81bNgjhttqhKNMKnQMWnecOCm7lZz4s+WLGpQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.53.1': + resolution: {integrity: sha512-NmZPVTUOitCXUH6erJDzTQ/jotYw4CnkMDjCYRxNHVD9bNyfrGoIse684F9okwzKCV4AIHRbUkeTBc9F2OOH5Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.53.1': + resolution: {integrity: sha512-2SNj7COIdAf6yliSpLdLG8BEsp5lgzRehgfkP0Av8zKfQFKku6JcvbobvHASPJu4f3BFxej5g+HuQPvqPhHvpQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.53.1': + resolution: {integrity: sha512-rLarc1Ofcs3DHtgSzFO31pZsCh8g05R2azN1q3fF+H423Co87My0R+tazOEvYVKXSLh8C4LerMK41/K7wlklcg==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@silvermine/videojs-airplay@1.3.0': + resolution: {integrity: sha512-Oxq31DIEuKVt0qLj8/n5aaC9RRAc0hryarPVD9SFxPwCQ3A9Ef7bkRkGJz2i7XQxpIhPQ4SkM9BudUj6oHsPzA==} + peerDependencies: + video.js: '>= 6.0.0' + + '@silvermine/videojs-chromecast@1.5.0': + resolution: {integrity: sha512-oDWu0WT6NORWqpUHf5xg+GoLlxA/YV7guNDOGsDV51gOqYiKb2HoPXodDfhdzwHczUmPFmbyPwfSC1E+etAOmQ==} + peerDependencies: + video.js: '>= 6 < 9' + + '@theguild/federation-composition@0.20.1': + resolution: {integrity: sha512-lwYYKCeHmstOtbMtzxC0BQKWsUPYbEVRVdJ3EqR4jSpcF4gvNf3MOJv6yuvq6QsKqgYZURKRBszmg7VEDoi5Aw==} + engines: {node: '>=18'} + peerDependencies: + graphql: ^16.0.0 + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@tweenjs/tween.js@18.6.4': + resolution: {integrity: sha512-lB9lMjuqjtuJrx7/kOkqQBtllspPIN+96OvTCeJ2j5FEzinoAXTdAMFnDAQT1KVPRlnYfBrqxtqP66vDM40xxQ==} + + '@types/apollo-upload-client@18.0.1': + resolution: {integrity: sha512-qumgUkhs9pqJAxlDtzmn3WTrJ9oAHBb6i9A7aR1HQyjLpX9+LRL5V84aErv5ZwcCSR2zEgG8cFsuBVYfZHFSRA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/cookie@0.3.3': + resolution: {integrity: sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==} + + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + + '@types/dom-screen-wake-lock@1.0.3': + resolution: {integrity: sha512-3Iten7X3Zgwvk6kh6/NRdwN7WbZ760YgFCsF5AxDifltUQzW1RaW+WRmcVtgwFzLjaNu64H+0MPJ13yRa8g3Dw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/extract-files@13.0.2': + resolution: {integrity: sha512-4sd7uDB0OVZmwH2wD6w7Qlpr2P5Pn8C9IGwnaq9aiiBDD3Lou7CwFjjkJTDYCDsEvk9zxAtmv9TaMg1lt/YJfA==} + + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + + '@types/history@4.7.11': + resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} + + '@types/hoist-non-react-statics@3.3.7': + resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} + peerDependencies: + '@types/react': '*' + + '@types/invariant@2.2.37': + resolution: {integrity: sha512-IwpIMieE55oGWiXkQPSBY1nw1nFs6bsKXTFskNY8sdS17K24vyEBRQZEwlRS7ZmXCWnJcQtbxWzly+cODWGs2A==} + + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/mdast@3.0.15': + resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} + + '@types/minimist@1.2.5': + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + + '@types/mousetrap@1.6.15': + resolution: {integrity: sha512-qL0hyIMNPow317QWW/63RvL1x5MVMV+Ru3NaY9f/CuEpCqrmb7WeuK2071ZY5hczOnm38qExWM2i2WtkXLSqFw==} + + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-datepicker@4.19.6': + resolution: {integrity: sha512-uH5fzxt9eXxnc+hDCy/iRSFqU2+9lR/q2lAmaG4WILMai1o3IOdpcV+VSypzBFJLTEC2jrfeDXcdol0CJVMq4g==} + + '@types/react-dom@17.0.26': + resolution: {integrity: sha512-Z+2VcYXJwOqQ79HreLU/1fyQ88eXSSFh6I3JdrEHQIfYSI0kCQpTGvOrbE6jFGGYXKsHuwY9tBa/w5Uo6KzrEg==} + peerDependencies: + '@types/react': ^17.0.0 + + '@types/react-helmet@6.1.11': + resolution: {integrity: sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==} + + '@types/react-router-bootstrap@0.24.5': + resolution: {integrity: sha512-GRx/8xF/skw4/Pmm6d+xbExi8gobCLOe8Eoz9kXPQGbYo7p5Wbi61tjpOF5AbfJ5XMN+fIzweToTi56odj/LOQ==} + + '@types/react-router-dom@5.3.3': + resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} + + '@types/react-router-hash-link@2.4.9': + resolution: {integrity: sha512-zl/VMj+lfJZhvjOAQXIlBVPNKSK+/fRG8AUHhlP9++LhlA2ziLeTmbRxIMJI3PCiCTS+W/FosEoDRoNOGH0OzA==} + + '@types/react-router@5.1.20': + resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} + + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + + '@types/react@17.0.89': + resolution: {integrity: sha512-I98SaDCar5lvEYl80ClRIUztH/hyWHR+I2f+5yTVp/MQ205HgYkA2b5mVdry/+nsEIrf8I65KA5V/PASx68MsQ==} + + '@types/scheduler@0.16.8': + resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} + + '@types/schema-utils@2.4.0': + resolution: {integrity: sha512-454hrj5gz/FXcUE20ygfEiN4DxZ1sprUo0V1gqIqkNZ/CzoEzAZEll2uxMsuyz6BYjiQan4Aa65xbTemfzW9hQ==} + deprecated: This is a stub types definition. schema-utils provides its own type definitions, so you do not need this installed. + + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + + '@types/three@0.154.0': + resolution: {integrity: sha512-IioqpGhch6FdLDh4zazRn3rXHj6Vn2nVOziJdXVbJFi9CaI65LtP9qqUtpzbsHK2Ezlox8NtsLNHSw3AQzucjA==} + + '@types/ua-parser-js@0.7.39': + resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/video.js@7.3.58': + resolution: {integrity: sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==} + + '@types/videojs-mobile-ui@0.8.3': + resolution: {integrity: sha512-0PqZblRCggDP6GRMkQA707ZKWi3qFbw5jeLEF6FxqtAQGFrRJOCj1Tuk70n7dnA/CMKy7gwZfskqvCOvgnFzZQ==} + + '@types/videojs-seek-buttons@2.1.3': + resolution: {integrity: sha512-zDCTkvkwpGtD3ZechuQn8MhvLBRMsSCkhnftbVFg9xKtHUSPBQ1lCPpGasav3m8P4+9cXZRhi8aw+Dsk2syKIQ==} + + '@types/warning@3.0.3': + resolution: {integrity: sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==} + + '@types/webxr@0.5.24': + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@typescript-eslint/eslint-plugin@5.62.0': + resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@5.62.0': + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@5.62.0': + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/type-utils@5.62.0': + resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@5.62.0': + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/typescript-estree@5.62.0': + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@5.62.0': + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@typescript-eslint/visitor-keys@5.62.0': + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@videojs/http-streaming@2.16.3': + resolution: {integrity: sha512-91CJv5PnFBzNBvyEjt+9cPzTK/xoVixARj2g7ZAvItA+5bx8VKdk5RxCz/PP2kdzz9W+NiDUMPkdmTsosmy69Q==} + engines: {node: '>=8', npm: '>=5'} + peerDependencies: + video.js: ^6 || ^7 + + '@videojs/vhs-utils@3.0.5': + resolution: {integrity: sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==} + engines: {node: '>=8', npm: '>=5'} + + '@videojs/xhr@2.6.0': + resolution: {integrity: sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==} + + '@vitejs/plugin-legacy@5.4.3': + resolution: {integrity: sha512-wsyXK9mascyplcqvww1gA1xYiy29iRHfyciw+a0t7qRNdzX6PdfSWmOoCi74epr87DujM+5J+rnnSv+4PazqVg==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + terser: ^5.4.0 + vite: ^5.0.0 + + '@vitejs/plugin-react@5.1.0': + resolution: {integrity: sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@whatwg-node/disposablestack@0.0.6': + resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/fetch@0.10.11': + resolution: {integrity: sha512-eR8SYtf9Nem1Tnl0IWrY33qJ5wCtIWlt3Fs3c6V4aAaTFLtkEQErXu3SSZg/XCHrj9hXSJ8/8t+CdMk5Qec/ZA==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/node-fetch@0.8.1': + resolution: {integrity: sha512-cQmQEo7IsI0EPX9VrwygXVzrVlX43Jb7/DBZSmpnC7xH4xkyOnn/HykHpTaQk7TUs7zh59A5uTGqx3p2Ouzffw==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/promise-helpers@1.3.2': + resolution: {integrity: sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==} + engines: {node: '>=16.0.0'} + + '@wry/caches@1.0.1': + resolution: {integrity: sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==} + engines: {node: '>=8'} + + '@wry/context@0.7.4': + resolution: {integrity: sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==} + engines: {node: '>=8'} + + '@wry/equality@0.5.7': + resolution: {integrity: sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==} + engines: {node: '>=8'} + + '@wry/trie@0.5.0': + resolution: {integrity: sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==} + engines: {node: '>=8'} + + '@xmldom/xmldom@0.8.11': + resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} + engines: {node: '>=10.0.0'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + aes-decrypter@3.1.3: + resolution: {integrity: sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + apollo-upload-client@18.0.1: + resolution: {integrity: sha512-OQvZg1rK05VNI79D658FUmMdoI2oB/KJKb6QGMa2Si25QXOaAvLMBFUEwJct7wf+19U8vk9ILhidBOU1ZWv6QA==} + engines: {node: ^18.15.0 || >=20.4.0} + peerDependencies: + '@apollo/client': ^3.8.0 + graphql: 14 - 16 + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + auto-bind@4.0.0: + resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} + engines: {node: '>=8'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.11.0: + resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + b64-to-blob@1.2.19: + resolution: {integrity: sha512-L3nSu8GgF4iEyNYakCQSfL2F5GI5aCXcot9mNTf+4N0/BMhpxqqHyOb6jIR24iq2xLjQZLG8FOt3gnUcV+9NVg==} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + babel-plugin-polyfill-corejs2@0.4.14: + resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.13.0: + resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.5: + resolution: {integrity: sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-react-intl@7.9.4: + resolution: {integrity: sha512-cMKrHEXrw43yT4M89Wbgq8A8N8lffSquj1Piwov/HVukR7jwOw8gf9btXNsQhT27ccyqEwy+M286JQYy0jby2g==} + deprecated: this package has been renamed to babel-plugin-formatjs + + babel-plugin-syntax-trailing-function-commas@7.0.0-beta.0: + resolution: {integrity: sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==} + + babel-preset-fbjs@3.4.0: + resolution: {integrity: sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow==} + peerDependencies: + '@babel/core': ^7.0.0 + + bail@1.0.5: + resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + + base64-blob@1.4.1: + resolution: {integrity: sha512-n5Ov4cPTbLBTX1PiFbaB5AmK7LMigO9HWh5Lzx+Kcx/yx1MppeeLYtAH8aLv1m++WNoHQnr+xbGSqcZinopwlw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.8.19: + resolution: {integrity: sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==} + hasBin: true + + bcp-47-match@1.0.3: + resolution: {integrity: sha512-LggQ4YTdjWQSKELZF5JwchnBa1u0pIQSZf5lSdOHEdbVP55h0qICA/FUp3+W99q0xqxYa1ZQizTUH87gecII5w==} + + bcp-47-normalize@1.1.1: + resolution: {integrity: sha512-jWZ1Jdu3cs0EZdfCkS0UE9Gg01PtxnChjEBySeB+Zo6nkqtFfnvtoQQgP1qU1Oo4qgJgxhTI6Sf9y/pZIhPs0A==} + + bcp-47@1.0.8: + resolution: {integrity: sha512-Y9y1QNBBtYtv7hcmoX0tR+tUNSFZGZ6OL6vKPObq8BbOhkCoyayF6ogfLTgAli/KuAEbsYHYUNq2AQuY6IuLag==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bootstrap@4.6.2: + resolution: {integrity: sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==} + deprecated: This version of Bootstrap is no longer supported. Please upgrade to the latest version. + peerDependencies: + jquery: 1.9.1 - 3 + popper.js: ^1.16.1 + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist-to-esbuild@2.1.1: + resolution: {integrity: sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + browserslist: '*' + + browserslist@4.26.3: + resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + + camelcase-keys@6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + + camelcase-keys@7.0.2: + resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} + engines: {node: '>=12'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001751: + resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} + + capital-case@1.0.4: + resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} + + cardboard-vr-display@1.0.19: + resolution: {integrity: sha512-+MjcnWKAkb95p68elqZLDPzoiF/dGncQilLGvPBM5ZorABp/ao3lCs7nnRcYBckmuNkg1V/5rdGDKoUaCVsHzQ==} + + ccount@1.1.0: + resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + change-case-all@1.0.15: + resolution: {integrity: sha512-3+GIFhk3sNuvFAJKU46o26OdzudQlPNBCu1ZQi3cMeMHhty1bhDxu2WrEilVNYaGvqUtR1VSigFcJOiS13dRhQ==} + + change-case@4.1.2: + resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==} + + character-entities-legacy@1.1.4: + resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} + + character-entities@1.2.4: + resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} + + character-reference-invalid@1.1.4: + resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + + chardet@2.1.0: + resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + codem-isoboxer@0.3.9: + resolution: {integrity: sha512-4XOTqEzBWrGOZaMd+sTED2hLpzfBbiQCf1W6OBGkIHqk1D8uwy8WFLazVbdQwfDpQ+vf39lqTGPa9IhWW0roTA==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + comma-separated-tokens@1.0.8: + resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confusing-browser-globals@1.0.11: + resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} + + constant-case@3.0.4: + resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + + core-js-compat@3.46.0: + resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==} + + core-js@3.46.0: + resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + + cross-inspect@1.0.1: + resolution: {integrity: sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==} + engines: {node: '>=16.0.0'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + + css-functions-list@3.2.3: + resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==} + engines: {node: '>=12 || >=16'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + dashjs@4.7.4: + resolution: {integrity: sha512-+hldo25QPP3H/NOwqUrvt4uKdMse60/Gsz9AUAnoYfhga8qHWq4nWiojUosOiigbigkDTCAn9ORcvUaKCvmfCA==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + dataloader@2.2.3: + resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==} + + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decamelize@5.0.1: + resolution: {integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==} + engines: {node: '>=10'} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@2.2.1: + resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + dependency-graph@0.11.0: + resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} + engines: {node: '>= 0.6.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + diacritics@1.3.0: + resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.238: + resolution: {integrity: sha512-khBdc+w/Gv+cS8e/Pbnaw/FXcBUeKrRVik9IxfXtgREOWyJhR4tj43n3amkVogJ/yeQUqzkrZcFhtIxIdqmmcQ==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.1: + resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-airbnb-base@15.0.0: + resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} + engines: {node: ^10.12.0 || >=12.0.0} + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.2 + + eslint-config-airbnb-typescript@17.1.0: + resolution: {integrity: sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.13.0 || ^6.0.0 + '@typescript-eslint/parser': ^5.0.0 || ^6.0.0 + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.3 + + eslint-config-airbnb@19.0.4: + resolution: {integrity: sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==} + engines: {node: ^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.3 + eslint-plugin-jsx-a11y: ^6.5.1 + eslint-plugin-react: ^7.28.0 + eslint-plugin-react-hooks: ^4.3.0 + + eslint-config-prettier@8.10.2: + resolution: {integrity: sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@4.6.2: + resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + event-target-polyfill@0.0.4: + resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extract-files@13.0.0: + resolution: {integrity: sha512-FXD+2Tsr8Iqtm3QZy1Zmwscca7Jx3mMC5Crr+sEP1I303Jy1CYMuYCm7hRTplFNg3XdUavErkxnTzpaqdSoi6g==} + engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} + + extract-react-intl-messages@4.1.1: + resolution: {integrity: sha512-dPogci5X7HVtV7VbUxajH/1YgfNRaW2VtEiVidZ/31Tq8314uzOtzVMNo0IrAPD2E+H1wHoPiu/j565TZsyIZg==} + engines: {node: '>=10'} + hasBin: true + + fast-deep-equal@2.0.1: + resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fbjs-css-vars@1.0.2: + resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} + + fbjs@3.0.5: + resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + fflate@0.6.10: + resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-entry-cache@7.0.2: + resolution: {integrity: sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==} + engines: {node: '>=12.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flag-icons@6.15.0: + resolution: {integrity: sha512-ARo9Q+aATZEjyjveeec9e+orx+xLWUBdOX9baOKoGqDzMbvZ65ghPhaHbVt5T7ZB+Q4OFsB4Hr+eQnpV8Q+dLA==} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + flexbin@0.2.0: + resolution: {integrity: sha512-dgCeT6/oVljr0eao0f7Eg2VXutK/+rp02J6Nkw22uTTFE4HSC7zfYRzjuy2/r0dhr/sUBRMJM2tMyOCi+HeU+A==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + formik@2.4.6: + resolution: {integrity: sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==} + peerDependencies: + react: '>=16.8.0' + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + gl-preserve-state@1.0.0: + resolution: {integrity: sha512-zQZ25l3haD4hvgJZ6C9+s0ebdkW9y+7U2qxvGu1uWOJh8a4RU+jURIKEQhf8elIlFpMH6CrAY2tH0mYrRjet3Q==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + graphql-config@5.1.5: + resolution: {integrity: sha512-mG2LL1HccpU8qg5ajLROgdsBzx/o2M6kgI3uAmoaXiSH9PCUbtIyLomLqUtCFaAeG2YCFsl0M5cfQ9rKmDoMVA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + cosmiconfig-toml-loader: ^1.0.0 + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + cosmiconfig-toml-loader: + optional: true + + graphql-request@6.1.0: + resolution: {integrity: sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==} + peerDependencies: + graphql: 14 - 16 + + graphql-tag@2.12.6: + resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} + engines: {node: '>=10'} + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + graphql-ws@5.16.2: + resolution: {integrity: sha512-E1uccsZxt/96jH/OwmLPuXMACILs76pKF2i3W861LpKBCYtGIyPQGtWLuBLkND4ox1KHns70e83PS4te50nvPQ==} + engines: {node: '>=10'} + peerDependencies: + graphql: '>=0.11 <=16' + + graphql-ws@6.0.6: + resolution: {integrity: sha512-zgfER9s+ftkGKUZgc0xbx8T7/HMO4AV5/YuYiFc+AtgcO5T0v8AxYYNQ+ltzuzDZgNkYJaFspm5MMYLjQzrkmw==} + engines: {node: '>=20'} + peerDependencies: + '@fastify/websocket': ^10 || ^11 + crossws: ~0.3 + graphql: ^15.10.1 || ^16 + uWebSockets.js: ^20 + ws: ^8 + peerDependenciesMeta: + '@fastify/websocket': + optional: true + crossws: + optional: true + uWebSockets.js: + optional: true + ws: + optional: true + + graphql@16.11.0: + resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-to-hyperscript@9.0.1: + resolution: {integrity: sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA==} + + header-case@2.0.4: + resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} + + history@4.10.1: + resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + html-entities@1.4.0: + resolution: {integrity: sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==} + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + i18n-iso-countries@7.14.0: + resolution: {integrity: sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==} + engines: {node: '>= 12'} + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + immutable@3.7.6: + resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==} + engines: {node: '>=0.8.0'} + + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-from@4.0.0: + resolution: {integrity: sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==} + engines: {node: '>=12.2'} + + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + + imsc@1.1.5: + resolution: {integrity: sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + + individual@2.0.0: + resolution: {integrity: sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + inline-style-parser@0.1.1: + resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} + + inquirer@8.2.7: + resolution: {integrity: sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==} + engines: {node: '>=12.0.0'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + intl-messageformat-parser@5.5.1: + resolution: {integrity: sha512-TvB3LqF2VtP6yI6HXlRT5TxX98HKha6hCcrg9dwlPwNaedVNuQA9KgBdtWKgiyakyCTYHQ+KJeFEstNKfZr64w==} + deprecated: We've written a new parser that's 6x faster and is backwards compatible. Please use @formatjs/icu-messageformat-parser + + intl-messageformat-parser@6.1.2: + resolution: {integrity: sha512-4GQDEPhl/ZMNDKwMsLqyw1LG2IAWjmLJXdmnRcHKeLQzpgtNYZI6lVw1279pqIkRk2MfKb9aDsVFzm565azK5A==} + deprecated: We've written a new parser that's 6x faster and is backwards compatible. Please use @formatjs/icu-messageformat-parser + + intl-messageformat@10.7.7: + resolution: {integrity: sha512-F134jIoeYMro/3I0h08D0Yt4N9o9pjddU/4IIxMMURqbAtI2wu70X8hvG1V48W49zXHXv3RKSF/po+0fDfsGjA==} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + is-absolute@1.0.0: + resolution: {integrity: sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==} + engines: {node: '>=0.10.0'} + + is-alphabetical@1.0.4: + resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} + + is-alphanumerical@1.0.4: + resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-decimal@1.0.4: + resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-function@1.0.2: + resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@1.0.4: + resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-lower-case@2.0.2: + resolution: {integrity: sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-relative@1.0.0: + resolution: {integrity: sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==} + engines: {node: '>=0.10.0'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-unc-path@1.0.0: + resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==} + engines: {node: '>=0.10.0'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-upper-case@2.0.2: + resolution: {integrity: sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + + jquery@3.7.1: + resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-to-pretty-yaml@1.2.2: + resolution: {integrity: sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==} + engines: {node: '>= 0.2.0'} + + json2mq@0.2.0: + resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keycode@2.2.1: + resolution: {integrity: sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + known-css-properties@0.29.0: + resolution: {integrity: sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lie@3.1.1: + resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + + lil-gui@0.17.0: + resolution: {integrity: sha512-MVBHmgY+uEbmJNApAaPbtvNh1RCAeMnKym82SBjtp5rODTYKWtM+MXHCifLe2H2Ti1HuBGBtK/5SyG4ShQ3pUQ==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + listr2@4.0.5: + resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} + engines: {node: '>=12'} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + + load-json-file@6.2.0: + resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==} + engines: {node: '>=8'} + + localforage@1.10.0: + resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.pick@4.4.0: + resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==} + deprecated: This package is deprecated. Use destructuring assignment syntax instead. + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@4.0.0: + resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} + engines: {node: '>=10'} + + longest-streak@2.0.4: + resolution: {integrity: sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lower-case-first@2.0.2: + resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==} + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + m3u8-parser@4.8.0: + resolution: {integrity: sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA==} + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} + + map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + + markdown-table@2.0.0: + resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + + mdast-util-definitions@4.0.0: + resolution: {integrity: sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==} + + mdast-util-find-and-replace@1.1.1: + resolution: {integrity: sha512-9cKl33Y21lyckGzpSmEQnIDjEfeeWelN5s1kUW1LwdB0Fkuq2u+4GdqcGEygYxJE8GVqCl0741bYXHgamfWAZA==} + + mdast-util-from-markdown@0.8.5: + resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==} + + mdast-util-gfm-autolink-literal@0.1.3: + resolution: {integrity: sha512-GjmLjWrXg1wqMIO9+ZsRik/s7PLwTaeCHVB7vRxUwLntZc8mzmTsLVr6HW1yLokcnhfURsn5zmSVdi3/xWWu1A==} + + mdast-util-gfm-strikethrough@0.2.3: + resolution: {integrity: sha512-5OQLXpt6qdbttcDG/UxYY7Yjj3e8P7X16LzvpX8pIQPYJ/C2Z1qFGMmcw+1PZMUM3Z8wt8NRfYTvCni93mgsgA==} + + mdast-util-gfm-table@0.1.6: + resolution: {integrity: sha512-j4yDxQ66AJSBwGkbpFEp9uG/LS1tZV3P33fN1gkyRB2LoRL+RR3f76m0HPHaby6F4Z5xr9Fv1URmATlRRUIpRQ==} + + mdast-util-gfm-task-list-item@0.1.6: + resolution: {integrity: sha512-/d51FFIfPsSmCIRNp7E6pozM9z1GYPIkSy1urQ8s/o4TC22BZ7DqfHFWiqBD23bc7J3vV1Fc9O4QIHBlfuit8A==} + + mdast-util-gfm@0.1.2: + resolution: {integrity: sha512-NNkhDx/qYcuOWB7xHUGWZYVXvjPFFd6afg6/e2g+SV4r9q5XUcCbV4Wfa3DLYIiD+xAEZc6K4MGaE/m0KDcPwQ==} + + mdast-util-to-hast@10.2.0: + resolution: {integrity: sha512-JoPBfJ3gBnHZ18icCwHR50orC9kNH81tiR1gs01D8Q5YpV6adHNO9nKNuFBCJQ941/32PT1a63UF/DitmS3amQ==} + + mdast-util-to-markdown@0.6.5: + resolution: {integrity: sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==} + + mdast-util-to-string@2.0.0: + resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + meow@10.1.5: + resolution: {integrity: sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + meow@6.1.1: + resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==} + engines: {node: '>=8'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + meros@1.3.2: + resolution: {integrity: sha512-Q3mobPbvEx7XbwhnC1J1r60+5H6EZyNccdzSz0eGexJRwouUtTZxPVRGdqKtxlpD84ScK4+tIGldkqDtCKdI0A==} + engines: {node: '>=13'} + peerDependencies: + '@types/node': '>=13' + peerDependenciesMeta: + '@types/node': + optional: true + + meshoptimizer@0.18.1: + resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==} + + micromark-extension-gfm-autolink-literal@0.5.7: + resolution: {integrity: sha512-ePiDGH0/lhcngCe8FtH4ARFoxKTUelMp4L7Gg2pujYD5CSMb9PbblnyL+AAMud/SNMyusbS2XDSiPIRcQoNFAw==} + + micromark-extension-gfm-strikethrough@0.6.5: + resolution: {integrity: sha512-PpOKlgokpQRwUesRwWEp+fHjGGkZEejj83k9gU5iXCbDG+XBA92BqnRKYJdfqfkrRcZRgGuPuXb7DaK/DmxOhw==} + + micromark-extension-gfm-table@0.4.3: + resolution: {integrity: sha512-hVGvESPq0fk6ALWtomcwmgLvH8ZSVpcPjzi0AjPclB9FsVRgMtGZkUcpE0zgjOCFAznKepF4z3hX8z6e3HODdA==} + + micromark-extension-gfm-tagfilter@0.3.0: + resolution: {integrity: sha512-9GU0xBatryXifL//FJH+tAZ6i240xQuFrSL7mYi8f4oZSbc+NvXjkrHemeYP0+L4ZUT+Ptz3b95zhUZnMtoi/Q==} + + micromark-extension-gfm-task-list-item@0.3.3: + resolution: {integrity: sha512-0zvM5iSLKrc/NQl84pZSjGo66aTGd57C1idmlWmE87lkMcXrTxg1uXa/nXomxJytoje9trP0NDLvw4bZ/Z/XCQ==} + + micromark-extension-gfm@0.3.3: + resolution: {integrity: sha512-oVN4zv5/tAIA+l3GbMi7lWeYpJ14oQyJ3uEim20ktYFAcfX1x3LNlFGGlmrZHt7u9YlKExmyJdDGaTt6cMSR/A==} + + micromark@2.11.4: + resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + min-document@2.19.0: + resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + mousetrap-pause@1.0.0: + resolution: {integrity: sha512-/92qasq/TIkogCZKRYZdX+XAiPOD8dBDIipaar+caXSdKrfhYQIe6UmweiXO9yQeETjhNAUWdopwLsU6po/IPw==} + + mousetrap@1.6.5: + resolution: {integrity: sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==} + + mpd-parser@0.22.1: + resolution: {integrity: sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + mux.js@6.0.1: + resolution: {integrity: sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==} + engines: {node: '>=8', npm: '>=5'} + hasBin: true + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.26: + resolution: {integrity: sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==} + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + + normalize-path@2.1.1: + resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} + engines: {node: '>=0.10.0'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-url@4.5.1: + resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} + engines: {node: '>=8'} + + nosleep.js@0.7.0: + resolution: {integrity: sha512-Z4B1HgvzR+en62ghwZf6BwAR6x4/pjezsiMcbF9KMLh7xoscpoYhaSXfY3lLkqC68AtW+/qLJ1lzvBIj0FGaTA==} + + nullthrows@1.1.1: + resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optimism@0.18.1: + resolution: {integrity: sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-entities@2.0.0: + resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} + + parse-filepath@1.0.2: + resolution: {integrity: sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==} + engines: {node: '>=0.8'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-case@3.0.4: + resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-root-regex@0.1.2: + resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} + engines: {node: '>=0.10.0'} + + path-root@0.1.1: + resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} + engines: {node: '>=0.10.0'} + + path-to-regexp@1.9.0: + resolution: {integrity: sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@5.0.0: + resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} + engines: {node: '>=10'} + + pkcs7@1.0.4: + resolution: {integrity: sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==} + hasBin: true + + popper.js@1.16.1: + resolution: {integrity: sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==} + deprecated: You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1 + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@6.0.0: + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-sorting@8.0.2: + resolution: {integrity: sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==} + peerDependencies: + postcss: ^8.4.20 + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + promise@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + + prop-types-extra@1.1.1: + resolution: {integrity: sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==} + peerDependencies: + react: '>=0.14.0' + + prop-types@15.7.2: + resolution: {integrity: sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + + property-information@5.6.0: + resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + react-bootstrap@1.6.8: + resolution: {integrity: sha512-yD6uN78XlFOkETQp6GRuVe0s5509x3XYx8PfPbirwFTYCj5/RfmSs9YZGCwkUrhZNFzj7tZPdpb+3k50mK1E4g==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + react-datepicker@4.25.0: + resolution: {integrity: sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg==} + peerDependencies: + react: ^16.9.0 || ^17 || ^18 + react-dom: ^16.9.0 || ^17 || ^18 + + react-dom@17.0.2: + resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==} + peerDependencies: + react: 17.0.2 + + react-fast-compare@2.0.4: + resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==} + + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + react-helmet@6.1.0: + resolution: {integrity: sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==} + peerDependencies: + react: '>=16.3.0' + + react-intl@6.8.9: + resolution: {integrity: sha512-TUfj5E7lyUDvz/GtovC9OMh441kBr08rtIbgh3p0R8iF3hVY+V2W9Am7rb8BpJ/29BH1utJOqOOhmvEVh3GfZg==} + peerDependencies: + react: ^16.6.0 || 17 || 18 + typescript: ^4.7 || 5 + peerDependenciesMeta: + typescript: + optional: true + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + + react-onclickoutside@6.13.2: + resolution: {integrity: sha512-h6Hbf1c8b7tIYY4u90mDdBLY4+AGQVMFtIE89HgC0DtVCh/JfKl477gYqUtGLmjZBKK3MJxomP/lFiLbz4sq9A==} + peerDependencies: + react: ^15.5.x || ^16.x || ^17.x || ^18.x + react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x + + react-overlays@5.2.1: + resolution: {integrity: sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==} + peerDependencies: + react: '>=16.3.0' + react-dom: '>=16.3.0' + + react-photo-gallery@8.0.0: + resolution: {integrity: sha512-Y9458yygEB9cIZAWlBWuenlR+ghin1RopmmU3Vice8BeJl0Se7hzfxGDq8W1armB/ic/kphGg+G1jq5fOEd0sw==} + peerDependencies: + react: ^16.8.0 + react-dom: ^16.8.0 + + react-popper@2.3.0: + resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react-remark@2.1.0: + resolution: {integrity: sha512-7dEPxRGQ23sOdvteuRGaQAs9cEOH/BOeCN4CqsJdk3laUDIDYRCWnM6a3z92PzXHUuxIRLXQNZx7SiO0ijUcbw==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16.8' + + react-router-bootstrap@0.25.0: + resolution: {integrity: sha512-/22eqxjn6Zv5fvY2rZHn57SKmjmJfK7xzJ6/G1OgxAjLtKVfWgV5sn41W2yiqzbtV5eE4/i4LeDLBGYTqx7jbA==} + peerDependencies: + react: '>=0.14.0' + react-router-dom: '>=4.0.0' + + react-router-dom@5.3.4: + resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} + peerDependencies: + react: '>=15' + + react-router-hash-link@2.4.3: + resolution: {integrity: sha512-NU7GWc265m92xh/aYD79Vr1W+zAIXDWp3L2YZOYP4rCqPnJ6LI6vh3+rKgkidtYijozHclaEQTAHaAaMWPVI4A==} + peerDependencies: + react: '>=15' + react-router-dom: '>=4' + + react-router@5.3.4: + resolution: {integrity: sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==} + peerDependencies: + react: '>=15' + + react-select@5.10.2: + resolution: {integrity: sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-side-effect@2.1.2: + resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==} + peerDependencies: + react: ^16.3.0 || ^17.0.0 || ^18.0.0 + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@17.0.2: + resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==} + engines: {node: '>=0.10.0'} + + read-babelrc-up@1.1.0: + resolution: {integrity: sha512-fcl0JeI85Ss3//kfC3z2rsG2VxSiHl1bJgpjQWrne2YuQEewZpAgAjb17A6q/Q3ozWeZsUSroiIBVsnjmOU8vw==} + engines: {node: '>=10'} + + read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + + read-pkg-up@8.0.0: + resolution: {integrity: sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==} + engines: {node: '>=12'} + + read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + + read-pkg@6.0.0: + resolution: {integrity: sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==} + engines: {node: '>=12'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + redent@4.0.0: + resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} + engines: {node: '>=12'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regenerate-unicode-properties@10.2.2: + resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regexpu-core@6.4.0: + resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.13.0: + resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} + hasBin: true + + rehackt@0.1.0: + resolution: {integrity: sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==} + peerDependencies: + '@types/react': '*' + react: '*' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + + rehype-react@6.2.1: + resolution: {integrity: sha512-f9KIrjktvLvmbGc7si25HepocOg4z0MuNOtweigKzBcDjiGSTGhyz6VSgaV5K421Cq1O+z4/oxRJ5G9owo0KVg==} + + relay-runtime@12.0.0: + resolution: {integrity: sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug==} + + remark-gfm@1.0.0: + resolution: {integrity: sha512-KfexHJCiqvrdBZVbQ6RopMZGwaXz6wFJEfByIuEwGf0arvITHjiKKZ1dpXujjH9KZdm1//XJQwgfnJ3lmXaDPA==} + + remark-parse@9.0.0: + resolution: {integrity: sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==} + + remark-rehype@8.1.0: + resolution: {integrity: sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA==} + + remedial@1.0.8: + resolution: {integrity: sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==} + + remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} + + remove-trailing-spaces@1.0.9: + resolution: {integrity: sha512-xzG7w5IRijvIkHIjDk65URsJJ7k4J95wmcArY5PRcmjldIOl7oTvG8+X2Ag690R7SfwiOcHrWZKVc1Pp5WIOzA==} + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pathname@3.0.0: + resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.53.1: + resolution: {integrity: sha512-n2I0V0lN3E9cxxMqBCT3opWOiQBzRN7UG60z/WDKqdX2zHUS/39lezBcsckZFsV6fUTSnfqI7kHf60jDAPGKug==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rust-result@1.0.0: + resolution: {integrity: sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-json-parse@4.0.0: + resolution: {integrity: sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass@1.93.2: + resolution: {integrity: sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==} + engines: {node: '>=14.0.0'} + hasBin: true + + sax@1.2.1: + resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} + + scheduler@0.20.2: + resolution: {integrity: sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==} + + schema-utils@2.7.1: + resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} + engines: {node: '>= 8.9.0'} + + scuid@1.1.0: + resolution: {integrity: sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + sentence-case@3.0.4: + resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + signedsource@1.0.0: + resolution: {integrity: sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + slick-carousel@1.8.1: + resolution: {integrity: sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==} + peerDependencies: + jquery: '>=1.8.0' + + snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + + sort-keys@4.2.0: + resolution: {integrity: sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@1.1.5: + resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.22: + resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==} + + sponge-case@1.0.1: + resolution: {integrity: sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-convert@0.2.1: + resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} + + string-env-interpolation@1.0.1: + resolution: {integrity: sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.replaceall@1.0.11: + resolution: {integrity: sha512-MtmYTo9i6i3Jpc0xuGVYd5GraPTml7vlZh4030YXRiBktXwYKYU7IDGJeMi008Dk8QKlgJUi/Q+oNnGKB++/fQ==} + engines: {node: '>= 0.4'} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + style-search@0.1.0: + resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} + + style-to-object@0.3.0: + resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==} + + stylelint-order@6.0.4: + resolution: {integrity: sha512-0UuKo4+s1hgQ/uAxlYU4h0o0HS4NiQDud0NAUNI0aa8FJdmYHA5ZZTFHiV5FpmE3071e9pZx5j0QpVJW5zOCUA==} + peerDependencies: + stylelint: ^14.0.0 || ^15.0.0 || ^16.0.1 + + stylelint@15.11.0: + resolution: {integrity: sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + swap-case@2.0.2: + resolution: {integrity: sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==} + + symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + + sync-fetch@0.6.0-2: + resolution: {integrity: sha512-c7AfkZ9udatCuAy9RSfiGPpeOKKUAUK5e1cXadLOGUjasdxqYqAK0jTNkM/FSEyJ3a5Ra27j/tw/PS0qLmaF/A==} + engines: {node: '>=18'} + + systemjs@6.15.1: + resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + terser@5.44.0: + resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} + engines: {node: '>=10'} + hasBin: true + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thehandy@1.1.0: + resolution: {integrity: sha512-ZifUw47kq6cKNiKLNgrnVPkBFbG+yR6tScgWy2INDnGT4XePhjRaQNni67rWn52nAOkotq9VyaK20OZoorHqTA==} + + three@0.93.0: + resolution: {integrity: sha512-Ys9+UBBsd6FxTZZl4BH7B4b2F+B2uR0cOwY7OQ/aCzU/VgO4Wmmr1LbWPH1fsTvSVik9KAuwxwOHlSC4IMGOLA==} + + throttle-debounce@5.0.2: + resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} + engines: {node: '>=12.22'} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + timeout-signal@2.0.0: + resolution: {integrity: sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==} + engines: {node: '>=16'} + + tiny-case@1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + title-case@3.0.3: + resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + + trim-newlines@4.1.1: + resolution: {integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==} + engines: {node: '>=12'} + + trough@1.0.5: + resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==} + + ts-invariant@0.10.3: + resolution: {integrity: sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==} + engines: {node: '>=8'} + + ts-log@2.2.7: + resolution: {integrity: sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==} + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.4.1: + resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} + + tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsutils@3.21.0: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + + typescript@4.8.4: + resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==} + engines: {node: '>=4.2.0'} + hasBin: true + + ua-parser-js@1.0.41: + resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + unc-path-regex@0.1.2: + resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} + engines: {node: '>=0.10.0'} + + uncontrollable@7.2.1: + resolution: {integrity: sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==} + peerDependencies: + react: '>=15.0.0' + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.1: + resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.2.0: + resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} + engines: {node: '>=4'} + + unified@9.2.2: + resolution: {integrity: sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==} + + unist-builder@2.0.3: + resolution: {integrity: sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==} + + unist-util-generated@1.1.6: + resolution: {integrity: sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==} + + unist-util-is@3.0.0: + resolution: {integrity: sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==} + + unist-util-is@4.1.0: + resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} + + unist-util-position@3.1.0: + resolution: {integrity: sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==} + + unist-util-stringify-position@2.0.3: + resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} + + unist-util-visit-parents@2.1.2: + resolution: {integrity: sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==} + + unist-util-visit-parents@3.1.1: + resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==} + + unist-util-visit@1.4.1: + resolution: {integrity: sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==} + + unist-util-visit@2.0.3: + resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==} + + universal-cookie@4.0.4: + resolution: {integrity: sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unixify@1.0.0: + resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==} + engines: {node: '>=0.10.0'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + upper-case-first@2.0.2: + resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} + + upper-case@2.0.2: + resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-toolkit@2.2.5: + resolution: {integrity: sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==} + + urlpattern-polyfill@10.1.0: + resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} + + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + value-equal@1.0.1: + resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} + + vfile-message@2.0.4: + resolution: {integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==} + + vfile@4.2.1: + resolution: {integrity: sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==} + + video.js@7.21.7: + resolution: {integrity: sha512-T2s3WFAht7Zjr2OSJamND9x9Dn2O+Z5WuHGdh8jI5SYh5mkMdVTQ7vSRmA5PYpjXJ2ycch6jpMjkJEIEU2xxqw==} + + videojs-abloop@1.2.0: + resolution: {integrity: sha512-6/hvtB5gNQUr5FJ969UhXVg5H+3wxhOzh9AVftlezOXlhzzaWfNfiOJYqNKo01Gc/eSQOvfttrOX7jH+aHpwrw==} + + videojs-contrib-dash@5.1.1: + resolution: {integrity: sha512-MI0kPHuQ3KH9Mc2mLVLqvFKCoEyTfXzHc02fm8pqMk8v7LXrJKnIv9xfugBccRF7vZHDZISftedD/CmEJfvvrA==} + + videojs-font@3.2.0: + resolution: {integrity: sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==} + + videojs-mobile-ui@0.8.0: + resolution: {integrity: sha512-Jd+u/ctjUkbZlT1cAA0umTu0LQwSZSFG+02cJxShuwq27B6rfrRALETK/gsuTc7U27lB9fbwcF7HBMaNxW62nA==} + engines: {node: '>=14', npm: '>=6'} + peerDependencies: + video.js: ^6 || ^7 + + videojs-seek-buttons@3.0.1: + resolution: {integrity: sha512-scVWOqCMqHajlbwYZIzJ5nBYkDXTAhEpWjfcdCu8ykksA1barrKnEKdQvS84TtDWOx6UXDD/e/x0acYEZCDMEQ==} + engines: {node: '>=14', npm: '>=6'} + peerDependencies: + video.js: ^6 || ^7 + + videojs-vr@1.8.0: + resolution: {integrity: sha512-776gXqt8g6/rLeV56nn/aUcO0sRy+mgFITCw8cIqzTzl93SE1PEK/QE3YNqtppUfU5igayrx7WKsWhDOpsXMpw==} + + videojs-vtt.js@0.15.5: + resolution: {integrity: sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==} + + vite-plugin-compression@0.5.1: + resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} + peerDependencies: + vite: '>=2.0.0' + + vite-tsconfig-paths@4.3.2: + resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-namespaces@1.1.4: + resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webcomponents.js@https://codeload.github.com/webcomponents/webcomponentsjs/tar.gz/8a2e40557b177e2cca0def2553f84c8269c8f93e: + resolution: {tarball: https://codeload.github.com/webcomponents/webcomponentsjs/tar.gz/8a2e40557b177e2cca0def2553f84c8269c8f93e} + version: 0.7.24 + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webvr-polyfill-dpdb@1.0.18: + resolution: {integrity: sha512-O0S1ZGEWyPvyZEkS2VbyV7mtir/NM9MNK3EuhbHPoJ8EHTky2pTXehjIl+IiDPr+Lldgx129QGt3NGly7rwRPw==} + + webvr-polyfill@0.10.12: + resolution: {integrity: sha512-trDJEVUQnRIVAnmImjEQ0BlL1NfuWl8+eaEdu+bs4g59c7OtETi/5tFkgEFDRaWEYwHntXs/uFF3OXZuutNGGA==} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + write-json-file@4.3.0: + resolution: {integrity: sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ==} + engines: {node: '>=8.3'} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yup@1.7.1: + resolution: {integrity: sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==} + + zen-observable-ts@1.2.5: + resolution: {integrity: sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==} + + zen-observable@0.8.15: + resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==} + + zwitch@1.0.5: + resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} + +snapshots: + + '@ant-design/react-slick@1.1.2(react@17.0.2)': + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + json2mq: 0.2.0 + react: 17.0.2 + resize-observer-polyfill: 1.5.1 + throttle-debounce: 5.0.2 + + '@apollo/client@3.14.0(@types/react@17.0.89)(graphql-ws@5.16.2(graphql@16.11.0))(graphql@16.11.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.11.0) + '@wry/caches': 1.0.1 + '@wry/equality': 0.5.7 + '@wry/trie': 0.5.0 + graphql: 16.11.0 + graphql-tag: 2.12.6(graphql@16.11.0) + hoist-non-react-statics: 3.3.2 + optimism: 0.18.1 + prop-types: 15.8.1 + rehackt: 0.1.0(@types/react@17.0.89)(react@17.0.2) + symbol-observable: 4.0.0 + ts-invariant: 0.10.3 + tslib: 2.8.1 + zen-observable-ts: 1.2.5 + optionalDependencies: + graphql-ws: 5.16.2(graphql@16.11.0) + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + transitivePeerDependencies: + - '@types/react' + + '@ardatan/relay-compiler@12.0.0(graphql@16.11.0)': + dependencies: + '@babel/core': 7.28.4 + '@babel/generator': 7.28.3 + '@babel/parser': 7.28.4 + '@babel/runtime': 7.28.4 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + babel-preset-fbjs: 3.4.0(@babel/core@7.28.4) + chalk: 4.1.2 + fb-watchman: 2.0.2 + fbjs: 3.0.5 + glob: 7.2.3 + graphql: 16.11.0 + immutable: 3.7.6 + invariant: 2.2.4 + nullthrows: 1.1.1 + relay-runtime: 12.0.0 + signedsource: 1.0.0 + yargs: 15.4.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@ardatan/relay-compiler@12.0.3(graphql@16.11.0)': + dependencies: + '@babel/generator': 7.28.3 + '@babel/parser': 7.28.4 + '@babel/runtime': 7.28.4 + chalk: 4.1.2 + fb-watchman: 2.0.2 + graphql: 16.11.0 + immutable: 3.7.6 + invariant: 2.2.4 + nullthrows: 1.1.1 + relay-runtime: 12.0.0 + signedsource: 1.0.0 + transitivePeerDependencies: + - encoding + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.4 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.26.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.4 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.4.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.4 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.28.3 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helper-wrap-function@7.28.3': + dependencies: + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.28.4)': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/core': 7.28.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4) + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-flow@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4) + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-block-scoping@7.28.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.28.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/template': 7.27.2 + + '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.28.4) + + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-object-rest-spread@7.28.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4) + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-regenerator@7.28.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/preset-env@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/core': 7.28.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.3(@babel/core@7.28.4) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.4) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.4) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-block-scoping': 7.28.4(@babel/core@7.28.4) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-class-static-block': 7.28.3(@babel/core@7.28.4) + '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.28.4) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-object-rest-spread': 7.28.4(@babel/core@7.28.4) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-regenerator': 7.28.4(@babel/core@7.28.4) + '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.28.4) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.4) + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.4) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.4) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.4) + core-js-compat: 3.46.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/types': 7.28.4 + esutils: 2.0.3 + + '@babel/runtime@7.28.4': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1)': + dependencies: + '@csstools/css-tokenizer': 2.4.1 + + '@csstools/css-tokenizer@2.4.1': {} + + '@csstools/media-query-list-parser@2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1)': + dependencies: + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + + '@csstools/selector-specificity@3.1.1(postcss-selector-parser@6.1.2)': + dependencies: + postcss-selector-parser: 6.1.2 + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/runtime': 7.28.4 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@17.0.89)(react@17.0.2)': + dependencies: + '@babel/runtime': 7.28.4 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@17.0.2) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 17.0.2 + optionalDependencies: + '@types/react': 17.0.89 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.1.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@17.0.2)': + dependencies: + react: 17.0.2 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@envelop/core@5.3.2': + dependencies: + '@envelop/instrumentation': 1.0.0 + '@envelop/types': 5.2.1 + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@envelop/instrumentation@1.0.0': + dependencies: + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@envelop/types@5.2.1': + dependencies: + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@fastify/busboy@3.2.0': {} + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@formatjs/ecma402-abstract@1.18.3': + dependencies: + '@formatjs/intl-localematcher': 0.5.4 + tslib: 2.8.1 + + '@formatjs/ecma402-abstract@1.4.0': + dependencies: + tslib: 2.8.1 + + '@formatjs/ecma402-abstract@1.5.0': + dependencies: + tslib: 2.8.1 + + '@formatjs/ecma402-abstract@2.2.4': + dependencies: + '@formatjs/fast-memoize': 2.2.3 + '@formatjs/intl-localematcher': 0.5.8 + tslib: 2.8.1 + + '@formatjs/ecma402-abstract@2.3.6': + dependencies: + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/intl-localematcher': 0.6.2 + decimal.js: 10.6.0 + tslib: 2.8.1 + + '@formatjs/fast-memoize@2.2.3': + dependencies: + tslib: 2.8.1 + + '@formatjs/fast-memoize@2.2.7': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@2.9.4': + dependencies: + '@formatjs/ecma402-abstract': 2.2.4 + '@formatjs/icu-skeleton-parser': 1.8.8 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@1.8.8': + dependencies: + '@formatjs/ecma402-abstract': 2.2.4 + tslib: 2.8.1 + + '@formatjs/intl-displaynames@6.8.5': + dependencies: + '@formatjs/ecma402-abstract': 2.2.4 + '@formatjs/intl-localematcher': 0.5.8 + tslib: 2.8.1 + + '@formatjs/intl-enumerator@1.4.6': + dependencies: + tslib: 2.8.1 + + '@formatjs/intl-getcanonicallocales@2.3.0': + dependencies: + tslib: 2.8.1 + + '@formatjs/intl-getcanonicallocales@2.5.6': + dependencies: + tslib: 2.8.1 + + '@formatjs/intl-listformat@7.7.5': + dependencies: + '@formatjs/ecma402-abstract': 2.2.4 + '@formatjs/intl-localematcher': 0.5.8 + tslib: 2.8.1 + + '@formatjs/intl-locale@3.4.6': + dependencies: + '@formatjs/ecma402-abstract': 1.18.3 + '@formatjs/intl-enumerator': 1.4.6 + '@formatjs/intl-getcanonicallocales': 2.3.0 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.5.4': + dependencies: + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.5.8': + dependencies: + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.6.2': + dependencies: + tslib: 2.8.1 + + '@formatjs/intl-numberformat@5.7.6': + dependencies: + '@formatjs/ecma402-abstract': 1.4.0 + tslib: 2.8.1 + + '@formatjs/intl-numberformat@8.15.6': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/intl-localematcher': 0.6.2 + decimal.js: 10.6.0 + tslib: 2.8.1 + + '@formatjs/intl-pluralrules@5.4.6': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/intl-localematcher': 0.6.2 + decimal.js: 10.6.0 + tslib: 2.8.1 + + '@formatjs/intl@2.10.15(typescript@4.8.4)': + dependencies: + '@formatjs/ecma402-abstract': 2.2.4 + '@formatjs/fast-memoize': 2.2.3 + '@formatjs/icu-messageformat-parser': 2.9.4 + '@formatjs/intl-displaynames': 6.8.5 + '@formatjs/intl-listformat': 7.7.5 + intl-messageformat: 10.7.7 + tslib: 2.8.1 + optionalDependencies: + typescript: 4.8.4 + + '@formatjs/ts-transformer@2.13.0': + dependencies: + intl-messageformat-parser: 6.1.2 + tslib: 2.8.1 + typescript: 4.8.4 + + '@fortawesome/fontawesome-common-types@7.1.0': {} + + '@fortawesome/fontawesome-svg-core@7.1.0': + dependencies: + '@fortawesome/fontawesome-common-types': 7.1.0 + + '@fortawesome/free-brands-svg-icons@7.1.0': + dependencies: + '@fortawesome/fontawesome-common-types': 7.1.0 + + '@fortawesome/free-regular-svg-icons@7.1.0': + dependencies: + '@fortawesome/fontawesome-common-types': 7.1.0 + + '@fortawesome/free-solid-svg-icons@7.1.0': + dependencies: + '@fortawesome/fontawesome-common-types': 7.1.0 + + '@fortawesome/react-fontawesome@0.2.6(@fortawesome/fontawesome-svg-core@7.1.0)(react@17.0.2)': + dependencies: + '@fortawesome/fontawesome-svg-core': 7.1.0 + prop-types: 15.8.1 + react: 17.0.2 + + '@graphql-codegen/add@5.0.3(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.1(graphql@16.11.0) + graphql: 16.11.0 + tslib: 2.6.3 + + '@graphql-codegen/cli@5.0.7(@parcel/watcher@2.5.1)(@types/node@18.19.130)(graphql@16.11.0)(typescript@4.8.4)': + dependencies: + '@babel/generator': 7.28.3 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + '@graphql-codegen/client-preset': 4.8.3(graphql@16.11.0) + '@graphql-codegen/core': 4.0.2(graphql@16.11.0) + '@graphql-codegen/plugin-helpers': 5.1.1(graphql@16.11.0) + '@graphql-tools/apollo-engine-loader': 8.0.22(graphql@16.11.0) + '@graphql-tools/code-file-loader': 8.1.22(graphql@16.11.0) + '@graphql-tools/git-loader': 8.0.26(graphql@16.11.0) + '@graphql-tools/github-loader': 8.0.22(@types/node@18.19.130)(graphql@16.11.0) + '@graphql-tools/graphql-file-loader': 8.1.2(graphql@16.11.0) + '@graphql-tools/json-file-loader': 8.0.20(graphql@16.11.0) + '@graphql-tools/load': 8.1.2(graphql@16.11.0) + '@graphql-tools/prisma-loader': 8.0.17(@types/node@18.19.130)(graphql@16.11.0) + '@graphql-tools/url-loader': 8.0.33(@types/node@18.19.130)(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + '@whatwg-node/fetch': 0.10.11 + chalk: 4.1.2 + cosmiconfig: 8.3.6(typescript@4.8.4) + debounce: 1.2.1 + detect-indent: 6.1.0 + graphql: 16.11.0 + graphql-config: 5.1.5(@types/node@18.19.130)(graphql@16.11.0)(typescript@4.8.4) + inquirer: 8.2.7(@types/node@18.19.130) + is-glob: 4.0.3 + jiti: 1.21.7 + json-to-pretty-yaml: 1.2.2 + listr2: 4.0.5 + log-symbols: 4.1.0 + micromatch: 4.0.8 + shell-quote: 1.8.3 + string-env-interpolation: 1.0.1 + ts-log: 2.2.7 + tslib: 2.8.1 + yaml: 2.8.1 + yargs: 17.7.2 + optionalDependencies: + '@parcel/watcher': 2.5.1 + transitivePeerDependencies: + - '@fastify/websocket' + - '@types/node' + - bufferutil + - cosmiconfig-toml-loader + - crossws + - encoding + - enquirer + - graphql-sock + - supports-color + - typescript + - uWebSockets.js + - utf-8-validate + + '@graphql-codegen/client-preset@4.8.3(graphql@16.11.0)': + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@babel/template': 7.27.2 + '@graphql-codegen/add': 5.0.3(graphql@16.11.0) + '@graphql-codegen/gql-tag-operations': 4.0.17(graphql@16.11.0) + '@graphql-codegen/plugin-helpers': 5.1.1(graphql@16.11.0) + '@graphql-codegen/typed-document-node': 5.1.2(graphql@16.11.0) + '@graphql-codegen/typescript': 4.1.6(graphql@16.11.0) + '@graphql-codegen/typescript-operations': 4.6.1(graphql@16.11.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(graphql@16.11.0) + '@graphql-tools/documents': 1.0.1(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.11.0) + graphql: 16.11.0 + tslib: 2.6.3 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/core@4.0.2(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.1(graphql@16.11.0) + '@graphql-tools/schema': 10.0.25(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + graphql: 16.11.0 + tslib: 2.6.3 + + '@graphql-codegen/gql-tag-operations@4.0.17(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.1(graphql@16.11.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + auto-bind: 4.0.0 + graphql: 16.11.0 + tslib: 2.6.3 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/plugin-helpers@3.1.2(graphql@16.11.0)': + dependencies: + '@graphql-tools/utils': 9.2.1(graphql@16.11.0) + change-case-all: 1.0.15 + common-tags: 1.8.2 + graphql: 16.11.0 + import-from: 4.0.0 + lodash: 4.17.21 + tslib: 2.4.1 + + '@graphql-codegen/plugin-helpers@5.1.1(graphql@16.11.0)': + dependencies: + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + change-case-all: 1.0.15 + common-tags: 1.8.2 + graphql: 16.11.0 + import-from: 4.0.0 + lodash: 4.17.21 + tslib: 2.6.3 + + '@graphql-codegen/schema-ast@4.1.0(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.1(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + graphql: 16.11.0 + tslib: 2.6.3 + + '@graphql-codegen/time@5.0.1(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.1(graphql@16.11.0) + graphql: 16.11.0 + moment: 2.30.1 + + '@graphql-codegen/typed-document-node@5.1.2(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.1(graphql@16.11.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(graphql@16.11.0) + auto-bind: 4.0.0 + change-case-all: 1.0.15 + graphql: 16.11.0 + tslib: 2.6.3 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/typescript-operations@4.6.1(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.1(graphql@16.11.0) + '@graphql-codegen/typescript': 4.1.6(graphql@16.11.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(graphql@16.11.0) + auto-bind: 4.0.0 + graphql: 16.11.0 + tslib: 2.6.3 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/typescript-react-apollo@4.3.3(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 3.1.2(graphql@16.11.0) + '@graphql-codegen/visitor-plugin-common': 2.13.8(graphql@16.11.0) + auto-bind: 4.0.0 + change-case-all: 1.0.15 + graphql: 16.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@graphql-codegen/typescript@4.1.6(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.1(graphql@16.11.0) + '@graphql-codegen/schema-ast': 4.1.0(graphql@16.11.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(graphql@16.11.0) + auto-bind: 4.0.0 + graphql: 16.11.0 + tslib: 2.6.3 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/visitor-plugin-common@2.13.8(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 3.1.2(graphql@16.11.0) + '@graphql-tools/optimize': 1.4.0(graphql@16.11.0) + '@graphql-tools/relay-operation-optimizer': 6.5.18(graphql@16.11.0) + '@graphql-tools/utils': 9.2.1(graphql@16.11.0) + auto-bind: 4.0.0 + change-case-all: 1.0.15 + dependency-graph: 0.11.0 + graphql: 16.11.0 + graphql-tag: 2.12.6(graphql@16.11.0) + parse-filepath: 1.0.2 + tslib: 2.4.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@graphql-codegen/visitor-plugin-common@5.8.0(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.1(graphql@16.11.0) + '@graphql-tools/optimize': 2.0.0(graphql@16.11.0) + '@graphql-tools/relay-operation-optimizer': 7.0.21(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + auto-bind: 4.0.0 + change-case-all: 1.0.15 + dependency-graph: 0.11.0 + graphql: 16.11.0 + graphql-tag: 2.12.6(graphql@16.11.0) + parse-filepath: 1.0.2 + tslib: 2.6.3 + transitivePeerDependencies: + - encoding + + '@graphql-hive/signal@1.0.0': {} + + '@graphql-tools/apollo-engine-loader@8.0.22(graphql@16.11.0)': + dependencies: + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + '@whatwg-node/fetch': 0.10.11 + graphql: 16.11.0 + sync-fetch: 0.6.0-2 + tslib: 2.8.1 + + '@graphql-tools/batch-execute@9.0.19(graphql@16.11.0)': + dependencies: + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + '@whatwg-node/promise-helpers': 1.3.2 + dataloader: 2.2.3 + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-tools/code-file-loader@8.1.22(graphql@16.11.0)': + dependencies: + '@graphql-tools/graphql-tag-pluck': 8.3.21(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + globby: 11.1.0 + graphql: 16.11.0 + tslib: 2.8.1 + unixify: 1.0.0 + transitivePeerDependencies: + - supports-color + + '@graphql-tools/delegate@10.2.23(graphql@16.11.0)': + dependencies: + '@graphql-tools/batch-execute': 9.0.19(graphql@16.11.0) + '@graphql-tools/executor': 1.4.9(graphql@16.11.0) + '@graphql-tools/schema': 10.0.25(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + '@repeaterjs/repeater': 3.0.6 + '@whatwg-node/promise-helpers': 1.3.2 + dataloader: 2.2.3 + dset: 3.1.4 + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-tools/documents@1.0.1(graphql@16.11.0)': + dependencies: + graphql: 16.11.0 + lodash.sortby: 4.7.0 + tslib: 2.8.1 + + '@graphql-tools/executor-common@0.0.4(graphql@16.11.0)': + dependencies: + '@envelop/core': 5.3.2 + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + graphql: 16.11.0 + + '@graphql-tools/executor-common@0.0.6(graphql@16.11.0)': + dependencies: + '@envelop/core': 5.3.2 + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + graphql: 16.11.0 + + '@graphql-tools/executor-graphql-ws@2.0.7(graphql@16.11.0)': + dependencies: + '@graphql-tools/executor-common': 0.0.6(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + '@whatwg-node/disposablestack': 0.0.6 + graphql: 16.11.0 + graphql-ws: 6.0.6(graphql@16.11.0)(ws@8.18.3) + isomorphic-ws: 5.0.0(ws@8.18.3) + tslib: 2.8.1 + ws: 8.18.3 + transitivePeerDependencies: + - '@fastify/websocket' + - bufferutil + - crossws + - uWebSockets.js + - utf-8-validate + + '@graphql-tools/executor-http@1.3.3(@types/node@18.19.130)(graphql@16.11.0)': + dependencies: + '@graphql-hive/signal': 1.0.0 + '@graphql-tools/executor-common': 0.0.4(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + '@repeaterjs/repeater': 3.0.6 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/fetch': 0.10.11 + '@whatwg-node/promise-helpers': 1.3.2 + graphql: 16.11.0 + meros: 1.3.2(@types/node@18.19.130) + tslib: 2.8.1 + transitivePeerDependencies: + - '@types/node' + + '@graphql-tools/executor-legacy-ws@1.1.19(graphql@16.11.0)': + dependencies: + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + '@types/ws': 8.18.1 + graphql: 16.11.0 + isomorphic-ws: 5.0.0(ws@8.18.3) + tslib: 2.8.1 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@graphql-tools/executor@1.4.9(graphql@16.11.0)': + dependencies: + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.11.0) + '@repeaterjs/repeater': 3.0.6 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/promise-helpers': 1.3.2 + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-tools/git-loader@8.0.26(graphql@16.11.0)': + dependencies: + '@graphql-tools/graphql-tag-pluck': 8.3.21(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + graphql: 16.11.0 + is-glob: 4.0.3 + micromatch: 4.0.8 + tslib: 2.8.1 + unixify: 1.0.0 + transitivePeerDependencies: + - supports-color + + '@graphql-tools/github-loader@8.0.22(@types/node@18.19.130)(graphql@16.11.0)': + dependencies: + '@graphql-tools/executor-http': 1.3.3(@types/node@18.19.130)(graphql@16.11.0) + '@graphql-tools/graphql-tag-pluck': 8.3.21(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + '@whatwg-node/fetch': 0.10.11 + '@whatwg-node/promise-helpers': 1.3.2 + graphql: 16.11.0 + sync-fetch: 0.6.0-2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@types/node' + - supports-color + + '@graphql-tools/graphql-file-loader@8.1.2(graphql@16.11.0)': + dependencies: + '@graphql-tools/import': 7.1.2(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + globby: 11.1.0 + graphql: 16.11.0 + tslib: 2.8.1 + unixify: 1.0.0 + transitivePeerDependencies: + - supports-color + + '@graphql-tools/graphql-tag-pluck@8.3.21(graphql@16.11.0)': + dependencies: + '@babel/core': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.4) + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + graphql: 16.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@graphql-tools/import@7.1.2(graphql@16.11.0)': + dependencies: + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + '@theguild/federation-composition': 0.20.1(graphql@16.11.0) + graphql: 16.11.0 + resolve-from: 5.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@graphql-tools/json-file-loader@8.0.20(graphql@16.11.0)': + dependencies: + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + globby: 11.1.0 + graphql: 16.11.0 + tslib: 2.8.1 + unixify: 1.0.0 + + '@graphql-tools/load@8.1.2(graphql@16.11.0)': + dependencies: + '@graphql-tools/schema': 10.0.25(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + graphql: 16.11.0 + p-limit: 3.1.0 + tslib: 2.8.1 + + '@graphql-tools/merge@9.1.1(graphql@16.11.0)': + dependencies: + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-tools/optimize@1.4.0(graphql@16.11.0)': + dependencies: + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-tools/optimize@2.0.0(graphql@16.11.0)': + dependencies: + graphql: 16.11.0 + tslib: 2.6.3 + + '@graphql-tools/prisma-loader@8.0.17(@types/node@18.19.130)(graphql@16.11.0)': + dependencies: + '@graphql-tools/url-loader': 8.0.33(@types/node@18.19.130)(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + '@types/js-yaml': 4.0.9 + '@whatwg-node/fetch': 0.10.11 + chalk: 4.1.2 + debug: 4.4.3 + dotenv: 16.6.1 + graphql: 16.11.0 + graphql-request: 6.1.0(graphql@16.11.0) + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + jose: 5.10.0 + js-yaml: 4.1.0 + lodash: 4.17.21 + scuid: 1.1.0 + tslib: 2.8.1 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - '@fastify/websocket' + - '@types/node' + - bufferutil + - crossws + - encoding + - supports-color + - uWebSockets.js + - utf-8-validate + + '@graphql-tools/relay-operation-optimizer@6.5.18(graphql@16.11.0)': + dependencies: + '@ardatan/relay-compiler': 12.0.0(graphql@16.11.0) + '@graphql-tools/utils': 9.2.1(graphql@16.11.0) + graphql: 16.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@graphql-tools/relay-operation-optimizer@7.0.21(graphql@16.11.0)': + dependencies: + '@ardatan/relay-compiler': 12.0.3(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + graphql: 16.11.0 + tslib: 2.6.3 + transitivePeerDependencies: + - encoding + + '@graphql-tools/schema@10.0.25(graphql@16.11.0)': + dependencies: + '@graphql-tools/merge': 9.1.1(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-tools/url-loader@8.0.33(@types/node@18.19.130)(graphql@16.11.0)': + dependencies: + '@graphql-tools/executor-graphql-ws': 2.0.7(graphql@16.11.0) + '@graphql-tools/executor-http': 1.3.3(@types/node@18.19.130)(graphql@16.11.0) + '@graphql-tools/executor-legacy-ws': 1.1.19(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + '@graphql-tools/wrap': 10.1.4(graphql@16.11.0) + '@types/ws': 8.18.1 + '@whatwg-node/fetch': 0.10.11 + '@whatwg-node/promise-helpers': 1.3.2 + graphql: 16.11.0 + isomorphic-ws: 5.0.0(ws@8.18.3) + sync-fetch: 0.6.0-2 + tslib: 2.8.1 + ws: 8.18.3 + transitivePeerDependencies: + - '@fastify/websocket' + - '@types/node' + - bufferutil + - crossws + - uWebSockets.js + - utf-8-validate + + '@graphql-tools/utils@10.9.1(graphql@16.11.0)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.11.0) + '@whatwg-node/promise-helpers': 1.3.2 + cross-inspect: 1.0.1 + dset: 3.1.4 + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-tools/utils@9.2.1(graphql@16.11.0)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.11.0) + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-tools/wrap@10.1.4(graphql@16.11.0)': + dependencies: + '@graphql-tools/delegate': 10.2.23(graphql@16.11.0) + '@graphql-tools/schema': 10.0.25(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + '@whatwg-node/promise-helpers': 1.3.2 + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-typed-document-node/core@3.2.0(graphql@16.11.0)': + dependencies: + graphql: 16.11.0 + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@inquirer/external-editor@1.0.2(@types/node@18.19.130)': + dependencies: + chardet: 2.1.0 + iconv-lite: 0.7.0 + optionalDependencies: + '@types/node': 18.19.130 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@juggle/resize-observer@3.4.0': {} + + '@mapbox/hast-util-table-cell-style@0.2.1': + dependencies: + unist-util-visit: 1.4.1 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@popperjs/core@2.11.8': {} + + '@react-hook/latest@1.0.3(react@17.0.2)': + dependencies: + react: 17.0.2 + + '@react-hook/passive-layout-effect@1.2.1(react@17.0.2)': + dependencies: + react: 17.0.2 + + '@react-hook/resize-observer@1.2.6(react@17.0.2)': + dependencies: + '@juggle/resize-observer': 3.4.0 + '@react-hook/latest': 1.0.3(react@17.0.2) + '@react-hook/passive-layout-effect': 1.2.1(react@17.0.2) + react: 17.0.2 + + '@repeaterjs/repeater@3.0.6': {} + + '@restart/context@2.1.4(react@17.0.2)': + dependencies: + react: 17.0.2 + + '@restart/hooks@0.4.16(react@17.0.2)': + dependencies: + dequal: 2.0.3 + react: 17.0.2 + + '@rolldown/pluginutils@1.0.0-beta.43': {} + + '@rollup/rollup-android-arm-eabi@4.53.1': + optional: true + + '@rollup/rollup-android-arm64@4.53.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.53.1': + optional: true + + '@rollup/rollup-darwin-x64@4.53.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.53.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.53.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.53.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.53.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.53.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.53.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.53.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.53.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.53.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.53.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.53.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.53.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.53.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.53.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.53.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.53.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.53.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.53.1': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@silvermine/videojs-airplay@1.3.0(video.js@7.21.7)': + dependencies: + video.js: 7.21.7 + + '@silvermine/videojs-chromecast@1.5.0(video.js@7.21.7)': + dependencies: + video.js: 7.21.7 + webcomponents.js: https://codeload.github.com/webcomponents/webcomponentsjs/tar.gz/8a2e40557b177e2cca0def2553f84c8269c8f93e + + '@theguild/federation-composition@0.20.1(graphql@16.11.0)': + dependencies: + constant-case: 3.0.4 + debug: 4.4.1 + graphql: 16.11.0 + json5: 2.2.3 + lodash.sortby: 4.7.0 + transitivePeerDependencies: + - supports-color + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@tweenjs/tween.js@18.6.4': {} + + '@types/apollo-upload-client@18.0.1(@types/react@17.0.89)(graphql-ws@5.16.2(graphql@16.11.0))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + dependencies: + '@apollo/client': 3.14.0(@types/react@17.0.89)(graphql-ws@5.16.2(graphql@16.11.0))(graphql@16.11.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@types/extract-files': 13.0.2 + graphql: 16.11.0 + transitivePeerDependencies: + - '@types/react' + - graphql-ws + - react + - react-dom + - subscriptions-transport-ws + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/cookie@0.3.3': {} + + '@types/crypto-js@4.2.2': {} + + '@types/dom-screen-wake-lock@1.0.3': {} + + '@types/estree@1.0.8': {} + + '@types/extract-files@13.0.2': {} + + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 18.19.130 + + '@types/history@4.7.11': {} + + '@types/hoist-non-react-statics@3.3.7(@types/react@17.0.89)': + dependencies: + '@types/react': 17.0.89 + hoist-non-react-statics: 3.3.2 + + '@types/invariant@2.2.37': {} + + '@types/js-yaml@4.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.20 + + '@types/lodash@4.17.20': {} + + '@types/mdast@3.0.15': + dependencies: + '@types/unist': 2.0.11 + + '@types/minimist@1.2.5': {} + + '@types/mousetrap@1.6.15': {} + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + + '@types/normalize-package-data@2.4.4': {} + + '@types/parse-json@4.0.2': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-datepicker@4.19.6(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + dependencies: + '@popperjs/core': 2.11.8 + '@types/react': 17.0.89 + date-fns: 2.30.0 + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + transitivePeerDependencies: + - react + - react-dom + + '@types/react-dom@17.0.26(@types/react@17.0.89)': + dependencies: + '@types/react': 17.0.89 + + '@types/react-helmet@6.1.11': + dependencies: + '@types/react': 17.0.89 + + '@types/react-router-bootstrap@0.24.5': + dependencies: + '@types/react': 17.0.89 + '@types/react-router-dom': 5.3.3 + + '@types/react-router-dom@5.3.3': + dependencies: + '@types/history': 4.7.11 + '@types/react': 17.0.89 + '@types/react-router': 5.1.20 + + '@types/react-router-hash-link@2.4.9': + dependencies: + '@types/history': 4.7.11 + '@types/react': 17.0.89 + '@types/react-router-dom': 5.3.3 + + '@types/react-router@5.1.20': + dependencies: + '@types/history': 4.7.11 + '@types/react': 17.0.89 + + '@types/react-transition-group@4.4.12(@types/react@17.0.89)': + dependencies: + '@types/react': 17.0.89 + + '@types/react@17.0.89': + dependencies: + '@types/prop-types': 15.7.15 + '@types/scheduler': 0.16.8 + csstype: 3.1.3 + + '@types/scheduler@0.16.8': {} + + '@types/schema-utils@2.4.0': + dependencies: + schema-utils: 2.7.1 + + '@types/semver@7.7.1': {} + + '@types/stats.js@0.17.4': {} + + '@types/three@0.154.0': + dependencies: + '@tweenjs/tween.js': 18.6.4 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + fflate: 0.6.10 + lil-gui: 0.17.0 + meshoptimizer: 0.18.1 + + '@types/ua-parser-js@0.7.39': {} + + '@types/unist@2.0.11': {} + + '@types/video.js@7.3.58': {} + + '@types/videojs-mobile-ui@0.8.3': + dependencies: + '@types/video.js': 7.3.58 + + '@types/videojs-seek-buttons@2.1.3': + dependencies: + '@types/video.js': 7.3.58 + + '@types/warning@3.0.3': {} + + '@types/webxr@0.5.24': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 18.19.130 + + '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1)(typescript@4.8.4)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.8.4) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@4.8.4) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@4.8.4) + debug: 4.4.3 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare-lite: 1.4.0 + semver: 7.7.3 + tsutils: 3.21.0(typescript@4.8.4) + optionalDependencies: + typescript: 4.8.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4)': + dependencies: + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.8.4) + debug: 4.4.3 + eslint: 8.57.1 + optionalDependencies: + typescript: 4.8.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + + '@typescript-eslint/type-utils@5.62.0(eslint@8.57.1)(typescript@4.8.4)': + dependencies: + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.8.4) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@4.8.4) + debug: 4.4.3 + eslint: 8.57.1 + tsutils: 3.21.0(typescript@4.8.4) + optionalDependencies: + typescript: 4.8.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@5.62.0': {} + + '@typescript-eslint/typescript-estree@5.62.0(typescript@4.8.4)': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.7.3 + tsutils: 3.21.0(typescript@4.8.4) + optionalDependencies: + typescript: 4.8.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@4.8.4)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.1 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.8.4) + eslint: 8.57.1 + eslint-scope: 5.1.1 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.3.0': {} + + '@videojs/http-streaming@2.16.3(video.js@7.21.7)': + dependencies: + '@babel/runtime': 7.28.4 + '@videojs/vhs-utils': 3.0.5 + aes-decrypter: 3.1.3 + global: 4.4.0 + m3u8-parser: 4.8.0 + mpd-parser: 0.22.1 + mux.js: 6.0.1 + video.js: 7.21.7 + + '@videojs/vhs-utils@3.0.5': + dependencies: + '@babel/runtime': 7.28.4 + global: 4.4.0 + url-toolkit: 2.2.5 + + '@videojs/xhr@2.6.0': + dependencies: + '@babel/runtime': 7.28.4 + global: 4.4.0 + is-function: 1.0.2 + + '@vitejs/plugin-legacy@5.4.3(terser@5.44.0)(vite@5.4.21(@types/node@18.19.130)(sass@1.93.2)(terser@5.44.0))': + dependencies: + '@babel/core': 7.28.4 + '@babel/preset-env': 7.28.3(@babel/core@7.28.4) + browserslist: 4.26.3 + browserslist-to-esbuild: 2.1.1(browserslist@4.26.3) + core-js: 3.46.0 + magic-string: 0.30.19 + regenerator-runtime: 0.14.1 + systemjs: 6.15.1 + terser: 5.44.0 + vite: 5.4.21(@types/node@18.19.130)(sass@1.93.2)(terser@5.44.0) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-react@5.1.0(vite@5.4.21(@types/node@18.19.130)(sass@1.93.2)(terser@5.44.0))': + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) + '@rolldown/pluginutils': 1.0.0-beta.43 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 5.4.21(@types/node@18.19.130)(sass@1.93.2)(terser@5.44.0) + transitivePeerDependencies: + - supports-color + + '@whatwg-node/disposablestack@0.0.6': + dependencies: + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@whatwg-node/fetch@0.10.11': + dependencies: + '@whatwg-node/node-fetch': 0.8.1 + urlpattern-polyfill: 10.1.0 + + '@whatwg-node/node-fetch@0.8.1': + dependencies: + '@fastify/busboy': 3.2.0 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@whatwg-node/promise-helpers@1.3.2': + dependencies: + tslib: 2.8.1 + + '@wry/caches@1.0.1': + dependencies: + tslib: 2.8.1 + + '@wry/context@0.7.4': + dependencies: + tslib: 2.8.1 + + '@wry/equality@0.5.7': + dependencies: + tslib: 2.8.1 + + '@wry/trie@0.5.0': + dependencies: + tslib: 2.8.1 + + '@xmldom/xmldom@0.8.11': {} + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + aes-decrypter@3.1.3: + dependencies: + '@babel/runtime': 7.28.4 + '@videojs/vhs-utils': 3.0.5 + global: 4.4.0 + pkcs7: 1.0.4 + + agent-base@7.1.4: {} + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + apollo-upload-client@18.0.1(@apollo/client@3.14.0(@types/react@17.0.89)(graphql-ws@5.16.2(graphql@16.11.0))(graphql@16.11.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(graphql@16.11.0): + dependencies: + '@apollo/client': 3.14.0(@types/react@17.0.89)(graphql-ws@5.16.2(graphql@16.11.0))(graphql@16.11.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + extract-files: 13.0.0 + graphql: 16.11.0 + + arg@4.1.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array-union@2.1.0: {} + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + arrify@1.0.1: {} + + asap@2.0.6: {} + + ast-types-flow@0.0.8: {} + + astral-regex@2.0.0: {} + + async-function@1.0.0: {} + + at-least-node@1.0.0: {} + + auto-bind@4.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.11.0: {} + + axobject-query@4.1.0: {} + + b64-to-blob@1.2.19: {} + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.28.4 + cosmiconfig: 7.1.0 + resolve: 1.22.11 + + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.4): + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/core': 7.28.4 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.4): + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) + core-js-compat: 3.46.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.4): + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color + + babel-plugin-react-intl@7.9.4: + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/types': 7.28.4 + '@formatjs/ts-transformer': 2.13.0 + '@types/babel__core': 7.20.5 + '@types/fs-extra': 9.0.13 + '@types/schema-utils': 2.4.0 + fs-extra: 9.1.0 + intl-messageformat-parser: 5.5.1 + schema-utils: 2.7.1 + transitivePeerDependencies: + - supports-color + - ts-jest + + babel-plugin-syntax-trailing-function-commas@7.0.0-beta.0: {} + + babel-preset-fbjs@3.4.0(@babel/core@7.28.4): + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.28.4) + '@babel/plugin-proposal-object-rest-spread': 7.20.7(@babel/core@7.28.4) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.4) + '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-block-scoping': 7.28.4(@babel/core@7.28.4) + '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.28.4) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.4) + babel-plugin-syntax-trailing-function-commas: 7.0.0-beta.0 + transitivePeerDependencies: + - supports-color + + bail@1.0.5: {} + + balanced-match@1.0.2: {} + + balanced-match@2.0.0: {} + + base64-blob@1.4.1: + dependencies: + b64-to-blob: 1.2.19 + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.8.19: {} + + bcp-47-match@1.0.3: {} + + bcp-47-normalize@1.1.1: + dependencies: + bcp-47: 1.0.8 + bcp-47-match: 1.0.3 + + bcp-47@1.0.8: + dependencies: + is-alphabetical: 1.0.4 + is-alphanumerical: 1.0.4 + is-decimal: 1.0.4 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bootstrap@4.6.2(jquery@3.7.1)(popper.js@1.16.1): + dependencies: + jquery: 3.7.1 + popper.js: 1.16.1 + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist-to-esbuild@2.1.1(browserslist@4.26.3): + dependencies: + browserslist: 4.26.3 + meow: 13.2.0 + + browserslist@4.26.3: + dependencies: + baseline-browser-mapping: 2.8.19 + caniuse-lite: 1.0.30001751 + electron-to-chromium: 1.5.238 + node-releases: 2.0.26 + update-browserslist-db: 1.1.3(browserslist@4.26.3) + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.8.1 + + camelcase-keys@6.2.2: + dependencies: + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 + + camelcase-keys@7.0.2: + dependencies: + camelcase: 6.3.0 + map-obj: 4.3.0 + quick-lru: 5.1.1 + type-fest: 1.4.0 + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001751: {} + + capital-case@1.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + upper-case-first: 2.0.2 + + cardboard-vr-display@1.0.19: + dependencies: + gl-preserve-state: 1.0.0 + nosleep.js: 0.7.0 + webvr-polyfill-dpdb: 1.0.18 + + ccount@1.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + change-case-all@1.0.15: + dependencies: + change-case: 4.1.2 + is-lower-case: 2.0.2 + is-upper-case: 2.0.2 + lower-case: 2.0.2 + lower-case-first: 2.0.2 + sponge-case: 1.0.1 + swap-case: 2.0.2 + title-case: 3.0.3 + upper-case: 2.0.2 + upper-case-first: 2.0.2 + + change-case@4.1.2: + dependencies: + camel-case: 4.1.2 + capital-case: 1.0.4 + constant-case: 3.0.4 + dot-case: 3.0.4 + header-case: 2.0.4 + no-case: 3.0.4 + param-case: 3.0.4 + pascal-case: 3.1.2 + path-case: 3.0.4 + sentence-case: 3.0.4 + snake-case: 3.0.4 + tslib: 2.8.1 + + character-entities-legacy@1.1.4: {} + + character-entities@1.2.4: {} + + character-reference-invalid@1.1.4: {} + + chardet@2.1.0: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + classnames@2.5.1: {} + + clean-stack@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + + cli-width@3.0.0: {} + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + codem-isoboxer@0.3.9: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colord@2.9.3: {} + + colorette@2.0.20: {} + + comma-separated-tokens@1.0.8: {} + + commander@2.20.3: {} + + common-tags@1.8.2: {} + + concat-map@0.0.1: {} + + confusing-browser-globals@1.0.11: {} + + constant-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + upper-case: 2.0.2 + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie@0.4.2: {} + + core-js-compat@3.46.0: + dependencies: + browserslist: 4.26.3 + + core-js@3.46.0: {} + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + + cosmiconfig@8.3.6(typescript@4.8.4): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 4.8.4 + + create-require@1.1.1: {} + + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + cross-inspect@1.0.1: + dependencies: + tslib: 2.8.1 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-js@4.2.0: {} + + css-functions-list@3.2.3: {} + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + cssesc@3.0.0: {} + + csstype@3.1.3: {} + + damerau-levenshtein@1.0.8: {} + + dashjs@4.7.4: + dependencies: + bcp-47-match: 1.0.3 + bcp-47-normalize: 1.1.1 + codem-isoboxer: 0.3.9 + es6-promise: 4.2.8 + fast-deep-equal: 2.0.1 + html-entities: 1.4.0 + imsc: 1.1.5 + localforage: 1.10.0 + path-browserify: 1.0.1 + ua-parser-js: 1.0.41 + + data-uri-to-buffer@4.0.1: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + dataloader@2.2.3: {} + + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.28.4 + + debounce@1.2.1: {} + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize-keys@1.1.1: + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + + decamelize@1.2.0: {} + + decamelize@5.0.1: {} + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + deepmerge@2.2.1: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + dependency-graph@0.11.0: {} + + dequal@2.0.3: {} + + detect-indent@6.1.0: {} + + detect-libc@1.0.3: + optional: true + + diacritics@1.3.0: {} + + diff@4.0.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.28.4 + csstype: 3.1.3 + + dom-walk@0.1.2: {} + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + dotenv@16.6.1: {} + + dset@3.1.4: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.238: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + es6-promise@4.2.8: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1))(eslint@8.57.1): + dependencies: + confusing-browser-globals: 1.0.11 + eslint: 8.57.1 + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1) + object.assign: 4.1.7 + object.entries: 1.1.9 + semver: 6.3.1 + + eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1)(typescript@4.8.4))(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1))(eslint@8.57.1): + dependencies: + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1)(typescript@4.8.4) + '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.8.4) + eslint: 8.57.1 + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1) + + eslint-config-airbnb@19.0.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1))(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1) + eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) + eslint-plugin-react: 7.37.5(eslint@8.57.1) + eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) + object.assign: 4.1.7 + object.entries: 1.1.9 + + eslint-config-prettier@8.10.2(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.8.4) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint@8.57.1): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.8.4))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.8.4) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.11.0 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 8.57.1 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-react@7.37.5(eslint@8.57.1): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.1 + eslint: 8.57.1 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + event-target-polyfill@0.0.4: {} + + extend@3.0.2: {} + + extract-files@13.0.0: + dependencies: + is-plain-obj: 4.1.0 + + extract-react-intl-messages@4.1.1: + dependencies: + '@babel/core': 7.28.4 + babel-plugin-react-intl: 7.9.4 + flat: 5.0.2 + glob: 7.2.3 + js-yaml: 3.14.1 + load-json-file: 6.2.0 + lodash.merge: 4.6.2 + lodash.mergewith: 4.6.2 + lodash.pick: 4.4.0 + meow: 6.1.1 + mkdirp: 1.0.4 + pify: 5.0.0 + read-babelrc-up: 1.1.0 + sort-keys: 4.2.0 + write-json-file: 4.3.0 + transitivePeerDependencies: + - supports-color + - ts-jest + + fast-deep-equal@2.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.0: {} + + fastest-levenshtein@1.0.16: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fbjs-css-vars@1.0.2: {} + + fbjs@3.0.5: + dependencies: + cross-fetch: 3.2.0 + fbjs-css-vars: 1.0.2 + loose-envify: 1.4.0 + object-assign: 4.1.1 + promise: 7.3.1 + setimmediate: 1.0.5 + ua-parser-js: 1.0.41 + transitivePeerDependencies: + - encoding + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + fflate@0.6.10: {} + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + file-entry-cache@7.0.2: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-root@1.1.0: {} + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flag-icons@6.15.0: {} + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flat@5.0.2: {} + + flatted@3.3.3: {} + + flexbin@0.2.0: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + formik@2.4.6(@types/react@17.0.89)(react@17.0.2): + dependencies: + '@types/hoist-non-react-statics': 3.3.7(@types/react@17.0.89) + deepmerge: 2.2.1 + hoist-non-react-statics: 3.3.2 + lodash: 4.17.21 + lodash-es: 4.17.23 + react: 17.0.2 + react-fast-compare: 2.0.4 + tiny-warning: 1.0.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@types/react' + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + gl-preserve-state@1.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + global@4.4.0: + dependencies: + min-document: 2.19.0 + process: 0.11.10 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globjoin@0.1.4: {} + + globrex@0.1.2: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + graphql-config@5.1.5(@types/node@18.19.130)(graphql@16.11.0)(typescript@4.8.4): + dependencies: + '@graphql-tools/graphql-file-loader': 8.1.2(graphql@16.11.0) + '@graphql-tools/json-file-loader': 8.0.20(graphql@16.11.0) + '@graphql-tools/load': 8.1.2(graphql@16.11.0) + '@graphql-tools/merge': 9.1.1(graphql@16.11.0) + '@graphql-tools/url-loader': 8.0.33(@types/node@18.19.130)(graphql@16.11.0) + '@graphql-tools/utils': 10.9.1(graphql@16.11.0) + cosmiconfig: 8.3.6(typescript@4.8.4) + graphql: 16.11.0 + jiti: 2.6.1 + minimatch: 9.0.5 + string-env-interpolation: 1.0.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@fastify/websocket' + - '@types/node' + - bufferutil + - crossws + - supports-color + - typescript + - uWebSockets.js + - utf-8-validate + + graphql-request@6.1.0(graphql@16.11.0): + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.11.0) + cross-fetch: 3.2.0 + graphql: 16.11.0 + transitivePeerDependencies: + - encoding + + graphql-tag@2.12.6(graphql@16.11.0): + dependencies: + graphql: 16.11.0 + tslib: 2.8.1 + + graphql-ws@5.16.2(graphql@16.11.0): + dependencies: + graphql: 16.11.0 + + graphql-ws@6.0.6(graphql@16.11.0)(ws@8.18.3): + dependencies: + graphql: 16.11.0 + optionalDependencies: + ws: 8.18.3 + + graphql@16.11.0: {} + + hard-rejection@2.1.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-to-hyperscript@9.0.1: + dependencies: + '@types/unist': 2.0.11 + comma-separated-tokens: 1.0.8 + property-information: 5.6.0 + space-separated-tokens: 1.1.5 + style-to-object: 0.3.0 + unist-util-is: 4.1.0 + web-namespaces: 1.1.4 + + header-case@2.0.4: + dependencies: + capital-case: 1.0.4 + tslib: 2.8.1 + + history@4.10.1: + dependencies: + '@babel/runtime': 7.28.4 + loose-envify: 1.4.0 + resolve-pathname: 3.0.0 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + value-equal: 1.0.1 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + hosted-git-info@2.8.9: {} + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + html-entities@1.4.0: {} + + html-tags@3.3.1: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + i18n-iso-countries@7.14.0: + dependencies: + diacritics: 1.3.0 + + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + immediate@3.0.6: {} + + immutable@3.7.6: {} + + immutable@5.1.4: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-from@4.0.0: {} + + import-lazy@4.0.0: {} + + imsc@1.1.5: + dependencies: + sax: 1.2.1 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + indent-string@5.0.0: {} + + individual@2.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + inline-style-parser@0.1.1: {} + + inquirer@8.2.7(@types/node@18.19.130): + dependencies: + '@inquirer/external-editor': 1.0.2(@types/node@18.19.130) + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.2 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 6.2.0 + transitivePeerDependencies: + - '@types/node' + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + intl-messageformat-parser@5.5.1: + dependencies: + '@formatjs/intl-numberformat': 5.7.6 + + intl-messageformat-parser@6.1.2: + dependencies: + '@formatjs/ecma402-abstract': 1.5.0 + tslib: 2.8.1 + + intl-messageformat@10.7.7: + dependencies: + '@formatjs/ecma402-abstract': 2.2.4 + '@formatjs/fast-memoize': 2.2.3 + '@formatjs/icu-messageformat-parser': 2.9.4 + tslib: 2.8.1 + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + is-absolute@1.0.0: + dependencies: + is-relative: 1.0.0 + is-windows: 1.0.2 + + is-alphabetical@1.0.4: {} + + is-alphanumerical@1.0.4: + dependencies: + is-alphabetical: 1.0.4 + is-decimal: 1.0.4 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-buffer@2.0.5: {} + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-decimal@1.0.4: {} + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-function@1.0.2: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@1.0.4: {} + + is-interactive@1.0.0: {} + + is-lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-plain-obj@1.1.0: {} + + is-plain-obj@2.1.0: {} + + is-plain-obj@4.1.0: {} + + is-plain-object@5.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-relative@1.0.0: + dependencies: + is-unc-path: 1.0.0 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-typedarray@1.0.0: {} + + is-unc-path@1.0.0: + dependencies: + unc-path-regex: 0.1.2 + + is-unicode-supported@0.1.0: {} + + is-upper-case@2.0.2: + dependencies: + tslib: 2.8.1 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-windows@1.0.2: {} + + isarray@0.0.1: {} + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + isomorphic-ws@5.0.0(ws@8.18.3): + dependencies: + ws: 8.18.3 + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jiti@1.21.7: {} + + jiti@2.6.1: {} + + jose@5.10.0: {} + + jquery@3.7.1: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-to-pretty-yaml@1.2.2: + dependencies: + remedial: 1.0.8 + remove-trailing-spaces: 1.0.9 + + json2mq@0.2.0: + dependencies: + string-convert: 0.2.1 + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keycode@2.2.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kind-of@6.0.3: {} + + known-css-properties@0.29.0: {} + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lie@3.1.1: + dependencies: + immediate: 3.0.6 + + lil-gui@0.17.0: {} + + lines-and-columns@1.2.4: {} + + listr2@4.0.5: + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.20 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.4.1 + rxjs: 7.8.2 + through: 2.3.8 + wrap-ansi: 7.0.0 + + load-json-file@6.2.0: + dependencies: + graceful-fs: 4.2.11 + parse-json: 5.2.0 + strip-bom: 4.0.0 + type-fest: 0.6.0 + + localforage@1.10.0: + dependencies: + lie: 3.1.1 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.23: {} + + lodash.debounce@4.0.8: {} + + lodash.merge@4.6.2: {} + + lodash.mergewith@4.6.2: {} + + lodash.pick@4.4.0: {} + + lodash.sortby@4.7.0: {} + + lodash.truncate@4.4.2: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-update@4.0.0: + dependencies: + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + + longest-streak@2.0.4: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lower-case-first@2.0.2: + dependencies: + tslib: 2.8.1 + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + m3u8-parser@4.8.0: + dependencies: + '@babel/runtime': 7.28.4 + '@videojs/vhs-utils': 3.0.5 + global: 4.4.0 + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + make-error@1.3.6: {} + + map-cache@0.2.2: {} + + map-obj@1.0.1: {} + + map-obj@4.3.0: {} + + markdown-table@2.0.0: + dependencies: + repeat-string: 1.6.1 + + math-intrinsics@1.1.0: {} + + mathml-tag-names@2.1.3: {} + + mdast-util-definitions@4.0.0: + dependencies: + unist-util-visit: 2.0.3 + + mdast-util-find-and-replace@1.1.1: + dependencies: + escape-string-regexp: 4.0.0 + unist-util-is: 4.1.0 + unist-util-visit-parents: 3.1.1 + + mdast-util-from-markdown@0.8.5: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-string: 2.0.0 + micromark: 2.11.4 + parse-entities: 2.0.0 + unist-util-stringify-position: 2.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@0.1.3: + dependencies: + ccount: 1.1.0 + mdast-util-find-and-replace: 1.1.1 + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@0.2.3: + dependencies: + mdast-util-to-markdown: 0.6.5 + + mdast-util-gfm-table@0.1.6: + dependencies: + markdown-table: 2.0.0 + mdast-util-to-markdown: 0.6.5 + + mdast-util-gfm-task-list-item@0.1.6: + dependencies: + mdast-util-to-markdown: 0.6.5 + + mdast-util-gfm@0.1.2: + dependencies: + mdast-util-gfm-autolink-literal: 0.1.3 + mdast-util-gfm-strikethrough: 0.2.3 + mdast-util-gfm-table: 0.1.6 + mdast-util-gfm-task-list-item: 0.1.6 + mdast-util-to-markdown: 0.6.5 + transitivePeerDependencies: + - supports-color + + mdast-util-to-hast@10.2.0: + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + mdast-util-definitions: 4.0.0 + mdurl: 1.0.1 + unist-builder: 2.0.3 + unist-util-generated: 1.1.6 + unist-util-position: 3.1.0 + unist-util-visit: 2.0.3 + + mdast-util-to-markdown@0.6.5: + dependencies: + '@types/unist': 2.0.11 + longest-streak: 2.0.4 + mdast-util-to-string: 2.0.0 + parse-entities: 2.0.0 + repeat-string: 1.6.1 + zwitch: 1.0.5 + + mdast-util-to-string@2.0.0: {} + + mdn-data@2.0.30: {} + + mdurl@1.0.1: {} + + memoize-one@6.0.0: {} + + meow@10.1.5: + dependencies: + '@types/minimist': 1.2.5 + camelcase-keys: 7.0.2 + decamelize: 5.0.1 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 8.0.0 + redent: 4.0.0 + trim-newlines: 4.1.1 + type-fest: 1.4.0 + yargs-parser: 20.2.9 + + meow@13.2.0: {} + + meow@6.1.1: + dependencies: + '@types/minimist': 1.2.5 + camelcase-keys: 6.2.2 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 2.5.0 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.13.1 + yargs-parser: 18.1.3 + + merge2@1.4.1: {} + + meros@1.3.2(@types/node@18.19.130): + optionalDependencies: + '@types/node': 18.19.130 + + meshoptimizer@0.18.1: {} + + micromark-extension-gfm-autolink-literal@0.5.7: + dependencies: + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + + micromark-extension-gfm-strikethrough@0.6.5: + dependencies: + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + + micromark-extension-gfm-table@0.4.3: + dependencies: + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + + micromark-extension-gfm-tagfilter@0.3.0: {} + + micromark-extension-gfm-task-list-item@0.3.3: + dependencies: + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + + micromark-extension-gfm@0.3.3: + dependencies: + micromark: 2.11.4 + micromark-extension-gfm-autolink-literal: 0.5.7 + micromark-extension-gfm-strikethrough: 0.6.5 + micromark-extension-gfm-table: 0.4.3 + micromark-extension-gfm-tagfilter: 0.3.0 + micromark-extension-gfm-task-list-item: 0.3.3 + transitivePeerDependencies: + - supports-color + + micromark@2.11.4: + dependencies: + debug: 4.4.3 + parse-entities: 2.0.0 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-fn@2.1.0: {} + + min-document@2.19.0: + dependencies: + dom-walk: 0.1.2 + + min-indent@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist-options@4.1.0: + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + + minimist@1.2.8: {} + + mkdirp@1.0.4: {} + + moment@2.30.1: {} + + mousetrap-pause@1.0.0: {} + + mousetrap@1.6.5: {} + + mpd-parser@0.22.1: + dependencies: + '@babel/runtime': 7.28.4 + '@videojs/vhs-utils': 3.0.5 + '@xmldom/xmldom': 0.8.11 + global: 4.4.0 + + ms@2.1.3: {} + + mute-stream@0.0.8: {} + + mux.js@6.0.1: + dependencies: + '@babel/runtime': 7.28.4 + global: 4.4.0 + + nanoid@3.3.11: {} + + natural-compare-lite@1.4.0: {} + + natural-compare@1.4.0: {} + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + node-addon-api@7.1.1: + optional: true + + node-domexception@1.0.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-int64@0.4.0: {} + + node-releases@2.0.26: {} + + normalize-package-data@2.5.0: + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.11 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + + normalize-package-data@3.0.3: + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.16.1 + semver: 7.7.3 + validate-npm-package-license: 3.0.4 + + normalize-path@2.1.1: + dependencies: + remove-trailing-separator: 1.1.0 + + normalize-path@3.0.0: {} + + normalize-url@4.5.1: {} + + nosleep.js@0.7.0: {} + + nullthrows@1.1.1: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optimism@0.18.1: + dependencies: + '@wry/caches': 1.0.1 + '@wry/context': 0.7.4 + '@wry/trie': 0.5.0 + tslib: 2.8.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-try@2.2.0: {} + + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-entities@2.0.0: + dependencies: + character-entities: 1.2.4 + character-entities-legacy: 1.1.4 + character-reference-invalid: 1.1.4 + is-alphanumerical: 1.0.4 + is-decimal: 1.0.4 + is-hexadecimal: 1.0.4 + + parse-filepath@1.0.2: + dependencies: + is-absolute: 1.0.0 + map-cache: 0.2.2 + path-root: 0.1.1 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + path-browserify@1.0.1: {} + + path-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-root-regex@0.1.2: {} + + path-root@0.1.1: + dependencies: + path-root-regex: 0.1.2 + + path-to-regexp@1.9.0: + dependencies: + isarray: 0.0.1 + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + pify@5.0.0: {} + + pkcs7@1.0.4: + dependencies: + '@babel/runtime': 7.28.4 + + popper.js@1.16.1: {} + + possible-typed-array-names@1.1.0: {} + + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@6.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-scss@4.0.9(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-sorting@8.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@2.8.8: {} + + process@0.11.10: {} + + promise@7.3.1: + dependencies: + asap: 2.0.6 + + prop-types-extra@1.1.1(react@17.0.2): + dependencies: + react: 17.0.2 + react-is: 16.13.1 + warning: 4.0.3 + + prop-types@15.7.2: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + property-expr@2.0.6: {} + + property-information@5.6.0: + dependencies: + xtend: 4.0.2 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + quick-lru@4.0.1: {} + + quick-lru@5.1.1: {} + + react-bootstrap@1.6.8(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + dependencies: + '@babel/runtime': 7.28.4 + '@restart/context': 2.1.4(react@17.0.2) + '@restart/hooks': 0.4.16(react@17.0.2) + '@types/invariant': 2.2.37 + '@types/prop-types': 15.7.15 + '@types/react': 17.0.89 + '@types/react-transition-group': 4.4.12(@types/react@17.0.89) + '@types/warning': 3.0.3 + classnames: 2.5.1 + dom-helpers: 5.2.1 + invariant: 2.2.4 + prop-types: 15.8.1 + prop-types-extra: 1.1.1(react@17.0.2) + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + react-overlays: 5.2.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + react-transition-group: 4.4.5(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + uncontrollable: 7.2.1(react@17.0.2) + warning: 4.0.3 + + react-datepicker@4.25.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + dependencies: + '@popperjs/core': 2.11.8 + classnames: 2.5.1 + date-fns: 2.30.0 + prop-types: 15.8.1 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + react-onclickoutside: 6.13.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + + react-dom@17.0.2(react@17.0.2): + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react: 17.0.2 + scheduler: 0.20.2 + + react-fast-compare@2.0.4: {} + + react-fast-compare@3.2.2: {} + + react-helmet@6.1.0(react@17.0.2): + dependencies: + object-assign: 4.1.1 + prop-types: 15.8.1 + react: 17.0.2 + react-fast-compare: 3.2.2 + react-side-effect: 2.1.2(react@17.0.2) + + react-intl@6.8.9(react@17.0.2)(typescript@4.8.4): + dependencies: + '@formatjs/ecma402-abstract': 2.2.4 + '@formatjs/icu-messageformat-parser': 2.9.4 + '@formatjs/intl': 2.10.15(typescript@4.8.4) + '@formatjs/intl-displaynames': 6.8.5 + '@formatjs/intl-listformat': 7.7.5 + '@types/hoist-non-react-statics': 3.3.7(@types/react@17.0.89) + '@types/react': 17.0.89 + hoist-non-react-statics: 3.3.2 + intl-messageformat: 10.7.7 + react: 17.0.2 + tslib: 2.8.1 + optionalDependencies: + typescript: 4.8.4 + + react-is@16.13.1: {} + + react-lifecycles-compat@3.0.4: {} + + react-onclickoutside@6.13.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + dependencies: + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + + react-overlays@5.2.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + dependencies: + '@babel/runtime': 7.28.4 + '@popperjs/core': 2.11.8 + '@restart/hooks': 0.4.16(react@17.0.2) + '@types/warning': 3.0.3 + dom-helpers: 5.2.1 + prop-types: 15.8.1 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + uncontrollable: 7.2.1(react@17.0.2) + warning: 4.0.3 + + react-photo-gallery@8.0.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + dependencies: + prop-types: 15.7.2 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + resize-observer-polyfill: 1.5.1 + + react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + dependencies: + '@popperjs/core': 2.11.8 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + react-fast-compare: 3.2.2 + warning: 4.0.3 + + react-refresh@0.18.0: {} + + react-remark@2.1.0(react@17.0.2): + dependencies: + react: 17.0.2 + rehype-react: 6.2.1 + remark-parse: 9.0.0 + remark-rehype: 8.1.0 + unified: 9.2.2 + transitivePeerDependencies: + - supports-color + + react-router-bootstrap@0.25.0(react-router-dom@5.3.4(react@17.0.2))(react@17.0.2): + dependencies: + prop-types: 15.8.1 + react: 17.0.2 + react-router-dom: 5.3.4(react@17.0.2) + + react-router-dom@5.3.4(react@17.0.2): + dependencies: + '@babel/runtime': 7.28.4 + history: 4.10.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 17.0.2 + react-router: 5.3.4(react@17.0.2) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + react-router-hash-link@2.4.3(react-router-dom@5.3.4(react@17.0.2))(react@17.0.2): + dependencies: + prop-types: 15.8.1 + react: 17.0.2 + react-router-dom: 5.3.4(react@17.0.2) + + react-router@5.3.4(react@17.0.2): + dependencies: + '@babel/runtime': 7.28.4 + history: 4.10.1 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + path-to-regexp: 1.9.0 + prop-types: 15.8.1 + react: 17.0.2 + react-is: 16.13.1 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + react-select@5.10.2(@types/react@17.0.89)(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + dependencies: + '@babel/runtime': 7.28.4 + '@emotion/cache': 11.14.0 + '@emotion/react': 11.14.0(@types/react@17.0.89)(react@17.0.2) + '@floating-ui/dom': 1.7.4 + '@types/react-transition-group': 4.4.12(@types/react@17.0.89) + memoize-one: 6.0.0 + prop-types: 15.8.1 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + react-transition-group: 4.4.5(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + use-isomorphic-layout-effect: 1.2.1(@types/react@17.0.89)(react@17.0.2) + transitivePeerDependencies: + - '@types/react' + - supports-color + + react-side-effect@2.1.2(react@17.0.2): + dependencies: + react: 17.0.2 + + react-transition-group@4.4.5(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + dependencies: + '@babel/runtime': 7.28.4 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + + react@17.0.2: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + + read-babelrc-up@1.1.0: + dependencies: + find-up: 4.1.0 + json5: 2.2.3 + + read-pkg-up@7.0.1: + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + + read-pkg-up@8.0.0: + dependencies: + find-up: 5.0.0 + read-pkg: 6.0.0 + type-fest: 1.4.0 + + read-pkg@5.2.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + + read-pkg@6.0.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 3.0.3 + parse-json: 5.2.0 + type-fest: 1.4.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + redent@4.0.0: + dependencies: + indent-string: 5.0.0 + strip-indent: 4.1.1 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regenerate-unicode-properties@10.2.2: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regenerator-runtime@0.14.1: {} + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + regexpu-core@6.4.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.2 + regjsgen: 0.8.0 + regjsparser: 0.13.0 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.1 + + regjsgen@0.8.0: {} + + regjsparser@0.13.0: + dependencies: + jsesc: 3.1.0 + + rehackt@0.1.0(@types/react@17.0.89)(react@17.0.2): + optionalDependencies: + '@types/react': 17.0.89 + react: 17.0.2 + + rehype-react@6.2.1: + dependencies: + '@mapbox/hast-util-table-cell-style': 0.2.1 + hast-to-hyperscript: 9.0.1 + + relay-runtime@12.0.0: + dependencies: + '@babel/runtime': 7.28.4 + fbjs: 3.0.5 + invariant: 2.2.4 + transitivePeerDependencies: + - encoding + + remark-gfm@1.0.0: + dependencies: + mdast-util-gfm: 0.1.2 + micromark-extension-gfm: 0.3.3 + transitivePeerDependencies: + - supports-color + + remark-parse@9.0.0: + dependencies: + mdast-util-from-markdown: 0.8.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@8.1.0: + dependencies: + mdast-util-to-hast: 10.2.0 + + remedial@1.0.8: {} + + remove-trailing-separator@1.1.0: {} + + remove-trailing-spaces@1.0.9: {} + + repeat-string@1.6.1: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-main-filename@2.0.0: {} + + resize-observer-polyfill@1.5.1: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pathname@3.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.53.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.1 + '@rollup/rollup-android-arm64': 4.53.1 + '@rollup/rollup-darwin-arm64': 4.53.1 + '@rollup/rollup-darwin-x64': 4.53.1 + '@rollup/rollup-freebsd-arm64': 4.53.1 + '@rollup/rollup-freebsd-x64': 4.53.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.1 + '@rollup/rollup-linux-arm-musleabihf': 4.53.1 + '@rollup/rollup-linux-arm64-gnu': 4.53.1 + '@rollup/rollup-linux-arm64-musl': 4.53.1 + '@rollup/rollup-linux-loong64-gnu': 4.53.1 + '@rollup/rollup-linux-ppc64-gnu': 4.53.1 + '@rollup/rollup-linux-riscv64-gnu': 4.53.1 + '@rollup/rollup-linux-riscv64-musl': 4.53.1 + '@rollup/rollup-linux-s390x-gnu': 4.53.1 + '@rollup/rollup-linux-x64-gnu': 4.53.1 + '@rollup/rollup-linux-x64-musl': 4.53.1 + '@rollup/rollup-openharmony-arm64': 4.53.1 + '@rollup/rollup-win32-arm64-msvc': 4.53.1 + '@rollup/rollup-win32-ia32-msvc': 4.53.1 + '@rollup/rollup-win32-x64-gnu': 4.53.1 + '@rollup/rollup-win32-x64-msvc': 4.53.1 + fsevents: 2.3.3 + + run-async@2.4.1: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rust-result@1.0.0: + dependencies: + individual: 2.0.0 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-json-parse@4.0.0: + dependencies: + rust-result: 1.0.0 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + sass@1.93.2: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.4 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + sax@1.2.1: {} + + scheduler@0.20.2: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + + schema-utils@2.7.1: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + scuid@1.1.0: {} + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.7.3: {} + + sentence-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + upper-case-first: 2.0.2 + + set-blocking@2.0.0: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + setimmediate@1.0.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + signedsource@1.0.0: {} + + slash@3.0.0: {} + + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slick-carousel@1.8.1(jquery@3.7.1): + dependencies: + jquery: 3.7.1 + + snake-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + sort-keys@4.2.0: + dependencies: + is-plain-obj: 2.1.0 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + space-separated-tokens@1.1.5: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.22 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.22 + + spdx-license-ids@3.0.22: {} + + sponge-case@1.0.1: + dependencies: + tslib: 2.8.1 + + sprintf-js@1.0.3: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-convert@0.2.1: {} + + string-env-interpolation@1.0.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.0 + + string.prototype.replaceall@1.0.11: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-regex: 1.2.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-indent@4.1.1: {} + + strip-json-comments@3.1.1: {} + + style-search@0.1.0: {} + + style-to-object@0.3.0: + dependencies: + inline-style-parser: 0.1.1 + + stylelint-order@6.0.4(stylelint@15.11.0(typescript@4.8.4)): + dependencies: + postcss: 8.5.6 + postcss-sorting: 8.0.2(postcss@8.5.6) + stylelint: 15.11.0(typescript@4.8.4) + + stylelint@15.11.0(typescript@4.8.4): + dependencies: + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/media-query-list-parser': 2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.1.2) + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 8.3.6(typescript@4.8.4) + css-functions-list: 3.2.3 + css-tree: 2.3.1 + debug: 4.4.3 + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 7.0.2 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 5.3.2 + import-lazy: 4.0.0 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.29.0 + mathml-tag-names: 2.1.3 + meow: 10.1.5 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 6.0.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + style-search: 0.1.0 + supports-hyperlinks: 3.2.0 + svg-tags: 1.0.0 + table: 6.9.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + stylis@4.2.0: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svg-tags@1.0.0: {} + + swap-case@2.0.2: + dependencies: + tslib: 2.8.1 + + symbol-observable@4.0.0: {} + + sync-fetch@0.6.0-2: + dependencies: + node-fetch: 3.3.2 + timeout-signal: 2.0.0 + whatwg-mimetype: 4.0.0 + + systemjs@6.15.1: {} + + table@6.9.0: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + terser@5.44.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + text-table@0.2.0: {} + + thehandy@1.1.0: {} + + three@0.93.0: {} + + throttle-debounce@5.0.2: {} + + through@2.3.8: {} + + timeout-signal@2.0.0: {} + + tiny-case@1.0.3: {} + + tiny-invariant@1.3.3: {} + + tiny-warning@1.0.3: {} + + title-case@3.0.3: + dependencies: + tslib: 2.8.1 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toposort@2.0.2: {} + + tr46@0.0.3: {} + + trim-newlines@3.0.1: {} + + trim-newlines@4.1.1: {} + + trough@1.0.5: {} + + ts-invariant@0.10.3: + dependencies: + tslib: 2.8.1 + + ts-log@2.2.7: {} + + ts-node@10.9.2(@types/node@18.19.130)(typescript@4.8.4): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.19.130 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.8.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tsconfck@3.1.6(typescript@4.8.4): + optionalDependencies: + typescript: 4.8.4 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@1.14.1: {} + + tslib@2.4.1: {} + + tslib@2.6.3: {} + + tslib@2.8.1: {} + + tsutils@3.21.0(typescript@4.8.4): + dependencies: + tslib: 1.14.1 + typescript: 4.8.4 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.13.1: {} + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-fest@0.6.0: {} + + type-fest@0.8.1: {} + + type-fest@1.4.0: {} + + type-fest@2.19.0: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typedarray-to-buffer@3.1.5: + dependencies: + is-typedarray: 1.0.0 + + typescript@4.8.4: {} + + ua-parser-js@1.0.41: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + unc-path-regex@0.1.2: {} + + uncontrollable@7.2.1(react@17.0.2): + dependencies: + '@babel/runtime': 7.28.4 + '@types/react': 17.0.89 + invariant: 2.2.4 + react: 17.0.2 + react-lifecycles-compat: 3.0.4 + + undici-types@5.26.5: {} + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.2.0 + + unicode-match-property-value-ecmascript@2.2.1: {} + + unicode-property-aliases-ecmascript@2.2.0: {} + + unified@9.2.2: + dependencies: + '@types/unist': 2.0.11 + bail: 1.0.5 + extend: 3.0.2 + is-buffer: 2.0.5 + is-plain-obj: 2.1.0 + trough: 1.0.5 + vfile: 4.2.1 + + unist-builder@2.0.3: {} + + unist-util-generated@1.1.6: {} + + unist-util-is@3.0.0: {} + + unist-util-is@4.1.0: {} + + unist-util-position@3.1.0: {} + + unist-util-stringify-position@2.0.3: + dependencies: + '@types/unist': 2.0.11 + + unist-util-visit-parents@2.1.2: + dependencies: + unist-util-is: 3.0.0 + + unist-util-visit-parents@3.1.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + + unist-util-visit@1.4.1: + dependencies: + unist-util-visit-parents: 2.1.2 + + unist-util-visit@2.0.3: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + unist-util-visit-parents: 3.1.1 + + universal-cookie@4.0.4: + dependencies: + '@types/cookie': 0.3.3 + cookie: 0.4.2 + + universalify@2.0.1: {} + + unixify@1.0.0: + dependencies: + normalize-path: 2.1.1 + + update-browserslist-db@1.1.3(browserslist@4.26.3): + dependencies: + browserslist: 4.26.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + upper-case-first@2.0.2: + dependencies: + tslib: 2.8.1 + + upper-case@2.0.2: + dependencies: + tslib: 2.8.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-toolkit@2.2.5: {} + + urlpattern-polyfill@10.1.0: {} + + use-isomorphic-layout-effect@1.2.1(@types/react@17.0.89)(react@17.0.2): + dependencies: + react: 17.0.2 + optionalDependencies: + '@types/react': 17.0.89 + + util-deprecate@1.0.2: {} + + v8-compile-cache-lib@3.0.1: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + value-equal@1.0.1: {} + + vfile-message@2.0.4: + dependencies: + '@types/unist': 2.0.11 + unist-util-stringify-position: 2.0.3 + + vfile@4.2.1: + dependencies: + '@types/unist': 2.0.11 + is-buffer: 2.0.5 + unist-util-stringify-position: 2.0.3 + vfile-message: 2.0.4 + + video.js@7.21.7: + dependencies: + '@babel/runtime': 7.28.4 + '@videojs/http-streaming': 2.16.3(video.js@7.21.7) + '@videojs/vhs-utils': 3.0.5 + '@videojs/xhr': 2.6.0 + aes-decrypter: 3.1.3 + global: 4.4.0 + keycode: 2.2.1 + m3u8-parser: 4.8.0 + mpd-parser: 0.22.1 + mux.js: 6.0.1 + safe-json-parse: 4.0.0 + videojs-font: 3.2.0 + videojs-vtt.js: 0.15.5 + + videojs-abloop@1.2.0: {} + + videojs-contrib-dash@5.1.1: + dependencies: + dashjs: 4.7.4 + global: 4.4.0 + video.js: 7.21.7 + + videojs-font@3.2.0: {} + + videojs-mobile-ui@0.8.0(video.js@7.21.7): + dependencies: + global: 4.4.0 + video.js: 7.21.7 + + videojs-seek-buttons@3.0.1(video.js@7.21.7): + dependencies: + global: 4.4.0 + video.js: 7.21.7 + + videojs-vr@1.8.0: + dependencies: + '@babel/runtime': 7.28.4 + global: 4.4.0 + three: 0.93.0 + video.js: 7.21.7 + webvr-polyfill: 0.10.12 + + videojs-vtt.js@0.15.5: + dependencies: + global: 4.4.0 + + vite-plugin-compression@0.5.1(vite@5.4.21(@types/node@18.19.130)(sass@1.93.2)(terser@5.44.0)): + dependencies: + chalk: 4.1.2 + debug: 4.4.3 + fs-extra: 10.1.0 + vite: 5.4.21(@types/node@18.19.130)(sass@1.93.2)(terser@5.44.0) + transitivePeerDependencies: + - supports-color + + vite-tsconfig-paths@4.3.2(typescript@4.8.4)(vite@5.4.21(@types/node@18.19.130)(sass@1.93.2)(terser@5.44.0)): + dependencies: + debug: 4.4.3 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@4.8.4) + optionalDependencies: + vite: 5.4.21(@types/node@18.19.130)(sass@1.93.2)(terser@5.44.0) + transitivePeerDependencies: + - supports-color + - typescript + + vite@5.4.21(@types/node@18.19.130)(sass@1.93.2)(terser@5.44.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.53.1 + optionalDependencies: + '@types/node': 18.19.130 + fsevents: 2.3.3 + sass: 1.93.2 + terser: 5.44.0 + + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + web-namespaces@1.1.4: {} + + web-streams-polyfill@3.3.3: {} + + webcomponents.js@https://codeload.github.com/webcomponents/webcomponentsjs/tar.gz/8a2e40557b177e2cca0def2553f84c8269c8f93e: {} + + webidl-conversions@3.0.1: {} + + webvr-polyfill-dpdb@1.0.18: {} + + webvr-polyfill@0.10.12: + dependencies: + cardboard-vr-display: 1.0.19 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-module@2.0.1: {} + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + write-file-atomic@3.0.3: + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + write-json-file@4.3.0: + dependencies: + detect-indent: 6.1.0 + graceful-fs: 4.2.11 + is-plain-obj: 2.1.0 + make-dir: 3.1.0 + sort-keys: 4.2.0 + write-file-atomic: 3.0.3 + + ws@8.18.3: {} + + xtend@4.0.2: {} + + y18n@4.0.3: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yaml-ast-parser@0.0.43: {} + + yaml@1.10.2: {} + + yaml@2.8.1: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@20.2.9: {} + + yargs-parser@21.1.1: {} + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + yup@1.7.1: + dependencies: + property-expr: 2.0.6 + tiny-case: 1.0.3 + toposort: 2.0.2 + type-fest: 2.19.0 + + zen-observable-ts@1.2.5: + dependencies: + zen-observable: 0.8.15 + + zen-observable@0.8.15: {} + + zwitch@1.0.5: {} diff --git a/ui/v2.5/pnpm-workspace.yaml b/ui/v2.5/pnpm-workspace.yaml new file mode 100644 index 000000000..2b12183a9 --- /dev/null +++ b/ui/v2.5/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +onlyBuiltDependencies: + - '@parcel/watcher' + - core-js + - esbuild diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index 005d101aa..d08274b18 100644 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -31,7 +31,10 @@ import * as GQL from "./core/generated-graphql"; import { makeTitleProps } from "./hooks/title"; import { LoadingIndicator } from "./components/Shared/LoadingIndicator"; -import { ConfigurationProvider } from "./hooks/Config"; +import { + ConfigurationProvider, + useConfigurationContextOptional, +} from "./hooks/Config"; import { ManualProvider } from "./components/Help/context"; import { InteractiveProvider } from "./hooks/Interactive/context"; import { ReleaseNotesDialog } from "./components/Dialogs/ReleaseNotesDialog"; @@ -46,10 +49,12 @@ import { PluginRoutes, PluginsLoader } from "./plugins"; // import plugin_api to run code import "./pluginApi"; import { ConnectionMonitor } from "./ConnectionMonitor"; +import { TroubleshootingModeOverlay } from "./components/TroubleshootingMode/TroubleshootingModeOverlay"; import { PatchFunction } from "./patch"; import moment from "moment/min/moment-with-locales"; import { ErrorMessage } from "./components/Shared/ErrorMessage"; +import cx from "classnames"; const Performers = lazyComponent( () => import("./components/Performers/Performers") @@ -104,8 +109,17 @@ const AppContainer: React.FC> = PatchFunction( ) as React.FC; const MainContainer: React.FC = ({ children }) => { + // use optional here because the configuration may have be loading or errored + const { configuration } = useConfigurationContextOptional() || {}; + const { sfwContentMode } = configuration?.interface || {}; + return ( -
+
{children}
); @@ -294,34 +308,43 @@ export const App: React.FC = () => { ); } - const titleProps = makeTitleProps(); + const title = config.data?.configuration.ui.title || "Stash"; + const titleProps = makeTitleProps(title); if (!messages) { return null; } - if (config.error) { + function renderSimple(content: React.ReactNode) { return ( - - - } - error={config.error.message} - /> - + {content} ); } + if (config.loading) { + return renderSimple(); + } + + if (config.error) { + return renderSimple( + + } + error={config.error.message} + /> + ); + } + return ( { formats={intlFormats} > - + - + {maybeRenderReleaseNotes()} + }> diff --git a/ui/v2.5/src/components/Changelog/Changelog.tsx b/ui/v2.5/src/components/Changelog/Changelog.tsx index ae7937588..97175e1c2 100644 --- a/ui/v2.5/src/components/Changelog/Changelog.tsx +++ b/ui/v2.5/src/components/Changelog/Changelog.tsx @@ -33,7 +33,13 @@ import V0250 from "src/docs/en/Changelog/v0250.md"; import V0260 from "src/docs/en/Changelog/v0260.md"; import V0270 from "src/docs/en/Changelog/v0270.md"; import V0280 from "src/docs/en/Changelog/v0280.md"; +import V0290 from "src/docs/en/Changelog/v0290.md"; +import V0300 from "src/docs/en/Changelog/v0300.md"; + +import V0290ReleaseNotes from "src/docs/en/ReleaseNotes/v0290.md"; + import { MarkdownPage } from "../Shared/MarkdownPage"; +import { FormattedMessage } from "react-intl"; const Changelog: React.FC = () => { const [{ data, loading }, setOpenState] = useChangelogStorage(); @@ -63,14 +69,15 @@ const Changelog: React.FC = () => { date?: string; page: string; defaultOpen?: boolean; + releaseNotes?: string; } // after new release: // add entry to releases, using the current* fields // then update the current fields. - const currentVersion = stashVersion || "v0.28.1"; + const currentVersion = stashVersion || "v0.30.0"; const currentDate = buildDate; - const currentPage = V0280; + const currentPage = V0300; const releases: IStashRelease[] = [ { @@ -79,6 +86,17 @@ const Changelog: React.FC = () => { page: currentPage, defaultOpen: true, }, + { + version: "v0.29.3", + date: "2025-11-06", + page: V0290, + releaseNotes: V0290ReleaseNotes, + }, + { + version: "v0.28.1", + date: "2025-03-20", + page: V0280, + }, { version: "v0.27.2", date: "2024-10-16", @@ -248,6 +266,15 @@ const Changelog: React.FC = () => { setOpenState={setVersionOpenState} defaultOpen={r.defaultOpen} > + {r.releaseNotes && ( +
+

+ +

+ +
+
+ )} ))} diff --git a/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx index 669bc8aa4..a5688aed0 100644 --- a/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx @@ -6,7 +6,7 @@ import { Icon } from "src/components/Shared/Icon"; import { useToast } from "src/hooks/Toast"; import * as GQL from "src/core/generated-graphql"; import { FormattedMessage, useIntl } from "react-intl"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { Manual } from "../Help/Manual"; import { withoutTypename } from "src/utils/data"; import { GenerateOptions } from "../Settings/Tasks/GenerateOptions"; @@ -14,18 +14,22 @@ import { SettingSection } from "../Settings/SettingSection"; import { faCogs, faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; import { SettingsContext } from "../Settings/context"; -interface ISceneGenerateDialog { +interface IGenerateDialog { selectedIds?: string[]; onClose: () => void; - type: "scene"; // TODO - add image generate + type: "scene" | "image" | "gallery"; } -export const GenerateDialog: React.FC = ({ +export const GenerateDialog: React.FC = ({ selectedIds, onClose, type, }) => { - const { configuration } = React.useContext(ConfigurationContext); + const sceneIDs = type === "scene" ? selectedIds : undefined; + const imageIDs = type === "image" ? selectedIds : undefined; + const galleryIDs = type === "gallery" ? selectedIds : undefined; + + const { configuration } = useConfigurationContext(); function getDefaultOptions(): GQL.GenerateMetadataInput { return { @@ -89,6 +93,13 @@ export const GenerateDialog: React.FC = ({ }, [configuration, configRead]); const selectionStatus = useMemo(() => { + const countableIds: Record = { + scene: "countables.scenes", + image: "countables.images", + gallery: "countables.galleries", + }; + const countableId = countableIds[type]; + if (selectedIds) { return ( @@ -98,7 +109,7 @@ export const GenerateDialog: React.FC = ({ num: selectedIds.length, scene: intl.formatMessage( { - id: "countables.scenes", + id: countableId, }, { count: selectedIds.length, @@ -118,7 +129,7 @@ export const GenerateDialog: React.FC = ({ num: intl.formatMessage({ id: "all" }), scene: intl.formatMessage( { - id: "countables.scenes", + id: countableId, }, { count: 0, @@ -135,13 +146,15 @@ export const GenerateDialog: React.FC = ({
{message}
); - }, [selectedIds, intl]); + }, [selectedIds, intl, type]); async function onGenerate() { try { await mutateMetadataGenerate({ ...options, - sceneIDs: selectedIds, + sceneIDs, + imageIDs, + galleryIDs, }); Toast.success( intl.formatMessage( diff --git a/ui/v2.5/src/components/FrontPage/Control.tsx b/ui/v2.5/src/components/FrontPage/Control.tsx index 7ce32cb44..72f84516f 100644 --- a/ui/v2.5/src/components/FrontPage/Control.tsx +++ b/ui/v2.5/src/components/FrontPage/Control.tsx @@ -1,9 +1,9 @@ -import React, { useContext, useMemo } from "react"; +import React, { useMemo } from "react"; import { useIntl } from "react-intl"; import { FrontPageContent, ICustomFilter } from "src/core/config"; import * as GQL from "src/core/generated-graphql"; import { useFindSavedFilter } from "src/core/StashService"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ListFilterModel } from "src/models/list-filter/filter"; import { GalleryRecommendationRow } from "../Galleries/GalleryRecommendationRow"; import { ImageRecommendationRow } from "../Images/ImageRecommendationRow"; @@ -12,6 +12,7 @@ import { PerformerRecommendationRow } from "../Performers/PerformerRecommendatio import { SceneRecommendationRow } from "../Scenes/SceneRecommendationRow"; import { StudioRecommendationRow } from "../Studios/StudioRecommendationRow"; import { TagRecommendationRow } from "../Tags/TagRecommendationRow"; +import { SceneMarkerRecommendationRow } from "../Scenes/SceneMarkerRecommendationRow"; interface IFilter { mode: GQL.FilterMode; @@ -84,6 +85,14 @@ const RecommendationRow: React.FC = ({ mode, filter, header }) => { header={header} /> ); + case GQL.FilterMode.SceneMarkers: + return ( + + ); default: return <>; } @@ -96,7 +105,7 @@ interface ISavedFilterResults { const SavedFilterResults: React.FC = ({ savedFilterID, }) => { - const { configuration: config } = useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const { loading, data } = useFindSavedFilter(savedFilterID.toString()); const filter = useMemo(() => { @@ -127,7 +136,7 @@ interface ICustomFilterProps { const CustomFilterResults: React.FC = ({ customFilter, }) => { - const { configuration: config } = useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const intl = useIntl(); const filter = useMemo(() => { diff --git a/ui/v2.5/src/components/FrontPage/FrontPage.tsx b/ui/v2.5/src/components/FrontPage/FrontPage.tsx index 89b4db468..12e56f6ab 100644 --- a/ui/v2.5/src/components/FrontPage/FrontPage.tsx +++ b/ui/v2.5/src/components/FrontPage/FrontPage.tsx @@ -6,7 +6,7 @@ import { Button } from "react-bootstrap"; import { FrontPageConfig } from "./FrontPageConfig"; import { useToast } from "src/hooks/Toast"; import { Control } from "./Control"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { FrontPageContent, generateDefaultFrontPageContent, @@ -24,7 +24,7 @@ const FrontPage: React.FC = PatchComponent("FrontPage", () => { const [saveUI] = useConfigureUI(); - const { configuration, loading } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); useScrollToTopOnMount(); @@ -51,7 +51,7 @@ const FrontPage: React.FC = PatchComponent("FrontPage", () => { setSaving(false); } - if (loading || saving) { + if (saving) { return ; } diff --git a/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx b/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx index f175a060e..33e6c066a 100644 --- a/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx +++ b/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx @@ -4,7 +4,7 @@ import { useFindSavedFilters } from "src/core/StashService"; import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { Button, Form, Modal } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ISavedFilterRow, ICustomFilter, @@ -93,11 +93,7 @@ const AddContentModal: React.FC = ({ ].concat( candidates.findSavedFilters .filter((f) => { - // markers not currently supported - return ( - f.mode !== GQL.FilterMode.SceneMarkers && - !existingSavedFilterIDs.includes(f.id) - ); + return !existingSavedFilterIDs.includes(f.id); }) .map((f) => { return { @@ -281,11 +277,11 @@ interface IFrontPageConfigProps { export const FrontPageConfig: React.FC = ({ onClose, }) => { - const { configuration, loading } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const ui = configuration?.ui; - const { data: allFilters, loading: loading2 } = useFindSavedFilters(); + const { data: allFilters, loading } = useFindSavedFilters(); const [isAdd, setIsAdd] = useState(false); const [currentContent, setCurrentContent] = useState([]); @@ -342,7 +338,7 @@ export const FrontPageConfig: React.FC = ({ setDragIndex(undefined); } - if (loading || loading2) { + if (loading) { return ; } diff --git a/ui/v2.5/src/components/FrontPage/RecommendationRow.tsx b/ui/v2.5/src/components/FrontPage/RecommendationRow.tsx index 0b48434c0..115d8642a 100644 --- a/ui/v2.5/src/components/FrontPage/RecommendationRow.tsx +++ b/ui/v2.5/src/components/FrontPage/RecommendationRow.tsx @@ -1,4 +1,5 @@ import React, { PropsWithChildren } from "react"; +import { PatchComponent } from "src/patch"; interface IProps { className?: string; @@ -6,19 +7,18 @@ interface IProps { link: JSX.Element; } -export const RecommendationRow: React.FC> = ({ - className, - header, - link, - children, -}) => ( -
-
-
-

{header}

+export const RecommendationRow: React.FC> = + PatchComponent( + "RecommendationRow", + ({ className, header, link, children }) => ( +
+
+
+

{header}

+
+ {link} +
+ {children}
- {link} -
- {children} -
-); + ) + ); diff --git a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx index 2feaa0f1e..35aaea797 100644 --- a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx +++ b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx @@ -4,7 +4,7 @@ import { useGalleryDestroy } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; import { ModalComponent } from "../Shared/Modal"; import { useToast } from "src/hooks/Toast"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { FormattedMessage, useIntl } from "react-intl"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; @@ -33,7 +33,7 @@ export const DeleteGalleriesDialog: React.FC = ( { count: props.selected.length, singularEntity, pluralEntity } ); - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const [deleteFile, setDeleteFile] = useState( config?.defaults.deleteFile ?? false @@ -84,6 +84,11 @@ export const DeleteGalleriesDialog: React.FC = ( return; } + const deleteTrashPath = config?.general.deleteTrashPath; + const deleteAlertId = deleteTrashPath + ? "dialogs.delete_alert_to_trash" + : "dialogs.delete_alert"; + return (

@@ -93,7 +98,7 @@ export const DeleteGalleriesDialog: React.FC = ( singularEntity: intl.formatMessage({ id: "file" }), pluralEntity: intl.formatMessage({ id: "files" }), }} - id="dialogs.delete_alert" + id={deleteAlertId} />

    diff --git a/ui/v2.5/src/components/Galleries/Galleries.tsx b/ui/v2.5/src/components/Galleries/Galleries.tsx index c845a153c..388ce6720 100644 --- a/ui/v2.5/src/components/Galleries/Galleries.tsx +++ b/ui/v2.5/src/components/Galleries/Galleries.tsx @@ -4,7 +4,7 @@ import { Helmet } from "react-helmet"; import { useTitleProps } from "src/hooks/title"; import Gallery from "./GalleryDetails/Gallery"; import GalleryCreate from "./GalleryDetails/GalleryCreate"; -import { GalleryList } from "./GalleryList"; +import { FilteredGalleryList } from "./GalleryList"; import { View } from "../List/views"; import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { ErrorMessage } from "../Shared/ErrorMessage"; @@ -40,7 +40,7 @@ const GalleryImage: React.FC> = ({ }; const Galleries: React.FC = () => { - return ; + return ; }; const GalleryRoutes: React.FC = () => { diff --git a/ui/v2.5/src/components/Galleries/GalleryCardGrid.tsx b/ui/v2.5/src/components/Galleries/GalleryCardGrid.tsx new file mode 100644 index 000000000..a249f27f7 --- /dev/null +++ b/ui/v2.5/src/components/Galleries/GalleryCardGrid.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import * as GQL from "src/core/generated-graphql"; +import { GalleryCard } from "./GalleryCard"; +import { + useCardWidth, + useContainerDimensions, +} from "../Shared/GridCard/GridCard"; +import { PatchComponent } from "src/patch"; + +interface IGalleryCardGrid { + galleries: GQL.SlimGalleryDataFragment[]; + selectedIds: Set; + zoomIndex: number; + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; +} + +const zoomWidths = [280, 340, 480, 640]; + +export const GalleryCardGrid: React.FC = PatchComponent( + "GalleryCardGrid", + ({ galleries, selectedIds, zoomIndex, onSelectChange }) => { + const [componentRef, { width: containerWidth }] = useContainerDimensions(); + const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); + + return ( +
    + {galleries.map((gallery) => ( + 0} + selected={selectedIds.has(gallery.id)} + onSelectedChanged={(selected: boolean, shiftKey: boolean) => + onSelectChange(gallery.id, selected, shiftKey) + } + /> + ))} +
    + ); + } +); diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx index 20023904b..18cbeff96 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx @@ -1,12 +1,12 @@ import { Button, Tab, Nav, Dropdown } from "react-bootstrap"; -import React, { useContext, useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { useHistory, Link, RouteComponentProps, Redirect, } from "react-router-dom"; -import { FormattedDate, FormattedMessage, useIntl } from "react-intl"; +import { FormattedMessage, useIntl } from "react-intl"; import { Helmet } from "react-helmet"; import * as GQL from "src/core/generated-graphql"; import { @@ -15,6 +15,11 @@ import { useFindGallery, useGalleryUpdate, } from "src/core/StashService"; +import { lazyComponent } from "src/utils/lazyComponent"; + +const GenerateDialog = lazyComponent( + () => import("../../Dialogs/GenerateDialog") +); import { ErrorMessage } from "src/components/Shared/ErrorMessage"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { Icon } from "src/components/Shared/Icon"; @@ -41,8 +46,10 @@ import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import cx from "classnames"; import { useRatingKeybinds } from "src/hooks/keybinds"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { TruncatedText } from "src/components/Shared/TruncatedText"; +import { goBackOrReplace } from "src/utils/history"; +import { FormattedDate } from "src/components/Shared/Date"; interface IProps { gallery: GQL.GalleryDataFragment; @@ -58,7 +65,7 @@ export const GalleryPage: React.FC = ({ gallery, add }) => { const history = useHistory(); const Toast = useToast(); const intl = useIntl(); - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const showLightbox = useGalleryLightbox(gallery.id, gallery.chapters); const [collapsed, setCollapsed] = useState(false); @@ -163,11 +170,12 @@ export const GalleryPage: React.FC = ({ gallery, add }) => { } const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); + const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false); function onDeleteDialogClosed(deleted: boolean) { setIsDeleteAlertOpen(false); if (deleted) { - history.goBack(); + goBackOrReplace(history, "/galleries"); } } @@ -182,6 +190,18 @@ export const GalleryPage: React.FC = ({ gallery, add }) => { } } + function maybeRenderGenerateDialog() { + if (isGenerateDialogOpen) { + return ( + setIsGenerateDialogOpen(false)} + type="gallery" + /> + ); + } + } + function renderOperations() { return ( @@ -208,6 +228,12 @@ export const GalleryPage: React.FC = ({ gallery, add }) => { > + setIsGenerateDialogOpen(true)} + > + {`${intl.formatMessage({ id: "actions.generate" })}…`} + setIsDeleteAlertOpen(true)} @@ -385,6 +411,7 @@ export const GalleryPage: React.FC = ({ gallery, add }) => { {title} {maybeRenderDeleteDialog()} + {maybeRenderGenerateDialog()}
    @@ -409,11 +436,7 @@ export const GalleryPage: React.FC = ({ gallery, add }) => {
    {!!gallery.date && ( - + )}
    diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx index 9e859c1c8..275c4263b 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx @@ -9,98 +9,102 @@ import { useToast } from "src/hooks/Toast"; import { useIntl } from "react-intl"; import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { galleryTitle } from "src/core/galleries"; +import { IItemListOperation } from "src/components/List/FilteredListToolbar"; +import { PatchComponent } from "src/patch"; interface IGalleryAddProps { active: boolean; gallery: GQL.GalleryDataFragment; + extraOperations?: IItemListOperation[]; } -export const GalleryAddPanel: React.FC = ({ - active, - gallery, -}) => { - const Toast = useToast(); - const intl = useIntl(); +export const GalleryAddPanel: React.FC = PatchComponent( + "GalleryAddPanel", + ({ active, gallery, extraOperations = [] }) => { + const Toast = useToast(); + const intl = useIntl(); - function filterHook(filter: ListFilterModel) { - const galleryValue = { - id: gallery.id, - label: galleryTitle(gallery), - }; - // if galleries is already present, then we modify it, otherwise add - let galleryCriterion = filter.criteria.find((c) => { - return c.criterionOption.type === "galleries"; - }) as GalleriesCriterion | undefined; + function filterHook(filter: ListFilterModel) { + const galleryValue = { + id: gallery.id, + label: galleryTitle(gallery), + }; + // if galleries is already present, then we modify it, otherwise add + let galleryCriterion = filter.criteria.find((c) => { + return c.criterionOption.type === "galleries"; + }) as GalleriesCriterion | undefined; - if ( - galleryCriterion && - galleryCriterion.modifier === GQL.CriterionModifier.Excludes - ) { - // add the gallery if not present if ( - !galleryCriterion.value.find((p) => { - return p.id === gallery.id; - }) + galleryCriterion && + galleryCriterion.modifier === GQL.CriterionModifier.Excludes ) { - galleryCriterion.value.push(galleryValue); + // add the gallery if not present + if ( + !galleryCriterion.value.find((p) => { + return p.id === gallery.id; + }) + ) { + galleryCriterion.value.push(galleryValue); + } + + galleryCriterion.modifier = GQL.CriterionModifier.Excludes; + } else { + // overwrite + galleryCriterion = new GalleriesCriterion(); + galleryCriterion.modifier = GQL.CriterionModifier.Excludes; + galleryCriterion.value = [galleryValue]; + filter.criteria.push(galleryCriterion); } - galleryCriterion.modifier = GQL.CriterionModifier.Excludes; - } else { - // overwrite - galleryCriterion = new GalleriesCriterion(); - galleryCriterion.modifier = GQL.CriterionModifier.Excludes; - galleryCriterion.value = [galleryValue]; - filter.criteria.push(galleryCriterion); + return filter; } - return filter; - } - - async function addImages( - result: GQL.FindImagesQueryResult, - filter: ListFilterModel, - selectedIds: Set - ) { - try { - await mutateAddGalleryImages({ - gallery_id: gallery.id!, - image_ids: Array.from(selectedIds.values()), - }); - const imageCount = selectedIds.size; - Toast.success( - intl.formatMessage( - { id: "toast.added_entity" }, - { - count: imageCount, - singularEntity: intl.formatMessage({ id: "image" }), - pluralEntity: intl.formatMessage({ id: "images" }), - } - ) - ); - } catch (e) { - Toast.error(e); + async function addImages( + result: GQL.FindImagesQueryResult, + filter: ListFilterModel, + selectedIds: Set + ) { + try { + await mutateAddGalleryImages({ + gallery_id: gallery.id!, + image_ids: Array.from(selectedIds.values()), + }); + const imageCount = selectedIds.size; + Toast.success( + intl.formatMessage( + { id: "toast.added_entity" }, + { + count: imageCount, + singularEntity: intl.formatMessage({ id: "image" }), + pluralEntity: intl.formatMessage({ id: "images" }), + } + ) + ); + } catch (e) { + Toast.error(e); + } } + + const otherOperations = [ + ...extraOperations, + { + text: intl.formatMessage( + { id: "actions.add_to_entity" }, + { entityType: intl.formatMessage({ id: "gallery" }) } + ), + onClick: addImages, + isDisplayed: showWhenSelected, + postRefetch: true, + icon: faPlus, + }, + ]; + + return ( + + ); } - - const otherOperations = [ - { - text: intl.formatMessage( - { id: "actions.add_to_entity" }, - { entityType: intl.formatMessage({ id: "gallery" }) } - ), - onClick: addImages, - isDisplayed: showWhenSelected, - postRefetch: true, - icon: faPlus, - }, - ]; - - return ( - - ); -}; +); diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx index 128e70d38..ebb465868 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx @@ -19,12 +19,14 @@ const GalleryCreate: React.FC = () => { const [createGallery] = useGalleryCreate(); - async function onSave(input: GQL.GalleryCreateInput) { + async function onSave(input: GQL.GalleryCreateInput, andNew?: boolean) { const result = await createGallery({ variables: { input }, }); if (result.data?.galleryCreate) { - history.push(`/galleries/${result.data.galleryCreate.id}`); + if (!andNew) { + history.push(`/galleries/${result.data.galleryCreate.id}`); + } Toast.success( intl.formatMessage( { id: "toast.created_entity" }, diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx index b9eea8f5d..04b802784 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { Prompt } from "react-router-dom"; -import { Button, Form, Col, Row } from "react-bootstrap"; +import { Button, Dropdown, Form, Col, Row, SplitButton } from "react-bootstrap"; import Mousetrap from "mousetrap"; import * as GQL from "src/core/generated-graphql"; import * as yup from "yup"; @@ -35,7 +35,7 @@ import { ScraperMenu } from "src/components/Shared/ScraperMenu"; interface IProps { gallery: Partial; isVisible: boolean; - onSubmit: (input: GQL.GalleryCreateInput) => Promise; + onSubmit: (input: GQL.GalleryCreateInput, andNew?: boolean) => Promise; onDelete: () => void; } @@ -162,10 +162,25 @@ export const GalleryEditPanel: React.FC = ({ ); }, [scrapers]); - async function onSave(input: InputValues) { + const cover = useMemo(() => { + if (gallery?.paths?.cover) { + return ( +
    + {intl.formatMessage({ +
    + ); + } + + return
    ; + }, [gallery?.paths?.cover, intl]); + + async function onSave(input: InputValues, andNew?: boolean) { setIsLoading(true); try { - await onSubmit(input); + await onSubmit(input, andNew); formik.resetForm(); } catch (e) { Toast.error(e); @@ -173,6 +188,11 @@ export const GalleryEditPanel: React.FC = ({ setIsLoading(false); } + async function onSaveAndNewClick() { + const input = schema.cast(formik.values); + onSave(input, true); + } + async function onScrapeClicked(s: GQL.ScraperSourceInput) { if (!gallery || !gallery.id) return; @@ -335,6 +355,19 @@ export const GalleryEditPanel: React.FC = ({ xl: 12, }, }; + const urlProps = isNew + ? splitProps + : { + labelProps: { + column: true, + md: 3, + lg: 12, + }, + fieldProps: { + md: 9, + lg: 12, + }, + }; const { renderField, renderInputField, renderDateField, renderURLListField } = formikUtils(intl, formik, splitProps); @@ -417,16 +450,31 @@ export const GalleryEditPanel: React.FC = ({
    - + {isNew ? ( + formik.submitForm()} + > + onSaveAndNewClick()}> + + + + ) : ( + + )} +
    + + ); +}; interface IGalleryList { filterHook?: (filter: ListFilterModel) => ListFilterModel; view?: View; alterQuery?: boolean; + extraOperations?: IItemListOperation[]; } -export const GalleryList: React.FC = ({ - filterHook, - view, - alterQuery, -}) => { - const intl = useIntl(); +function useViewRandom(filter: ListFilterModel, count: number) { const history = useHistory(); - const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); - const [isExportAll, setIsExportAll] = useState(false); - const filterMode = GQL.FilterMode.Galleries; + const viewRandom = useCallback(async () => { + // query for a random scene + if (count === 0) { + return; + } - const otherOperations = [ - { - text: intl.formatMessage({ id: "actions.view_random" }), - onClick: viewRandom, - }, - { - text: intl.formatMessage({ id: "actions.export" }), - onClick: onExport, - isDisplayed: showWhenSelected, - }, - { - text: intl.formatMessage({ id: "actions.export_all" }), - onClick: onExportAll, - }, - ]; + const index = Math.floor(Math.random() * count); + const filterCopy = cloneDeep(filter); + filterCopy.itemsPerPage = 1; + filterCopy.currentPage = index + 1; + const singleResult = await queryFindGalleries(filterCopy); + if (singleResult.data.findGalleries.galleries.length === 1) { + const { id } = singleResult.data.findGalleries.galleries[0]; + // navigate to the image player page + history.push(`/galleries/${id}`); + } + }, [history, filter, count]); - function addKeybinds( - result: GQL.FindGalleriesQueryResult, - filter: ListFilterModel - ) { + return viewRandom; +} + +function useAddKeybinds(filter: ListFilterModel, count: number) { + const viewRandom = useViewRandom(filter, count); + + useEffect(() => { Mousetrap.bind("p r", () => { - viewRandom(result, filter); + viewRandom(); }); return () => { Mousetrap.unbind("p r"); }; - } + }, [viewRandom]); +} - async function viewRandom( - result: GQL.FindGalleriesQueryResult, - filter: ListFilterModel - ) { - // query for a random image - if (result.data?.findGalleries) { - const { count } = result.data.findGalleries; +export const FilteredGalleryList = PatchComponent( + "FilteredGalleryList", + (props: IGalleryList) => { + const intl = useIntl(); + const history = useHistory(); + const location = useLocation(); - const index = Math.floor(Math.random() * count); - const filterCopy = cloneDeep(filter); - filterCopy.itemsPerPage = 1; - filterCopy.currentPage = index + 1; - const singleResult = await queryFindGalleries(filterCopy); - if (singleResult.data.findGalleries.galleries.length === 1) { - const { id } = singleResult.data.findGalleries.galleries[0]; - // navigate to the image player page - history.push(`/galleries/${id}`); - } - } - } - - async function onExport() { - setIsExportAll(false); - setIsExportDialogOpen(true); - } - - async function onExportAll() { - setIsExportAll(true); - setIsExportDialogOpen(true); - } - - function renderContent( - result: GQL.FindGalleriesQueryResult, - filter: ListFilterModel, - selectedIds: Set, - onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void - ) { - function maybeRenderGalleryExportDialog() { - if (isExportDialogOpen) { - return ( - setIsExportDialogOpen(false)} - /> - ); + const searchFocus = useFocus(); + + const { filterHook, view, alterQuery } = props; + + // States + const { + showSidebar, + setShowSidebar, + sectionOpen, + setSectionOpen, + loading: sidebarStateLoading, + } = useSidebarState(view); + + const { filterState, queryResult, modalState, listSelect, showEditFilter } = + useFilteredItemList({ + filterStateProps: { + filterMode: GQL.FilterMode.Galleries, + view, + useURL: alterQuery, + }, + queryResultProps: { + useResult: useFindGalleries, + getCount: (r) => r.data?.findGalleries.count ?? 0, + getItems: (r) => r.data?.findGalleries.galleries ?? [], + filterHook, + }, + }); + + const { filter, setFilter } = filterState; + + const { effectiveFilter, result, cachedResult, items, totalCount } = + queryResult; + + const { + selectedIds, + selectedItems, + onSelectChange, + onSelectAll, + onSelectNone, + onInvertSelection, + hasSelection, + } = listSelect; + + const { modal, showModal, closeModal } = modalState; + + // Utility hooks + const { setPage, removeCriterion, clearAllCriteria } = useFilterOperations({ + filter, + setFilter, + }); + + useAddKeybinds(filter, totalCount); + useFilteredSidebarKeybinds({ + showSidebar, + setShowSidebar, + }); + + useEffect(() => { + Mousetrap.bind("e", () => { + if (hasSelection) { + onEdit?.(); + } + }); + + Mousetrap.bind("d d", () => { + if (hasSelection) { + onDelete?.(); + } + }); + + return () => { + Mousetrap.unbind("e"); + Mousetrap.unbind("d d"); + }; + }); + + const onCloseEditDelete = useCloseEditDelete({ + closeModal, + onSelectNone, + result, + }); + + function onCreateNew() { + let queryParam = new URLSearchParams(location.search).get("q"); + let newPath = "/galleries/new"; + if (queryParam) { + newPath += "?q=" + encodeURIComponent(queryParam); } + history.push(newPath); } - function renderGalleries() { - if (!result.data?.findGalleries) return; + const viewRandom = useViewRandom(filter, totalCount); - if (filter.displayMode === DisplayMode.Grid) { - return ( - - ); - } - if (filter.displayMode === DisplayMode.List) { - return ( - - ); - } - if (filter.displayMode === DisplayMode.Wall) { - return ( -
    -
    - {result.data.findGalleries.galleries.map((gallery) => ( - - ))} -
    -
    - ); - } + function onExport(all: boolean) { + showModal( + closeModal()} + /> + ); } - return ( - <> - {maybeRenderGalleryExportDialog()} - {renderGalleries()} - - ); - } + function onEdit() { + showModal( + + ); + } - function renderEditDialog( - selectedImages: GQL.SlimGalleryDataFragment[], - onClose: (applied: boolean) => void - ) { - return ; - } + function onDelete() { + showModal( + + ); + } - function renderDeleteDialog( - selectedImages: GQL.SlimGalleryDataFragment[], - onClose: (confirmed: boolean) => void - ) { - return ( - - ); - } + function onGenerate() { + showModal( + closeModal()} + /> + ); + } - return ( - - onSelectAll(), + isDisplayed: () => totalCount > 0, + }, + { + text: intl.formatMessage({ id: "actions.select_none" }), + onClick: () => onSelectNone(), + isDisplayed: () => hasSelection, + }, + { + text: intl.formatMessage({ id: "actions.invert_selection" }), + onClick: () => onInvertSelection(), + isDisplayed: () => totalCount > 0, + }, + { + text: intl.formatMessage({ id: "actions.view_random" }), + onClick: viewRandom, + }, + { + text: `${intl.formatMessage({ id: "actions.generate" })}…`, + onClick: onGenerate, + isDisplayed: () => hasSelection, + }, + { + text: intl.formatMessage({ id: "actions.export" }), + onClick: () => onExport(false), + isDisplayed: () => hasSelection, + }, + { + text: intl.formatMessage({ id: "actions.export_all" }), + onClick: () => onExport(true), + }, + ]; + + // render + if (sidebarStateLoading) return null; + + const operations = ( + - - ); -}; + ); + + return ( +
    + {modal} + + + + setShowSidebar(false)}> + setShowSidebar(false)} + count={cachedResult.loading ? undefined : totalCount} + focus={searchFocus} + /> + + setShowSidebar(!showSidebar)} + > + + + showEditFilter(c.criterionOption.type)} + onRemoveCriterion={removeCriterion} + onRemoveAll={clearAllCriteria} + /> + +
    + setFilter(filter.changePage(page))} + /> + +
    + + + + + + {totalCount > filter.itemsPerPage && ( +
    +
    + +
    +
    + )} +
    +
    +
    +
    + ); + } +); diff --git a/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx b/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx index ee94d6da2..b56b48c36 100644 --- a/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx @@ -7,6 +7,7 @@ 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 { PatchComponent } from "src/patch"; interface IProps { isTouch: boolean; @@ -14,41 +15,44 @@ interface IProps { header: string; } -export const GalleryRecommendationRow: React.FC = (props) => { - const result = useFindGalleries(props.filter); - const cardCount = result.data?.findGalleries.count; +export const GalleryRecommendationRow: React.FC = PatchComponent( + "GalleryRecommendationRow", + (props) => { + const result = useFindGalleries(props.filter); + const cardCount = result.data?.findGalleries.count; - if (!result.loading && !cardCount) { - return null; - } + if (!result.loading && !cardCount) { + return null; + } - return ( - - - - } - > - + + + } > - {result.loading - ? [...Array(props.filter.itemsPerPage)].map((i) => ( -
    - )) - : result.data?.findGalleries.galleries.map((g) => ( - - ))} -
    -
    - ); -}; + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
    + )) + : result.data?.findGalleries.galleries.map((g) => ( + + ))} +
    + + ); + } +); diff --git a/ui/v2.5/src/components/Galleries/GallerySelect.tsx b/ui/v2.5/src/components/Galleries/GallerySelect.tsx index 4cd8825bb..0e02b8cb3 100644 --- a/ui/v2.5/src/components/Galleries/GallerySelect.tsx +++ b/ui/v2.5/src/components/Galleries/GallerySelect.tsx @@ -11,8 +11,9 @@ import * as GQL from "src/core/generated-graphql"; import { queryFindGalleriesForSelect, queryFindGalleriesByIDForSelect, + useGalleryCreate, } from "src/core/StashService"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { useIntl } from "react-intl"; import { defaultMaxOptionsShown } from "src/core/config"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -70,10 +71,14 @@ const gallerySelectSort = PatchFunction( const _GallerySelect: React.FC< IFilterProps & IFilterValueProps & ExtraGalleryProps > = (props) => { - const { configuration } = React.useContext(ConfigurationContext); + const [createGallery] = useGalleryCreate(); + + const { configuration } = useConfigurationContext(); const intl = useIntl(); const maxOptionsShown = configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; + const defaultCreatable = + !configuration?.interface.disableDropdownCreate.gallery; const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]); @@ -203,6 +208,42 @@ const _GallerySelect: React.FC< return ; }; + const onCreate = async (name: string) => { + const result = await createGallery({ + variables: { input: { title: name } }, + }); + return { + value: result.data!.galleryCreate!.id, + item: result.data!.galleryCreate!, + message: "Created gallery", + }; + }; + + const getNamedObject = (id: string, name: string): Gallery => { + return { + id, + title: name, + files: [], + folder: null, + }; + }; + + const isValidNewOption = (inputValue: string, options: Gallery[]) => { + if (!inputValue) { + return false; + } + + if ( + options.some((o) => { + return galleryTitle(o).toLowerCase() === inputValue.toLowerCase(); + }) + ) { + return false; + } + + return true; + }; + return ( {...props} @@ -214,12 +255,16 @@ const _GallerySelect: React.FC< props.className )} loadOptions={loadGalleries} + getNamedObject={getNamedObject} + isValidNewOption={isValidNewOption} components={{ Option: GalleryOption, MultiValueLabel: GalleryMultiValueLabel, SingleValue: GalleryValueLabel, }} isMulti={props.isMulti ?? false} + creatable={props.creatable ?? defaultCreatable} + onCreate={onCreate} placeholder={ props.noSelectionString ?? intl.formatMessage( diff --git a/ui/v2.5/src/components/Galleries/GalleryViewer.tsx b/ui/v2.5/src/components/Galleries/GalleryViewer.tsx index 7ebb679fd..f570f9990 100644 --- a/ui/v2.5/src/components/Galleries/GalleryViewer.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryViewer.tsx @@ -67,8 +67,8 @@ export const GalleryViewer: React.FC = ({ galleryId }) => { images.forEach((image, index) => { let imageData = { src: image.paths.thumbnail!, - width: image.visual_files[0].width, - height: image.visual_files[0].height, + width: image.visual_files[0]?.width ?? 0, + height: image.visual_files[0]?.height ?? 0, tabIndex: index, key: image.id ?? index, loading: "lazy", diff --git a/ui/v2.5/src/components/Galleries/GalleryWallCard.tsx b/ui/v2.5/src/components/Galleries/GalleryWallCard.tsx index c794ddc14..c1501bd9d 100644 --- a/ui/v2.5/src/components/Galleries/GalleryWallCard.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryWallCard.tsx @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import { Form } from "react-bootstrap"; import { useIntl } from "react-intl"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; @@ -8,6 +9,7 @@ import { useGalleryLightbox } from "src/hooks/Lightbox/hooks"; import { galleryTitle } from "src/core/galleries"; import { RatingSystem } from "../Shared/Rating/RatingSystem"; import { GalleryPreviewScrubber } from "./GalleryPreviewScrubber"; +import { useDragMoveSelect } from "../Shared/GridCard/dragMoveSelect"; import cx from "classnames"; const CLASSNAME = "GalleryWallCard"; @@ -18,6 +20,9 @@ const CLASSNAME_IMG_CONTAIN = `${CLASSNAME}-img-contain`; interface IProps { gallery: GQL.SlimGalleryDataFragment; + selected?: boolean; + onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void; + selecting?: boolean; } type Orientation = "landscape" | "portrait"; @@ -26,7 +31,12 @@ function getOrientation(width: number, height: number): Orientation { return width > height ? "landscape" : "portrait"; } -const GalleryWallCard: React.FC = ({ gallery }) => { +const GalleryWallCard: React.FC = ({ + gallery, + selected, + onSelectedChanged, + selecting, +}) => { const intl = useIntl(); const [coverOrientation, setCoverOrientation] = React.useState("landscape"); @@ -34,6 +44,12 @@ const GalleryWallCard: React.FC = ({ gallery }) => { React.useState("landscape"); const showLightbox = useGalleryLightbox(gallery.id, gallery.chapters); + const { dragProps } = useDragMoveSelect({ + selecting: selecting || false, + selected: selected || false, + onSelectedChanged: onSelectedChanged, + }); + const cover = gallery?.paths.cover; function onCoverLoad(e: React.SyntheticEvent) { @@ -58,6 +74,14 @@ const GalleryWallCard: React.FC = ({ gallery }) => { ? [...performerNames.slice(0, -2), performerNames.slice(-2).join(" & ")] : performerNames; + function handleCardClick(event: React.MouseEvent) { + if (selecting && onSelectedChanged) { + onSelectedChanged(!selected, event.shiftKey); + return; + } + showLightboxStart(); + } + async function showLightboxStart() { if (gallery.image_count === 0) { return; @@ -69,15 +93,32 @@ const GalleryWallCard: React.FC = ({ gallery }) => { const imgClassname = imageOrientation !== coverOrientation ? CLASSNAME_IMG_CONTAIN : ""; + let shiftKey = false; + return ( <>
    showLightboxStart()} role="button" tabIndex={0} + {...dragProps} > + {onSelectedChanged && ( + onSelectedChanged(!selected, shiftKey)} + onClick={( + event: React.MouseEvent + ) => { + shiftKey = event.shiftKey; + event.stopPropagation(); + }} + /> + )} = ({ gallery }) => { )}
    - {gallery.date && TextUtils.formatDate(intl, gallery.date)} + {gallery.date && TextUtils.formatFuzzyDate(intl, gallery.date)}
    diff --git a/ui/v2.5/src/components/Galleries/styles.scss b/ui/v2.5/src/components/Galleries/styles.scss index 58116e936..c53175313 100644 --- a/ui/v2.5/src/components/Galleries/styles.scss +++ b/ui/v2.5/src/components/Galleries/styles.scss @@ -204,13 +204,31 @@ $galleryTabWidth: 450px; font-size: 1.3em; height: calc(1.5em + 0.75rem + 2px); } + + .form-group[data-field="urls"] .string-list-input input.form-control { + font-size: 0.85em; + } +} + +.gallery-cover { + aspect-ratio: 4 / 3; + display: block; + height: auto; + width: 100%; +} + +.gallery-cover img { + height: auto; + max-height: 100%; + max-width: 100%; + object-fit: contain; + width: auto; } div.GalleryWall { display: flex; flex-wrap: wrap; margin: 0 auto; - width: 96vw; /* Prevents last row from consuming all space and stretching images to oblivion */ &::after { diff --git a/ui/v2.5/src/components/Groups/EditGroupsDialog.tsx b/ui/v2.5/src/components/Groups/EditGroupsDialog.tsx index efd14e757..ef3171de2 100644 --- a/ui/v2.5/src/components/Groups/EditGroupsDialog.tsx +++ b/ui/v2.5/src/components/Groups/EditGroupsDialog.tsx @@ -23,12 +23,12 @@ import { ContainingGroupsMultiSet } from "./ContainingGroupsMultiSet"; import { IRelatedGroupEntry } from "./GroupDetails/RelatedGroupTable"; interface IListOperationProps { - selected: GQL.GroupDataFragment[]; + selected: GQL.ListGroupDataFragment[]; onClose: (applied: boolean) => void; } export function getAggregateContainingGroups( - state: Pick[] + state: Pick[] ) { const sortedLists: IRelatedGroupEntry[][] = state.map((o) => o.containing_groups @@ -144,7 +144,7 @@ export const EditGroupsDialog: React.FC = ( let updateDirector: string | undefined; let first = true; - state.forEach((group: GQL.GroupDataFragment) => { + state.forEach((group: GQL.ListGroupDataFragment) => { const groupTagIDs = (group.tags ?? []).map((p) => p.id).sort(); const groupContainingGroupIDs = (group.containing_groups ?? []).sort( (a, b) => a.group.id.localeCompare(b.group.id) diff --git a/ui/v2.5/src/components/Groups/GroupCard.tsx b/ui/v2.5/src/components/Groups/GroupCard.tsx index f1d6089d0..5bc1b5d7f 100644 --- a/ui/v2.5/src/components/Groups/GroupCard.tsx +++ b/ui/v2.5/src/components/Groups/GroupCard.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from "react"; import { Button, ButtonGroup } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; +import { PatchComponent } from "src/patch"; import { GridCard } from "../Shared/GridCard/GridCard"; import { HoverPopover } from "../Shared/HoverPopover"; import { Icon } from "../Shared/Icon"; @@ -10,6 +11,7 @@ import { FormattedMessage } from "react-intl"; import { RatingBanner } from "../Shared/RatingBanner"; import { faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons"; import { RelatedGroupPopoverButton } from "./RelatedGroupPopover"; +import { OCounterButton } from "../Shared/CountButton"; const Description: React.FC<{ sceneNumber?: number; @@ -35,7 +37,7 @@ const Description: React.FC<{ }; interface IProps { - group: GQL.GroupDataFragment; + group: GQL.ListGroupDataFragment; cardWidth?: number; sceneNumber?: number; selecting?: boolean; @@ -46,130 +48,140 @@ interface IProps { onMove?: (srcIds: string[], targetId: string, after: boolean) => void; } -export const GroupCard: React.FC = ({ - group, - sceneNumber, - cardWidth, - selecting, - selected, - zoomIndex, - onSelectedChanged, - fromGroupId, - onMove, -}) => { - const groupDescription = useMemo(() => { - if (!fromGroupId) { - return undefined; - } +export const GroupCard: React.FC = PatchComponent( + "GroupCard", + ({ + group, + sceneNumber, + cardWidth, + selecting, + selected, + zoomIndex, + onSelectedChanged, + fromGroupId, + onMove, + }) => { + const groupDescription = useMemo(() => { + if (!fromGroupId) { + return undefined; + } - const containingGroup = group.containing_groups.find( - (cg) => cg.group.id === fromGroupId - ); + const containingGroup = group.containing_groups.find( + (cg) => cg.group.id === fromGroupId + ); - return containingGroup?.description ?? undefined; - }, [fromGroupId, group.containing_groups]); + return containingGroup?.description ?? undefined; + }, [fromGroupId, group.containing_groups]); - function maybeRenderScenesPopoverButton() { - if (group.scenes.length === 0) return; + function maybeRenderScenesPopoverButton() { + if (group.scenes.length === 0) return; - const popoverContent = group.scenes.map((scene) => ( - - )); + const popoverContent = group.scenes.map((scene) => ( + + )); - return ( - - - - ); - } - - function maybeRenderTagPopoverButton() { - if (group.tags.length <= 0) return; - - const popoverContent = group.tags.map((tag) => ( - - )); - - return ( - - - - ); - } - - function maybeRenderPopoverButtonGroup() { - if ( - sceneNumber || - groupDescription || - group.scenes.length > 0 || - group.tags.length > 0 || - group.containing_groups.length > 0 || - group.sub_group_count > 0 - ) { return ( - <> - -
    - - {maybeRenderScenesPopoverButton()} - {maybeRenderTagPopoverButton()} - {(group.sub_group_count > 0 || - group.containing_groups.length > 0) && ( - - )} - - + + + ); } - } - return ( - - {group.name - - + function maybeRenderTagPopoverButton() { + if (group.tags.length <= 0) return; + + const popoverContent = group.tags.map((tag) => ( + + )); + + return ( + + + + ); + } + + function maybeRenderOCounter() { + if (!group.o_counter) return; + + return ; + } + + function maybeRenderPopoverButtonGroup() { + if ( + sceneNumber || + groupDescription || + group.scenes.length > 0 || + group.tags.length > 0 || + group.containing_groups.length > 0 || + group.sub_group_count > 0 + ) { + return ( + <> + +
    + + {maybeRenderScenesPopoverButton()} + {maybeRenderTagPopoverButton()} + {(group.sub_group_count > 0 || + group.containing_groups.length > 0) && ( + + )} + {maybeRenderOCounter()} + + + ); } - details={ -
    - {group.date} - -
    - } - selected={selected} - selecting={selecting} - onSelectedChanged={onSelectedChanged} - popovers={maybeRenderPopoverButtonGroup()} - /> - ); -}; + } + + return ( + + {group.name + + + } + details={ +
    + {group.date} + +
    + } + selected={selected} + selecting={selecting} + onSelectedChanged={onSelectedChanged} + popovers={maybeRenderPopoverButtonGroup()} + /> + ); + } +); diff --git a/ui/v2.5/src/components/Groups/GroupCardGrid.tsx b/ui/v2.5/src/components/Groups/GroupCardGrid.tsx index b73919e64..e3b70c75f 100644 --- a/ui/v2.5/src/components/Groups/GroupCardGrid.tsx +++ b/ui/v2.5/src/components/Groups/GroupCardGrid.tsx @@ -5,9 +5,10 @@ import { useCardWidth, useContainerDimensions, } from "../Shared/GridCard/GridCard"; +import { PatchComponent } from "src/patch"; interface IGroupCardGrid { - groups: GQL.GroupDataFragment[]; + groups: GQL.ListGroupDataFragment[]; selectedIds: Set; zoomIndex: number; onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; @@ -17,34 +18,30 @@ interface IGroupCardGrid { const zoomWidths = [210, 250, 300, 375]; -export const GroupCardGrid: React.FC = ({ - groups, - selectedIds, - zoomIndex, - onSelectChange, - fromGroupId, - onMove, -}) => { - const [componentRef, { width: containerWidth }] = useContainerDimensions(); - const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); +export const GroupCardGrid: React.FC = PatchComponent( + "GroupCardGrid", + ({ groups, selectedIds, zoomIndex, onSelectChange, fromGroupId, onMove }) => { + const [componentRef, { width: containerWidth }] = useContainerDimensions(); + const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); - return ( -
    - {groups.map((p) => ( - 0} - selected={selectedIds.has(p.id)} - onSelectedChanged={(selected: boolean, shiftKey: boolean) => - onSelectChange(p.id, selected, shiftKey) - } - fromGroupId={fromGroupId} - onMove={onMove} - /> - ))} -
    - ); -}; + return ( +
    + {groups.map((p) => ( + 0} + selected={selectedIds.has(p.id)} + onSelectedChanged={(selected: boolean, shiftKey: boolean) => + onSelectChange(p.id, selected, shiftKey) + } + fromGroupId={fromGroupId} + onMove={onMove} + /> + ))} +
    + ); + } +); diff --git a/ui/v2.5/src/components/Groups/GroupDetails/AddGroupsDialog.tsx b/ui/v2.5/src/components/Groups/GroupDetails/AddGroupsDialog.tsx index b89356810..79c6075c0 100644 --- a/ui/v2.5/src/components/Groups/GroupDetails/AddGroupsDialog.tsx +++ b/ui/v2.5/src/components/Groups/GroupDetails/AddGroupsDialog.tsx @@ -114,6 +114,7 @@ export const AddSubGroupsDialog: React.FC = ( onUpdate={(input) => setEntries(input)} excludeIDs={excludeIDs} filterHook={filterHook} + menuPortalTarget={document.body} /> diff --git a/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx b/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx index bd58a6682..b2b3d8176 100644 --- a/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx +++ b/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx @@ -23,7 +23,7 @@ import { import { GroupEditPanel } from "./GroupEditPanel"; import { faRefresh, faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { DetailImage } from "src/components/Shared/DetailImage"; import { useRatingKeybinds } from "src/hooks/keybinds"; import { useLoadStickyHeader } from "src/hooks/detailsPanel"; @@ -43,6 +43,7 @@ import { Button, Tab, Tabs } from "react-bootstrap"; import { GroupSubGroupsPanel } from "./GroupSubGroupsPanel"; import { GroupPerformersPanel } from "./GroupPerformersPanel"; import { Icon } from "src/components/Shared/Icon"; +import { goBackOrReplace } from "src/utils/history"; const validTabs = ["default", "scenes", "performers", "subgroups"] as const; type TabKey = (typeof validTabs)[number]; @@ -145,7 +146,7 @@ const GroupPage: React.FC = ({ group, tabKey }) => { const Toast = useToast(); // Configuration settings - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const uiConfig = configuration?.ui; const enableBackgroundImage = uiConfig?.enableMovieBackgroundImage ?? false; const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false; @@ -276,7 +277,7 @@ const GroupPage: React.FC = ({ group, tabKey }) => { return; } - history.goBack(); + goBackOrReplace(history, "/groups"); } function toggleEditing(value?: boolean) { diff --git a/ui/v2.5/src/components/Groups/GroupDetails/GroupCreate.tsx b/ui/v2.5/src/components/Groups/GroupDetails/GroupCreate.tsx index 9dd3e22b9..5026d5b6e 100644 --- a/ui/v2.5/src/components/Groups/GroupDetails/GroupCreate.tsx +++ b/ui/v2.5/src/components/Groups/GroupDetails/GroupCreate.tsx @@ -25,12 +25,14 @@ const GroupCreate: React.FC = () => { const [createGroup] = useGroupCreate(); - async function onSave(input: GQL.GroupCreateInput) { + async function onSave(input: GQL.GroupCreateInput, andNew?: boolean) { const result = await createGroup({ variables: { input }, }); if (result.data?.groupCreate?.id) { - history.push(`/groups/${result.data.groupCreate.id}`); + if (!andNew) { + history.push(`/groups/${result.data.groupCreate.id}`); + } Toast.success( intl.formatMessage( { id: "toast.created_entity" }, diff --git a/ui/v2.5/src/components/Groups/GroupDetails/GroupDetailsPanel.tsx b/ui/v2.5/src/components/Groups/GroupDetails/GroupDetailsPanel.tsx index d93b06466..b8e39ffe6 100644 --- a/ui/v2.5/src/components/Groups/GroupDetails/GroupDetailsPanel.tsx +++ b/ui/v2.5/src/components/Groups/GroupDetails/GroupDetailsPanel.tsx @@ -65,7 +65,7 @@ export const GroupDetailsPanel: React.FC = ({ /> ; - onSubmit: (group: GQL.GroupCreateInput) => Promise; + onSubmit: (group: GQL.GroupCreateInput, andNew?: boolean) => Promise; onCancel: () => void; onDelete: () => void; setFrontImage: (image?: string | null) => void; @@ -208,10 +208,10 @@ export const GroupEditPanel: React.FC = ({ } } - async function onSave(input: InputValues) { + async function onSave(input: InputValues, andNew?: boolean) { setIsLoading(true); try { - await onSubmit(input); + await onSubmit(input, andNew); formik.resetForm(); } catch (e) { Toast.error(e); @@ -219,6 +219,11 @@ export const GroupEditPanel: React.FC = ({ setIsLoading(false); } + async function onSaveAndNewClick() { + const input = schema.cast(formik.values); + onSave(input, true); + } + async function onScrapeGroupURL(url: string) { if (!url) return; setIsLoading(true); @@ -462,6 +467,7 @@ export const GroupEditPanel: React.FC = ({ isEditing onToggleEdit={onCancel} onSave={formik.handleSubmit} + onSaveAndNew={isNew ? onSaveAndNewClick : undefined} saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})} onImageChange={onFrontImageChange} onImageChangeURL={onFrontImageLoad} diff --git a/ui/v2.5/src/components/Groups/GroupDetails/GroupScrapeDialog.tsx b/ui/v2.5/src/components/Groups/GroupDetails/GroupScrapeDialog.tsx index bdb5d6ad5..21fc3282f 100644 --- a/ui/v2.5/src/components/Groups/GroupDetails/GroupScrapeDialog.tsx +++ b/ui/v2.5/src/components/Groups/GroupDetails/GroupScrapeDialog.tsx @@ -2,12 +2,12 @@ import React, { useState } from "react"; import { useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { - ScrapeDialog, ScrapedInputGroupRow, ScrapedImageRow, ScrapedTextAreaRow, ScrapedStringListRow, -} from "src/components/Shared/ScrapeDialog/ScrapeDialog"; +} from "src/components/Shared/ScrapeDialog/ScrapeDialogRow"; +import { ScrapeDialog } from "src/components/Shared/ScrapeDialog/ScrapeDialog"; import TextUtils from "src/utils/text"; import { ObjectScrapeResult, @@ -98,7 +98,7 @@ export const GroupScrapeDialog: React.FC = ({ setNewObject: setNewStudio, }); - const { tags, newTags, scrapedTagsRow } = useScrapedTags( + const { tags, newTags, scrapedTagsRow, linkDialog } = useScrapedTags( groupTags, scraped.tags ); @@ -149,37 +149,44 @@ export const GroupScrapeDialog: React.FC = ({ return ( <> setName(value)} /> setAliases(value)} /> setDuration(value)} /> setDate(value)} /> setDirector(value)} /> setSynopsis(value)} /> setStudio(value)} @@ -187,18 +194,21 @@ export const GroupScrapeDialog: React.FC = ({ onCreateNew={createNewStudio} /> setURLs(value)} /> {scrapedTagsRow} setFrontImage(value)} /> = ({ ); } + if (linkDialog) { + return linkDialog; + } + return ( { onClose(apply ? makeNewScrapedItem() : undefined); }} - /> + > + {renderScrapeRows()} + ); }; diff --git a/ui/v2.5/src/components/Groups/GroupDetails/GroupSubGroupsPanel.tsx b/ui/v2.5/src/components/Groups/GroupDetails/GroupSubGroupsPanel.tsx index a2bb26e95..32836ab24 100644 --- a/ui/v2.5/src/components/Groups/GroupDetails/GroupSubGroupsPanel.tsx +++ b/ui/v2.5/src/components/Groups/GroupDetails/GroupSubGroupsPanel.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React from "react"; import * as GQL from "src/core/generated-graphql"; import { GroupList } from "../GroupList"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -18,7 +18,10 @@ import { SearchTermInput, } from "src/components/List/ListFilter"; import { useFilter } from "src/components/List/FilterProvider"; -import { IFilteredListToolbar } from "src/components/List/FilteredListToolbar"; +import { + IFilteredListToolbar, + IItemListOperation, +} from "src/components/List/FilteredListToolbar"; import { showWhenNoneSelected, showWhenSelected, @@ -28,6 +31,7 @@ import { useIntl } from "react-intl"; import { useToast } from "src/hooks/Toast"; import { useModal } from "src/hooks/modal"; import { AddSubGroupsDialog } from "./AddGroupsDialog"; +import { PatchComponent } from "src/patch"; const useContainingGroupFilterHook = ( group: Pick, @@ -72,7 +76,8 @@ const Toolbar: React.FC = ({ onDelete, operations, }) => { - const { getSelected, onSelectAll, onSelectNone } = useListContext(); + const { getSelected, onSelectAll, onSelectNone, onInvertSelection } = + useListContext(); const { filter, setFilter } = useFilter(); return ( @@ -87,6 +92,7 @@ const Toolbar: React.FC = ({ 0} otherOperations={operations} onEdit={onEdit} @@ -99,106 +105,114 @@ const Toolbar: React.FC = ({ interface IGroupSubGroupsPanel { active: boolean; group: GQL.GroupDataFragment; + extraOperations?: IItemListOperation[]; } -export const GroupSubGroupsPanel: React.FC = ({ - active, - group, -}) => { - const intl = useIntl(); - const Toast = useToast(); - const { modal, showModal, closeModal } = useModal(); +const defaultFilter = (() => { + const sortBy = "sub_group_order"; + const ret = new ListFilterModel(GQL.FilterMode.Groups, undefined, { + defaultSortBy: sortBy, + }); - const [reorderSubGroups] = useReorderSubGroupsMutation(); - const mutateRemoveSubGroups = useRemoveSubGroups(); + // unset the sort by so that its not included in the URL + ret.sortBy = undefined; - const filterHook = useContainingGroupFilterHook(group); + return ret; +})(); - const defaultFilter = useMemo(() => { - const sortBy = "sub_group_order"; - const ret = new ListFilterModel(GQL.FilterMode.Groups, undefined, { - defaultSortBy: sortBy, - }); +export const GroupSubGroupsPanel: React.FC = + PatchComponent( + "GroupSubGroupsPanel", + ({ active, group, extraOperations = [] }) => { + const intl = useIntl(); + const Toast = useToast(); + const { modal, showModal, closeModal } = useModal(); - // unset the sort by so that its not included in the URL - ret.sortBy = undefined; + const [reorderSubGroups] = useReorderSubGroupsMutation(); + const mutateRemoveSubGroups = useRemoveSubGroups(); - return ret; - }, []); + const filterHook = useContainingGroupFilterHook(group); - async function removeSubGroups( - result: GQL.FindGroupsQueryResult, - filter: ListFilterModel, - selectedIds: Set - ) { - try { - await mutateRemoveSubGroups(group.id, Array.from(selectedIds.values())); + async function removeSubGroups( + result: GQL.FindGroupsQueryResult, + filter: ListFilterModel, + selectedIds: Set + ) { + try { + await mutateRemoveSubGroups( + group.id, + Array.from(selectedIds.values()) + ); - Toast.success( - intl.formatMessage( - { id: "toast.removed_entity" }, - { - count: selectedIds.size, - singularEntity: intl.formatMessage({ id: "group" }), - pluralEntity: intl.formatMessage({ id: "groups" }), - } - ) - ); - } catch (e) { - Toast.error(e); - } - } + Toast.success( + intl.formatMessage( + { id: "toast.removed_entity" }, + { + count: selectedIds.size, + singularEntity: intl.formatMessage({ id: "group" }), + pluralEntity: intl.formatMessage({ id: "groups" }), + } + ) + ); + } catch (e) { + Toast.error(e); + } + } - async function onAddSubGroups() { - showModal( - - ); - } + async function onAddSubGroups() { + showModal( + + ); + } - const otherOperations = [ - { - text: intl.formatMessage({ id: "actions.add_sub_groups" }), - onClick: onAddSubGroups, - isDisplayed: showWhenNoneSelected, - postRefetch: true, - icon: faPlus, - buttonVariant: "secondary", - }, - { - text: intl.formatMessage({ id: "actions.remove_from_containing_group" }), - onClick: removeSubGroups, - isDisplayed: showWhenSelected, - postRefetch: true, - icon: faMinus, - buttonVariant: "danger", - }, - ]; - - function onMove(srcIds: string[], targetId: string, after: boolean) { - reorderSubGroups({ - variables: { - input: { - group_id: group.id, - sub_group_ids: srcIds, - insert_at_id: targetId, - insert_after: after, + const otherOperations = [ + ...extraOperations, + { + text: intl.formatMessage({ id: "actions.add_sub_groups" }), + onClick: onAddSubGroups, + isDisplayed: showWhenNoneSelected, + postRefetch: true, + icon: faPlus, + buttonVariant: "secondary", }, - }, - }); - } + { + text: intl.formatMessage({ + id: "actions.remove_from_containing_group", + }), + onClick: removeSubGroups, + isDisplayed: showWhenSelected, + postRefetch: true, + icon: faMinus, + buttonVariant: "danger", + }, + ]; - return ( - <> - {modal} - } - /> - + function onMove(srcIds: string[], targetId: string, after: boolean) { + reorderSubGroups({ + variables: { + input: { + group_id: group.id, + sub_group_ids: srcIds, + insert_at_id: targetId, + insert_after: after, + }, + }, + }); + } + + return ( + <> + {modal} + } + /> + + ); + } ); -}; diff --git a/ui/v2.5/src/components/Groups/GroupList.tsx b/ui/v2.5/src/components/Groups/GroupList.tsx index 707b6fa4b..a08610569 100644 --- a/ui/v2.5/src/components/Groups/GroupList.tsx +++ b/ui/v2.5/src/components/Groups/GroupList.tsx @@ -21,6 +21,7 @@ import { IFilteredListToolbar, IItemListOperation, } from "../List/FilteredListToolbar"; +import { PatchComponent } from "src/patch"; const GroupExportDialog: React.FC<{ open?: boolean; @@ -90,151 +91,153 @@ interface IGroupList extends IGroupListContext { otherOperations?: IItemListOperation[]; } -export const GroupList: React.FC = ({ - filterHook, - alterQuery, - defaultFilter, - view, - fromGroupId, - onMove, - selectable, - renderToolbar, - otherOperations: providedOperations = [], -}) => { - const intl = useIntl(); - const history = useHistory(); - const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); - const [isExportAll, setIsExportAll] = useState(false); +export const GroupList: React.FC = PatchComponent( + "GroupList", + ({ + filterHook, + alterQuery, + defaultFilter, + view, + fromGroupId, + onMove, + selectable, + renderToolbar, + otherOperations: providedOperations = [], + }) => { + const intl = useIntl(); + const history = useHistory(); + const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); + const [isExportAll, setIsExportAll] = useState(false); - const otherOperations = [ - { - text: intl.formatMessage({ id: "actions.view_random" }), - onClick: viewRandom, - }, - { - text: intl.formatMessage({ id: "actions.export" }), - onClick: onExport, - isDisplayed: showWhenSelected, - }, - { - text: intl.formatMessage({ id: "actions.export_all" }), - onClick: onExportAll, - }, - ...providedOperations, - ]; + const otherOperations = [ + { + text: intl.formatMessage({ id: "actions.view_random" }), + onClick: viewRandom, + }, + { + text: intl.formatMessage({ id: "actions.export" }), + onClick: onExport, + isDisplayed: showWhenSelected, + }, + { + text: intl.formatMessage({ id: "actions.export_all" }), + onClick: onExportAll, + }, + ...providedOperations, + ]; - function addKeybinds( - result: GQL.FindGroupsQueryResult, - filter: ListFilterModel - ) { - Mousetrap.bind("p r", () => { - viewRandom(result, filter); - }); + function addKeybinds( + result: GQL.FindGroupsQueryResult, + filter: ListFilterModel + ) { + Mousetrap.bind("p r", () => { + viewRandom(result, filter); + }); - return () => { - Mousetrap.unbind("p r"); - }; - } + return () => { + Mousetrap.unbind("p r"); + }; + } - async function viewRandom( - result: GQL.FindGroupsQueryResult, - filter: ListFilterModel - ) { - // query for a random image - if (result.data?.findGroups) { - const { count } = result.data.findGroups; + async function viewRandom( + result: GQL.FindGroupsQueryResult, + filter: ListFilterModel + ) { + // query for a random image + if (result.data?.findGroups) { + const { count } = result.data.findGroups; - const index = Math.floor(Math.random() * count); - const filterCopy = cloneDeep(filter); - filterCopy.itemsPerPage = 1; - filterCopy.currentPage = index + 1; - const singleResult = await queryFindGroups(filterCopy); - if (singleResult.data.findGroups.groups.length === 1) { - const { id } = singleResult.data.findGroups.groups[0]; - // navigate to the group page - history.push(`/groups/${id}`); + const index = Math.floor(Math.random() * count); + const filterCopy = cloneDeep(filter); + filterCopy.itemsPerPage = 1; + filterCopy.currentPage = index + 1; + const singleResult = await queryFindGroups(filterCopy); + if (singleResult.data.findGroups.groups.length === 1) { + const { id } = singleResult.data.findGroups.groups[0]; + // navigate to the group page + history.push(`/groups/${id}`); + } } } - } - async function onExport() { - setIsExportAll(false); - setIsExportDialogOpen(true); - } + async function onExport() { + setIsExportAll(false); + setIsExportDialogOpen(true); + } - async function onExportAll() { - setIsExportAll(true); - setIsExportDialogOpen(true); - } + async function onExportAll() { + setIsExportAll(true); + setIsExportDialogOpen(true); + } - function renderContent( - result: GQL.FindGroupsQueryResult, - filter: ListFilterModel, - selectedIds: Set, - onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void - ) { - return ( - <> - setIsExportDialogOpen(false)} - /> - {filter.displayMode === DisplayMode.Grid && ( - , + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void + ) { + return ( + <> + setIsExportDialogOpen(false)} /> - )} - - ); - } + {filter.displayMode === DisplayMode.Grid && ( + + )} + + ); + } - function renderEditDialog( - selectedGroups: GQL.GroupDataFragment[], - onClose: (applied: boolean) => void - ) { - return ; - } + function renderEditDialog( + selectedGroups: GQL.ListGroupDataFragment[], + onClose: (applied: boolean) => void + ) { + return ; + } + + function renderDeleteDialog( + selectedGroups: GQL.SlimGroupDataFragment[], + onClose: (confirmed: boolean) => void + ) { + return ( + + ); + } - function renderDeleteDialog( - selectedGroups: GQL.SlimGroupDataFragment[], - onClose: (confirmed: boolean) => void - ) { return ( - + + + ); } - - return ( - - - - ); -}; +); diff --git a/ui/v2.5/src/components/Groups/GroupRecommendationRow.tsx b/ui/v2.5/src/components/Groups/GroupRecommendationRow.tsx index 3a8fee856..228cb3467 100644 --- a/ui/v2.5/src/components/Groups/GroupRecommendationRow.tsx +++ b/ui/v2.5/src/components/Groups/GroupRecommendationRow.tsx @@ -7,6 +7,7 @@ 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 { PatchComponent } from "src/patch"; interface IProps { isTouch: boolean; @@ -14,38 +15,44 @@ interface IProps { header: string; } -export const GroupRecommendationRow: React.FC = (props: IProps) => { - const result = useFindGroups(props.filter); - const cardCount = result.data?.findGroups.count; +export const GroupRecommendationRow: React.FC = PatchComponent( + "GroupRecommendationRow", + (props: IProps) => { + const result = useFindGroups(props.filter); + const cardCount = result.data?.findGroups.count; - if (!result.loading && !cardCount) { - return null; - } + if (!result.loading && !cardCount) { + return null; + } - return ( - - - - } - > - + + + } > - {result.loading - ? [...Array(props.filter.itemsPerPage)].map((i) => ( -
    - )) - : result.data?.findGroups.groups.map((g) => ( - - ))} -
    -
    - ); -}; + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
    + )) + : result.data?.findGroups.groups.map((g) => ( + + ))} +
    + + ); + } +); diff --git a/ui/v2.5/src/components/Groups/GroupSelect.tsx b/ui/v2.5/src/components/Groups/GroupSelect.tsx index dd16088e9..a904e2223 100644 --- a/ui/v2.5/src/components/Groups/GroupSelect.tsx +++ b/ui/v2.5/src/components/Groups/GroupSelect.tsx @@ -13,7 +13,7 @@ import { queryFindGroupsByIDForSelect, useGroupCreate, } from "src/core/StashService"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { useIntl } from "react-intl"; import { defaultMaxOptionsShown } from "src/core/config"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -66,12 +66,12 @@ export const GroupSelect: React.FC< > = PatchComponent("GroupSelect", (props) => { const [createGroup] = useGroupCreate(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const intl = useIntl(); const maxOptionsShown = configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; const defaultCreatable = - !configuration?.interface.disableDropdownCreate.movie ?? true; + !configuration?.interface.disableDropdownCreate.movie; const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]); diff --git a/ui/v2.5/src/components/Groups/RelatedGroupPopover.tsx b/ui/v2.5/src/components/Groups/RelatedGroupPopover.tsx index 03095f284..a2eea9975 100644 --- a/ui/v2.5/src/components/Groups/RelatedGroupPopover.tsx +++ b/ui/v2.5/src/components/Groups/RelatedGroupPopover.tsx @@ -16,7 +16,7 @@ import { GroupTag } from "./GroupTag"; interface IProps { group: Pick< - GQL.GroupDataFragment, + GQL.ListGroupDataFragment, "id" | "name" | "containing_groups" | "sub_group_count" >; } diff --git a/ui/v2.5/src/components/Help/Manual.tsx b/ui/v2.5/src/components/Help/Manual.tsx index d8fc1dbed..e90e2e5ac 100644 --- a/ui/v2.5/src/components/Help/Manual.tsx +++ b/ui/v2.5/src/components/Help/Manual.tsx @@ -23,6 +23,7 @@ import Interactive from "src/docs/en/Manual/Interactive.md"; import Captions from "src/docs/en/Manual/Captions.md"; import Identify from "src/docs/en/Manual/Identify.md"; import Browsing from "src/docs/en/Manual/Browsing.md"; +import TroubleshootingMode from "src/docs/en/Manual/TroubleshootingMode.md"; import { MarkdownPage } from "../Shared/MarkdownPage"; interface IManualProps { @@ -152,6 +153,11 @@ export const Manual: React.FC = ({ title: "Keyboard Shortcuts", content: KeyboardShortcuts, }, + { + key: "TroubleshootingMode.md", + title: "Troubleshooting Mode", + content: TroubleshootingMode, + }, { key: "Contributing.md", title: "Contributing", diff --git a/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx b/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx index 36a3ead3c..d57c60ab4 100644 --- a/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx +++ b/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx @@ -4,7 +4,7 @@ import { useImagesDestroy } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; import { ModalComponent } from "src/components/Shared/Modal"; import { useToast } from "src/hooks/Toast"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { FormattedMessage, useIntl } from "react-intl"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; @@ -33,7 +33,7 @@ export const DeleteImagesDialog: React.FC = ( { count: props.selected.length, singularEntity, pluralEntity } ); - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const [deleteFile, setDeleteFile] = useState( config?.defaults.deleteFile ?? false @@ -80,6 +80,11 @@ export const DeleteImagesDialog: React.FC = ( deletedFiles.push(...paths); }); + const deleteTrashPath = config?.general.deleteTrashPath; + const deleteAlertId = deleteTrashPath + ? "dialogs.delete_alert_to_trash" + : "dialogs.delete_alert"; + return (

    @@ -89,7 +94,7 @@ export const DeleteImagesDialog: React.FC = ( singularEntity: intl.formatMessage({ id: "file" }), pluralEntity: intl.formatMessage({ id: "files" }), }} - id="dialogs.delete_alert" + id={deleteAlertId} />

      diff --git a/ui/v2.5/src/components/Images/ImageCard.tsx b/ui/v2.5/src/components/Images/ImageCard.tsx index d530b253e..adaee9923 100644 --- a/ui/v2.5/src/components/Images/ImageCard.tsx +++ b/ui/v2.5/src/components/Images/ImageCard.tsx @@ -5,7 +5,6 @@ import * as GQL from "src/core/generated-graphql"; import { Icon } from "src/components/Shared/Icon"; import { GalleryLink, TagLink } from "src/components/Shared/TagLink"; import { HoverPopover } from "src/components/Shared/HoverPopover"; -import { SweatDrops } from "src/components/Shared/SweatDrops"; import { PerformerPopoverButton } from "src/components/Shared/PerformerPopoverButton"; import { GridCard } from "src/components/Shared/GridCard/GridCard"; import { RatingBanner } from "src/components/Shared/RatingBanner"; @@ -16,8 +15,10 @@ import { faTag, } from "@fortawesome/free-solid-svg-icons"; import { imageTitle } from "src/core/files"; +import { PatchComponent } from "src/patch"; import { TruncatedText } from "../Shared/TruncatedText"; import { StudioOverlay } from "../Shared/GridCard/StudioOverlay"; +import { OCounterButton } from "../Shared/CountButton"; interface IImageCardProps { image: GQL.SlimImageDataFragment; @@ -29,98 +30,80 @@ interface IImageCardProps { onPreview?: (ev: MouseEvent) => void; } -export const ImageCard: React.FC = ( - props: IImageCardProps -) => { - const file = useMemo( - () => - props.image.visual_files.length > 0 - ? props.image.visual_files[0] - : undefined, - [props.image] - ); +const ImageCardPopovers = PatchComponent( + "ImageCard.Popovers", + (props: IImageCardProps) => { + function maybeRenderTagPopoverButton() { + if (props.image.tags.length <= 0) return; - function maybeRenderTagPopoverButton() { - if (props.image.tags.length <= 0) return; + const popoverContent = props.image.tags.map((tag) => ( + + )); - const popoverContent = props.image.tags.map((tag) => ( - - )); - - return ( - - - - ); - } - - function maybeRenderPerformerPopoverButton() { - if (props.image.performers.length <= 0) return; - - return ( - - ); - } - - function maybeRenderOCounter() { - if (props.image.o_counter) { return ( -
      + -
      + ); } - } - function maybeRenderGallery() { - if (props.image.galleries.length <= 0) return; + function maybeRenderPerformerPopoverButton() { + if (props.image.performers.length <= 0) return; - const popoverContent = props.image.galleries.map((gallery) => ( - - )); - - return ( - - - - ); - } - - function maybeRenderOrganized() { - if (props.image.organized) { return ( -
      - -
      + ); } - } - function maybeRenderPopoverButtonGroup() { + function maybeRenderOCounter() { + if (props.image.o_counter) { + return ; + } + } + + function maybeRenderGallery() { + if (props.image.galleries.length <= 0) return; + + const popoverContent = props.image.galleries.map((gallery) => ( + + )); + + return ( + + + + ); + } + + function maybeRenderOrganized() { + if (props.image.organized) { + return ( +
      + +
      + ); + } + } + if ( props.image.tags.length > 0 || props.image.performers.length > 0 || @@ -141,64 +124,101 @@ export const ImageCard: React.FC = ( ); } + + return null; } +); - function isPortrait() { - const width = file?.width ? file.width : 0; - const height = file?.height ? file.height : 0; - return height > width; +const ImageCardDetails = PatchComponent( + "ImageCard.Details", + (props: IImageCardProps) => { + return ( +
      + {props.image.date} + +
      + ); } +); - const source = - props.image.paths.preview != "" - ? props.image.paths.preview ?? "" - : props.image.paths.thumbnail ?? ""; - const video = source.includes("preview"); - const ImagePreview = video ? "video" : "img"; +const ImageCardOverlays = PatchComponent( + "ImageCard.Overlays", + (props: IImageCardProps) => { + return ; + } +); - return ( - -
      - - {props.onPreview ? ( -
      - -
      - ) : undefined} -
      - - - } - details={ -
      - {props.image.date} - { + const file = useMemo( + () => + props.image.visual_files.length > 0 + ? props.image.visual_files[0] + : undefined, + [props.image] + ); + + function isPortrait() { + const width = file?.width ? file.width : 0; + const height = file?.height ? file.height : 0; + return height > width; + } + + const source = + props.image.paths.preview != "" + ? props.image.paths.preview ?? "" + : props.image.paths.thumbnail ?? ""; + const video = source.includes("preview"); + const ImagePreview = video ? "video" : "img"; + + return ( + <> +
      + + {props.onPreview ? ( +
      + +
      + ) : undefined}
      - } - overlays={} - popovers={maybeRenderPopoverButtonGroup()} - selected={props.selected} - selecting={props.selecting} - onSelectedChanged={props.onSelectedChanged} - /> - ); -}; + + + ); + } +); + +export const ImageCard: React.FC = PatchComponent( + "ImageCard", + (props: IImageCardProps) => { + return ( + } + details={} + overlays={} + popovers={} + selected={props.selected} + selecting={props.selecting} + onSelectedChanged={props.onSelectedChanged} + /> + ); + } +); diff --git a/ui/v2.5/src/components/Images/ImageCardGrid.tsx b/ui/v2.5/src/components/Images/ImageCardGrid.tsx new file mode 100644 index 000000000..dadab571b --- /dev/null +++ b/ui/v2.5/src/components/Images/ImageCardGrid.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import * as GQL from "src/core/generated-graphql"; +import { ImageCard } from "./ImageCard"; +import { + useCardWidth, + useContainerDimensions, +} from "../Shared/GridCard/GridCard"; +import { PatchComponent } from "src/patch"; + +interface IImageCardGrid { + images: GQL.SlimImageDataFragment[]; + selectedIds: Set; + zoomIndex: number; + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; + onPreview: (index: number, ev: React.MouseEvent) => void; +} + +const zoomWidths = [280, 340, 480, 640]; + +export const ImageCardGrid: React.FC = PatchComponent( + "ImageCardGrid", + ({ images, selectedIds, zoomIndex, onSelectChange, onPreview }) => { + const [componentRef, { width: containerWidth }] = useContainerDimensions(); + const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); + + return ( +
      + {images.map((image, index) => ( + 0} + selected={selectedIds.has(image.id)} + onSelectedChanged={(selected: boolean, shiftKey: boolean) => + onSelectChange(image.id, selected, shiftKey) + } + onPreview={ + selectedIds.size < 1 ? (ev) => onPreview(index, ev) : undefined + } + /> + ))} +
      + ); + } +); diff --git a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx index 4ab6641d7..f79d95fca 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx @@ -1,6 +1,6 @@ import { Tab, Nav, Dropdown } from "react-bootstrap"; -import React, { useContext, useEffect, useMemo, useState } from "react"; -import { FormattedDate, FormattedMessage, useIntl } from "react-intl"; +import React, { useEffect, useMemo, useState } from "react"; +import { FormattedMessage, useIntl } from "react-intl"; import { useHistory, Link, RouteComponentProps } from "react-router-dom"; import { Helmet } from "react-helmet"; import { @@ -29,11 +29,14 @@ import { imagePath, imageTitle } from "src/core/files"; import { isVideo } from "src/utils/visualFile"; import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { useRatingKeybinds } from "src/hooks/keybinds"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import TextUtils from "src/utils/text"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import cx from "classnames"; import { TruncatedText } from "src/components/Shared/TruncatedText"; +import { goBackOrReplace } from "src/utils/history"; +import { FormattedDate } from "src/components/Shared/Date"; +import { GenerateDialog } from "src/components/Dialogs/GenerateDialog"; interface IProps { image: GQL.ImageDataFragment; @@ -47,7 +50,7 @@ const ImagePage: React.FC = ({ image }) => { const history = useHistory(); const Toast = useToast(); const intl = useIntl(); - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const [incrementO] = useImageIncrementO(image.id); const [decrementO] = useImageDecrementO(image.id); @@ -60,6 +63,7 @@ const ImagePage: React.FC = ({ image }) => { const [activeTabKey, setActiveTabKey] = useState("image-details-panel"); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); + const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false); async function onSave(input: GQL.ImageUpdateInput) { await updateImage({ @@ -156,7 +160,7 @@ const ImagePage: React.FC = ({ image }) => { function onDeleteDialogClosed(deleted: boolean) { setIsDeleteAlertOpen(false); if (deleted) { - history.goBack(); + goBackOrReplace(history, "/images"); } } @@ -168,6 +172,20 @@ const ImagePage: React.FC = ({ image }) => { } } + function maybeRenderSceneGenerateDialog() { + if (isGenerateDialogOpen) { + return ( + { + setIsGenerateDialogOpen(false); + }} + type="image" + /> + ); + } + } + function renderOperations() { return ( @@ -187,6 +205,13 @@ const ImagePage: React.FC = ({ image }) => { > + setIsGenerateDialogOpen(true)} + > + … + = ({ image }) => { {maybeRenderDeleteDialog()} + {maybeRenderSceneGenerateDialog()}
      @@ -318,13 +344,7 @@ const ImagePage: React.FC = ({ image }) => {
      - {!!image.date && ( - - )} + {!!image.date && } {resolution ? ( @@ -369,6 +389,7 @@ const ImagePage: React.FC = ({ image }) => { = ({ xl: 12, }, }; + const urlProps = isNew + ? splitProps + : { + labelProps: { + column: true, + md: 3, + lg: 12, + }, + fieldProps: { + md: 9, + lg: 12, + }, + }; const { renderField, renderInputField, renderDateField, renderURLListField } = formikUtils(intl, formik, splitProps); @@ -461,7 +474,13 @@ export const ImageEditPanel: React.FC = ({ {renderInputField("title")} {renderInputField("code", "text", "scene_code")} - {renderURLListField("urls", onScrapeImageURL, urlScrapable)} + {renderURLListField( + "urls", + onScrapeImageURL, + urlScrapable, + "urls", + urlProps + )} {renderDateField("date")} {renderInputField("photographer")} diff --git a/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx b/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx index 4e566a626..f247e062b 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx @@ -9,6 +9,7 @@ import { useToast } from "src/hooks/Toast"; import TextUtils from "src/utils/text"; import { TextField, URLField, URLsField } from "src/utils/field"; import { FileSize } from "src/components/Shared/FileSize"; +import NavUtils from "src/utils/navigation"; interface IFileInfoPanelProps { file: GQL.ImageFileDataFragment | GQL.VideoFileDataFragment; @@ -23,6 +24,7 @@ const FileInfoPanel: React.FC = ( props: IFileInfoPanelProps ) => { const checksum = props.file.fingerprints.find((f) => f.type === "md5"); + const phash = props.file.fingerprints.find((f) => f.type === "phash"); return (
      @@ -36,6 +38,15 @@ const FileInfoPanel: React.FC = ( )} + = ({ scraped.performers?.filter((t) => !t.stored_id) ?? [] ); - const { tags, newTags, scrapedTagsRow } = useScrapedTags( + const { tags, newTags, scrapedTagsRow, linkDialog } = useScrapedTags( imageTags, scraped.tags ); @@ -163,32 +163,38 @@ export const ImageScrapeDialog: React.FC = ({ return ( <> setTitle(value)} /> setCode(value)} /> setURLs(value)} /> setDate(value)} /> setPhotographer(value)} /> setStudio(value)} @@ -196,6 +202,7 @@ export const ImageScrapeDialog: React.FC = ({ onCreateNew={createNewStudio} /> setPerformers(value)} @@ -204,6 +211,7 @@ export const ImageScrapeDialog: React.FC = ({ /> {scrapedTagsRow} setDetails(value)} @@ -212,16 +220,21 @@ export const ImageScrapeDialog: React.FC = ({ ); } + if (linkDialog) { + return linkDialog; + } + return ( { onClose(apply ? makeNewScrapedItem() : undefined); }} - /> + > + {renderScrapeRows()} + ); }; diff --git a/ui/v2.5/src/components/Images/ImageGridCard.tsx b/ui/v2.5/src/components/Images/ImageGridCard.tsx deleted file mode 100644 index cbb76d853..000000000 --- a/ui/v2.5/src/components/Images/ImageGridCard.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from "react"; -import * as GQL from "src/core/generated-graphql"; -import { ImageCard } from "./ImageCard"; -import { - useCardWidth, - useContainerDimensions, -} from "../Shared/GridCard/GridCard"; - -interface IImageCardGrid { - images: GQL.SlimImageDataFragment[]; - selectedIds: Set; - zoomIndex: number; - onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; - onPreview: (index: number, ev: React.MouseEvent) => void; -} - -const zoomWidths = [280, 340, 480, 640]; - -export const ImageGridCard: React.FC = ({ - images, - selectedIds, - zoomIndex, - onSelectChange, - onPreview, -}) => { - const [componentRef, { width: containerWidth }] = useContainerDimensions(); - const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); - - return ( -
      - {images.map((image, index) => ( - 0} - selected={selectedIds.has(image.id)} - onSelectedChanged={(selected: boolean, shiftKey: boolean) => - onSelectChange(image.id, selected, shiftKey) - } - onPreview={ - selectedIds.size < 1 ? (ev) => onPreview(index, ev) : undefined - } - /> - ))} -
      - ); -}; diff --git a/ui/v2.5/src/components/Images/ImageList.tsx b/ui/v2.5/src/components/Images/ImageList.tsx index a468c2815..eee789bb1 100644 --- a/ui/v2.5/src/components/Images/ImageList.tsx +++ b/ui/v2.5/src/components/Images/ImageList.tsx @@ -1,16 +1,14 @@ -import React, { - useCallback, - useState, - useMemo, - MouseEvent, - useContext, -} from "react"; +import React, { useCallback, useState, useMemo, MouseEvent } from "react"; import { FormattedNumber, useIntl } from "react-intl"; import cloneDeep from "lodash-es/cloneDeep"; import { useHistory } from "react-router-dom"; import Mousetrap from "mousetrap"; import * as GQL from "src/core/generated-graphql"; -import { queryFindImages, useFindImages } from "src/core/StashService"; +import { + queryFindImages, + useFindImages, + useFindImagesMetadata, +} from "src/core/StashService"; import { ItemList, ItemListContext, showWhenSelected } from "../List/ItemList"; import { useLightbox } from "src/hooks/Lightbox/hooks"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -20,14 +18,17 @@ import { ImageWallItem } from "./ImageWallItem"; import { EditImagesDialog } from "./EditImagesDialog"; import { DeleteImagesDialog } from "./DeleteImagesDialog"; import "flexbin/flexbin.css"; -import Gallery from "react-photo-gallery"; +import Gallery, { RenderImageProps } from "react-photo-gallery"; import { ExportDialog } from "../Shared/ExportDialog"; import { objectTitle } from "src/core/files"; -import { ConfigurationContext } from "src/hooks/Config"; -import { ImageGridCard } from "./ImageGridCard"; +import { useConfigurationContext } from "src/hooks/Config"; +import { ImageCardGrid } from "./ImageCardGrid"; import { View } from "../List/views"; import { IItemListOperation } from "../List/FilteredListToolbar"; import { FileSize } from "../Shared/FileSize"; +import { PatchComponent } from "src/patch"; +import { GenerateDialog } from "../Dialogs/GenerateDialog"; +import { useModal } from "src/hooks/modal"; interface IImageWallProps { images: GQL.SlimImageDataFragment[]; @@ -36,6 +37,9 @@ interface IImageWallProps { pageCount: number; handleImageOpen: (index: number) => void; zoomIndex: number; + selectedIds?: Set; + onSelectChange?: (id: string, selected: boolean, shiftKey: boolean) => void; + selecting?: boolean; } const zoomWidths = [280, 340, 480, 640]; @@ -50,10 +54,15 @@ const ImageWall: React.FC = ({ images, zoomIndex, handleImageOpen, + selectedIds, + onSelectChange, + selecting, }) => { - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const uiConfig = configuration?.ui; + const containerRef = React.useRef(null); + let photos: { src: string; srcSet?: string | string[] | undefined; @@ -70,8 +79,8 @@ const ImageWall: React.FC = ({ image.paths.preview != "" ? image.paths.preview! : image.paths.thumbnail!, - width: image.visual_files[0].width, - height: image.visual_files[0].height, + width: image.visual_files?.[0]?.width ?? 0, + height: image.visual_files?.[0]?.height ?? 0, tabIndex: index, key: image.id, loading: "lazy", @@ -94,22 +103,60 @@ const ImageWall: React.FC = ({ return Math.round(columnCount); } - function targetRowHeight(containerWidth: number) { - let zoomHeight = 280; - breakpointZoomHeights.forEach((e) => { - if (containerWidth >= e.minWidth) { - zoomHeight = e.heights[zoomIndex]; + const targetRowHeight = useCallback( + (containerWidth: number) => { + let zoomHeight = 280; + breakpointZoomHeights.forEach((e) => { + if (containerWidth >= e.minWidth) { + zoomHeight = e.heights[zoomIndex]; + } + }); + return zoomHeight; + }, + [zoomIndex] + ); + + // set the max height as a factor of the targetRowHeight + // this allows some images to be taller than the target row height + // but prevents images from becoming too tall when there is a small number of items + const maxHeightFactor = 1.3; + + const renderImage = useCallback( + (props: RenderImageProps) => { + // #6165 - only use targetRowHeight in row direction + const maxHeight = + props.direction === "column" + ? props.photo.height + : targetRowHeight(containerRef.current?.offsetWidth ?? 0) * + maxHeightFactor; + const imageId = props.photo.key; + if (!imageId) { + return null; } - }); - return zoomHeight; - } + return ( + + onSelectChange(imageId, selected, shiftKey) + : undefined + } + selecting={selecting} + /> + ); + }, + [targetRowHeight, selectedIds, onSelectChange, selecting] + ); return ( -
      +
      {photos.length ? ( = ({ if (filter.displayMode === DisplayMode.Grid) { return ( - = ({ pageCount={pageCount} handleImageOpen={handleImageOpen} zoomIndex={filter.zoomIndex} + selectedIds={selectedIds} + onSelectChange={onSelectChange} + selecting={!!selectedIds && selectedIds.size > 0} /> ); } @@ -252,9 +302,17 @@ function getCount(result: GQL.FindImagesQueryResult) { return result?.data?.findImages?.count ?? 0; } -function renderMetadataByline(result: GQL.FindImagesQueryResult) { - const megapixels = result?.data?.findImages?.megapixels; - const size = result?.data?.findImages?.filesize; +function renderMetadataByline( + result: GQL.FindImagesQueryResult, + metadataInfo?: GQL.FindImagesMetadataQueryResult +) { + const megapixels = metadataInfo?.data?.findImages?.megapixels; + const size = metadataInfo?.data?.findImages?.filesize; + + if (metadataInfo?.loading) { + // return ellipsis + return  (...); + } if (!megapixels && !size) { return; @@ -289,167 +347,185 @@ interface IImageList { chapters?: GQL.GalleryChapterDataFragment[]; } -export const ImageList: React.FC = ({ - filterHook, - view, - alterQuery, - extraOperations, - chapters = [], -}) => { - const intl = useIntl(); - const history = useHistory(); - const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); - const [isExportAll, setIsExportAll] = useState(false); - const [slideshowRunning, setSlideshowRunning] = useState(false); +export const ImageList: React.FC = PatchComponent( + "ImageList", + ({ filterHook, view, alterQuery, extraOperations = [], chapters = [] }) => { + const intl = useIntl(); + const history = useHistory(); + const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); + const [isExportAll, setIsExportAll] = useState(false); + const [slideshowRunning, setSlideshowRunning] = useState(false); - const filterMode = GQL.FilterMode.Images; + const filterMode = GQL.FilterMode.Images; - const otherOperations = [ - ...(extraOperations ?? []), - { - text: intl.formatMessage({ id: "actions.view_random" }), - onClick: viewRandom, - }, - { - text: intl.formatMessage({ id: "actions.export" }), - onClick: onExport, - isDisplayed: showWhenSelected, - }, - { - text: intl.formatMessage({ id: "actions.export_all" }), - onClick: onExportAll, - }, - ]; + const { modal, showModal, closeModal } = useModal(); - function addKeybinds( - result: GQL.FindImagesQueryResult, - filter: ListFilterModel - ) { - Mousetrap.bind("p r", () => { - viewRandom(result, filter); - }); + const otherOperations: IItemListOperation[] = [ + ...extraOperations, + { + text: intl.formatMessage({ id: "actions.view_random" }), + onClick: viewRandom, + }, + { + text: `${intl.formatMessage({ id: "actions.generate" })}…`, + onClick: (result, filter, selectedIds) => { + showModal( + closeModal()} + /> + ); + return Promise.resolve(); + }, + isDisplayed: showWhenSelected, + }, + { + text: intl.formatMessage({ id: "actions.export" }), + onClick: onExport, + isDisplayed: showWhenSelected, + }, + { + text: intl.formatMessage({ id: "actions.export_all" }), + onClick: onExportAll, + }, + ]; - return () => { - Mousetrap.unbind("p r"); - }; - } + function addKeybinds( + result: GQL.FindImagesQueryResult, + filter: ListFilterModel + ) { + Mousetrap.bind("p r", () => { + viewRandom(result, filter); + }); - async function viewRandom( - result: GQL.FindImagesQueryResult, - filter: ListFilterModel - ) { - // query for a random image - if (result.data?.findImages) { - const { count } = result.data.findImages; + return () => { + Mousetrap.unbind("p r"); + }; + } - const index = Math.floor(Math.random() * count); - const filterCopy = cloneDeep(filter); - filterCopy.itemsPerPage = 1; - filterCopy.currentPage = index + 1; - const singleResult = await queryFindImages(filterCopy); - if (singleResult.data.findImages.images.length === 1) { - const { id } = singleResult.data.findImages.images[0]; - // navigate to the image player page - history.push(`/images/${id}`); + async function viewRandom( + result: GQL.FindImagesQueryResult, + filter: ListFilterModel + ) { + // query for a random image + if (result.data?.findImages) { + const { count } = result.data.findImages; + + const index = Math.floor(Math.random() * count); + const filterCopy = cloneDeep(filter); + filterCopy.itemsPerPage = 1; + filterCopy.currentPage = index + 1; + const singleResult = await queryFindImages(filterCopy); + if (singleResult.data.findImages.images.length === 1) { + const { id } = singleResult.data.findImages.images[0]; + // navigate to the image player page + history.push(`/images/${id}`); + } } } - } - async function onExport() { - setIsExportAll(false); - setIsExportDialogOpen(true); - } + async function onExport() { + setIsExportAll(false); + setIsExportDialogOpen(true); + } - async function onExportAll() { - setIsExportAll(true); - setIsExportDialogOpen(true); - } + async function onExportAll() { + setIsExportAll(true); + setIsExportDialogOpen(true); + } + + function renderContent( + result: GQL.FindImagesQueryResult, + filter: ListFilterModel, + selectedIds: Set, + onSelectChange: ( + id: string, + selected: boolean, + shiftKey: boolean + ) => void, + onChangePage: (page: number) => void, + pageCount: number + ) { + function maybeRenderImageExportDialog() { + if (isExportDialogOpen) { + return ( + setIsExportDialogOpen(false)} + /> + ); + } + } + + function renderImages() { + if (!result.data?.findImages) return; - function renderContent( - result: GQL.FindImagesQueryResult, - filter: ListFilterModel, - selectedIds: Set, - onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void, - onChangePage: (page: number) => void, - pageCount: number - ) { - function maybeRenderImageExportDialog() { - if (isExportDialogOpen) { return ( - setIsExportDialogOpen(false)} + ); } - } - - function renderImages() { - if (!result.data?.findImages) return; return ( - + <> + {maybeRenderImageExportDialog()} + {renderImages()} + ); } + function renderEditDialog( + selectedImages: GQL.SlimImageDataFragment[], + onClose: (applied: boolean) => void + ) { + return ; + } + + function renderDeleteDialog( + selectedImages: GQL.SlimImageDataFragment[], + onClose: (confirmed: boolean) => void + ) { + return ; + } + return ( - <> - {maybeRenderImageExportDialog()} - {renderImages()} - + + {modal} + + ); } - - function renderEditDialog( - selectedImages: GQL.SlimImageDataFragment[], - onClose: (applied: boolean) => void - ) { - return ; - } - - function renderDeleteDialog( - selectedImages: GQL.SlimImageDataFragment[], - onClose: (confirmed: boolean) => void - ) { - return ; - } - - return ( - - - - ); -}; +); diff --git a/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx b/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx index f0fc84493..6499be894 100644 --- a/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx +++ b/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx @@ -7,6 +7,7 @@ import { getSlickSliderSettings } from "src/core/recommendations"; import { RecommendationRow } from "../FrontPage/RecommendationRow"; import { FormattedMessage } from "react-intl"; import { ImageCard } from "./ImageCard"; +import { PatchComponent } from "src/patch"; interface IProps { isTouch: boolean; @@ -14,38 +15,44 @@ interface IProps { header: string; } -export const ImageRecommendationRow: React.FC = (props: IProps) => { - const result = useFindImages(props.filter); - const cardCount = result.data?.findImages.count; +export const ImageRecommendationRow: React.FC = PatchComponent( + "ImageRecommendationRow", + (props: IProps) => { + const result = useFindImages(props.filter); + const cardCount = result.data?.findImages.count; - if (!result.loading && !cardCount) { - return null; - } + if (!result.loading && !cardCount) { + return null; + } - return ( - - - - } - > - + + + } > - {result.loading - ? [...Array(props.filter.itemsPerPage)].map((i) => ( -
      - )) - : result.data?.findImages.images.map((i) => ( - - ))} -
      -
      - ); -}; + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
      + )) + : result.data?.findImages.images.map((i) => ( + + ))} +
      + + ); + } +); diff --git a/ui/v2.5/src/components/Images/ImageWallItem.tsx b/ui/v2.5/src/components/Images/ImageWallItem.tsx index 8403b3a98..a9f681474 100644 --- a/ui/v2.5/src/components/Images/ImageWallItem.tsx +++ b/ui/v2.5/src/components/Images/ImageWallItem.tsx @@ -1,38 +1,50 @@ import React from "react"; -import type { - RenderImageProps, - renderImageClickHandler, - PhotoProps, -} from "react-photo-gallery"; +import { Form } from "react-bootstrap"; +import type { RenderImageProps } from "react-photo-gallery"; +import { useDragMoveSelect } from "../Shared/GridCard/dragMoveSelect"; -interface IImageWallProps { - margin?: string; - index: number; - photo: PhotoProps; - onClick: renderImageClickHandler | null; - direction: "row" | "column"; - top?: number; - left?: number; +interface IExtraProps { + maxHeight: number; + selected?: boolean; + onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void; + selecting?: boolean; } -export const ImageWallItem: React.FC = ( - props: IImageWallProps +export const ImageWallItem: React.FC = ( + props: RenderImageProps & IExtraProps ) => { + const { dragProps } = useDragMoveSelect({ + selecting: props.selecting || false, + selected: props.selected || false, + onSelectedChanged: props.onSelectedChanged, + }); + + const height = Math.min(props.maxHeight, props.photo.height); + const zoomFactor = height / props.photo.height; + const width = props.photo.width * zoomFactor; + type style = Record; - var imgStyle: style = { + var divStyle: style = { margin: props.margin, display: "block", + position: "relative", }; if (props.direction === "column") { - imgStyle.position = "absolute"; - imgStyle.left = props.left; - imgStyle.top = props.top; + divStyle.position = "absolute"; + divStyle.left = props.left; + divStyle.top = props.top; } var handleClick = function handleClick( event: React.MouseEvent ) { + if (props.selecting && props.onSelectedChanged) { + props.onSelectedChanged(!props.selected, event.shiftKey); + event.preventDefault(); + event.stopPropagation(); + return; + } if (props.onClick) { props.onClick(event, { index: props.index }); } @@ -41,18 +53,39 @@ export const ImageWallItem: React.FC = ( const video = props.photo.src.includes("preview"); const ImagePreview = video ? "video" : "img"; + let shiftKey = false; + return ( - + {...dragProps} + > + {props.onSelectedChanged && ( + props.onSelectedChanged!(!props.selected, shiftKey)} + onClick={(event: React.MouseEvent) => { + shiftKey = event.shiftKey; + event.stopPropagation(); + }} + /> + )} + +
      ); }; diff --git a/ui/v2.5/src/components/Images/styles.scss b/ui/v2.5/src/components/Images/styles.scss index 936947bc3..0050a9434 100644 --- a/ui/v2.5/src/components/Images/styles.scss +++ b/ui/v2.5/src/components/Images/styles.scss @@ -86,6 +86,7 @@ } &-preview { + align-items: center; display: flex; justify-content: center; margin-bottom: 5px; @@ -94,7 +95,6 @@ &-image { height: 100%; object-fit: contain; - object-position: top; width: 100%; } @@ -175,6 +175,10 @@ $imageTabWidth: 450px; font-size: 1.3em; height: calc(1.5em + 0.75rem + 2px); } + + .form-group[data-field="urls"] .string-list-input input.form-control { + font-size: 0.85em; + } } .image-file-card.card { diff --git a/ui/v2.5/src/components/List/EditFilterDialog.tsx b/ui/v2.5/src/components/List/EditFilterDialog.tsx index 4b31ac31a..3f0f486b8 100644 --- a/ui/v2.5/src/components/List/EditFilterDialog.tsx +++ b/ui/v2.5/src/components/List/EditFilterDialog.tsx @@ -1,7 +1,6 @@ import cloneDeep from "lodash-es/cloneDeep"; import React, { useCallback, - useContext, useEffect, useMemo, useRef, @@ -14,7 +13,7 @@ import { CriterionOption, } from "src/models/list-filter/criteria/criterion"; import { FormattedMessage, useIntl } from "react-intl"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getFilterOptions } from "src/models/list-filter/factory"; import { FilterTags } from "./FilterTags"; @@ -29,11 +28,16 @@ import { import { useCompare, usePrevious } from "src/hooks/state"; import { CriterionType } from "src/models/list-filter/types"; import { useToast } from "src/hooks/Toast"; -import { useConfigureUI } from "src/core/StashService"; -import { FilterMode } from "src/core/generated-graphql"; +import { useConfigureUI, useSaveFilter } from "src/core/StashService"; +import { + FilterMode, + SavedFilterDataFragment, +} from "src/core/generated-graphql"; import { useFocusOnce } from "src/utils/focus"; import Mousetrap from "mousetrap"; import ScreenUtils from "src/utils/screen"; +import { LoadFilterDialog, SaveFilterDialog } from "./SavedFilterList"; +import { SearchTermInput } from "./ListFilter"; interface ICriterionList { criteria: string[]; @@ -45,6 +49,7 @@ interface ICriterionList { optionSelected: (o?: CriterionOption) => void; onRemoveCriterion: (c: string) => void; onTogglePin: (c: CriterionOption) => void; + externallySelected?: boolean; } const CriterionOptionList: React.FC = ({ @@ -57,7 +62,11 @@ const CriterionOptionList: React.FC = ({ optionSelected, onRemoveCriterion, onTogglePin, + externallySelected = false, }) => { + const { configuration } = useConfigurationContext(); + const { sfwContentMode } = configuration.interface; + const prevCriterion = usePrevious(currentCriterion); const scrolled = useRef(false); @@ -96,14 +105,19 @@ const CriterionOptionList: React.FC = ({ // scrolling to the current criterion doesn't work well when the // dialog is already open, so limit to when we click on the // criterion from the external tags - if (!scrolled.current && type && criteriaRefs[type]?.current) { + if ( + externallySelected && + !scrolled.current && + type && + criteriaRefs[type]?.current + ) { criteriaRefs[type].current!.scrollIntoView({ behavior: "smooth", block: "start", }); scrolled.current = true; } - }, [currentCriterion, criteriaRefs, type]); + }, [externallySelected, currentCriterion, criteriaRefs, type]); function getReleventCriterion(t: CriterionType) { if (currentCriterion?.criterionOption.type === t) { @@ -136,7 +150,9 @@ const CriterionOptionList: React.FC = ({ className="collapse-icon fa-fw" icon={type === c.type ? faChevronDown : faChevronRight} /> - + {criteria.some((cc) => c.type === cc) && ( - +
      + + +
      +
      + + +
      diff --git a/ui/v2.5/src/components/List/FilterTags.tsx b/ui/v2.5/src/components/List/FilterTags.tsx index 8d9b24e40..5597cae79 100644 --- a/ui/v2.5/src/components/List/FilterTags.tsx +++ b/ui/v2.5/src/components/List/FilterTags.tsx @@ -9,31 +9,38 @@ import { Badge, BadgeProps, Button, Overlay, Popover } from "react-bootstrap"; import { Criterion } from "src/models/list-filter/criteria/criterion"; import { FormattedMessage, useIntl } from "react-intl"; import { Icon } from "../Shared/Icon"; -import { faTimes } from "@fortawesome/free-solid-svg-icons"; +import { faMagnifyingGlass, faTimes } from "@fortawesome/free-solid-svg-icons"; import { BsPrefixProps, ReplaceProps } from "react-bootstrap/esm/helpers"; import { CustomFieldsCriterion } from "src/models/list-filter/criteria/custom-fields"; import { useDebounce } from "src/hooks/debounce"; +import cx from "classnames"; +import { useConfigurationContext } from "src/hooks/Config"; type TagItemProps = PropsWithChildren< ReplaceProps<"span", BsPrefixProps<"span"> & BadgeProps> >; export const TagItem: React.FC = (props) => { - const { children } = props; + const { className, children, ...others } = props; return ( - + {children} ); }; export const FilterTag: React.FC<{ + className?: string; label: React.ReactNode; onClick: React.MouseEventHandler; onRemove: React.MouseEventHandler; -}> = ({ label, onClick, onRemove }) => { +}> = ({ className, label, onClick, onRemove }) => { return ( - + {label} - )} -
      + if (searchTerm && searchTerm.length > 0) { + filterTags.unshift( + + + {searchTerm} + + } + onClick={() => onEditSearchTerm?.()} + onRemove={() => onRemoveSearchTerm?.()} + /> ); } + const visibleCriteria = + cutoff !== undefined ? filterTags.slice(0, cutoff) : filterTags; + const hiddenCriteria = cutoff !== undefined ? filterTags.slice(cutoff) : []; + return (
      - {filterTags} - {criteria.length >= 3 && ( + {visibleCriteria} + + {filterTags.length >= 3 && ( + {selected} + +
      + ); +}; export interface IItemListOperation { text: string; @@ -42,8 +78,8 @@ export interface IFilteredListToolbar { onEdit?: () => void; onDelete?: () => void; operations?: IListFilterOperation[]; + operationComponent?: React.ReactNode; zoomable?: boolean; - onToggleSidebar?: () => void; } export const FilteredListToolbar: React.FC = ({ @@ -55,63 +91,86 @@ export const FilteredListToolbar: React.FC = ({ onEdit, onDelete, operations, + operationComponent, zoomable = false, - onToggleSidebar, }) => { - const intl = useIntl(); const filterOptions = filter.options; const { setDisplayMode, setZoom } = useFilterOperations({ filter, setFilter, }); - const { selectedIds, onSelectAll, onSelectNone } = listSelect; + const { selectedIds, onSelectAll, onSelectNone, onInvertSelection } = + listSelect; + const hasSelection = selectedIds.size > 0; + + const renderOperations = operationComponent ?? ( + 0} + onEdit={onEdit} + onDelete={onDelete} + /> + ); return ( - - - {onToggleSidebar && ( - - - - )} - - - - {showEditFilter && ( - showEditFilter()} - view={view} - withSidebar={!!onToggleSidebar} - /> - )} - + {hasSelection ? ( + 0} - onEdit={onEdit} - onDelete={onDelete} /> - - + + + + + showEditFilter()} + count={filter.count()} + /> + + + setFilter(filter.setSortBy(e ?? undefined))} + onChangeSortDirection={() => + setFilter(filter.toggleSortDirection()) + } + onReshuffleRandomSort={() => + setFilter(filter.reshuffleRandomSort()) + } /> - - - + + setFilter(filter.setPageSize(size))} + /> + + )} + + {renderOperations} + + ); }; diff --git a/ui/v2.5/src/components/List/Filters/BooleanFilter.tsx b/ui/v2.5/src/components/List/Filters/BooleanFilter.tsx index 18df1b9f1..657e9ddbd 100644 --- a/ui/v2.5/src/components/List/Filters/BooleanFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/BooleanFilter.tsx @@ -54,6 +54,7 @@ interface ISidebarFilter { option: CriterionOption; filter: ListFilterModel; setFilter: (f: ListFilterModel) => void; + sectionID?: string; } export const SidebarBooleanFilter: React.FC = ({ @@ -61,6 +62,7 @@ export const SidebarBooleanFilter: React.FC = ({ option, filter, setFilter, + sectionID, }) => { const intl = useIntl(); @@ -127,6 +129,7 @@ export const SidebarBooleanFilter: React.FC = ({ onUnselect={onUnselect} selected={selected} singleValue + sectionID={sectionID} /> ); diff --git a/ui/v2.5/src/components/List/Filters/FilterSidebar.tsx b/ui/v2.5/src/components/List/Filters/FilterSidebar.tsx index 0ff201f86..6b6503993 100644 --- a/ui/v2.5/src/components/List/Filters/FilterSidebar.tsx +++ b/ui/v2.5/src/components/List/Filters/FilterSidebar.tsx @@ -10,20 +10,31 @@ import ScreenUtils from "src/utils/screen"; import Mousetrap from "mousetrap"; import { Button } from "react-bootstrap"; +const savedFiltersSectionID = "saved-filters"; + export const FilteredSidebarHeader: React.FC<{ sidebarOpen: boolean; showEditFilter: () => void; filter: ListFilterModel; setFilter: (filter: ListFilterModel) => void; view?: View; -}> = ({ sidebarOpen, showEditFilter, filter, setFilter, view }) => { - const focus = useFocus(); + focus?: ReturnType; +}> = ({ + sidebarOpen, + showEditFilter, + filter, + setFilter, + view, + focus: providedFocus, +}) => { + const localFocus = useFocus(); + const focus = providedFocus ?? localFocus; const [, setFocus] = focus; // Set the focus on the input field when the sidebar is opened - // Don't do this on mobile devices + // Don't do this on touch devices useEffect(() => { - if (sidebarOpen && !ScreenUtils.isMobile()) { + if (sidebarOpen && !ScreenUtils.isTouch()) { setFocus(); } }, [sidebarOpen, setFocus]); @@ -51,6 +62,7 @@ export const FilteredSidebarHeader: React.FC<{ } + sectionID={savedFiltersSectionID} > { - Mousetrap.bind("/", (e) => { - if (!showSidebar) { - setShowSidebar(true); - e.preventDefault(); - } - }); - - return () => { - Mousetrap.unbind("/"); - }; - }, [showSidebar, setShowSidebar]); - // Hide the sidebar when the user presses the "Esc" key useEffect(() => { Mousetrap.bind("esc", (e) => { diff --git a/ui/v2.5/src/components/List/Filters/LabeledIdFilter.tsx b/ui/v2.5/src/components/List/Filters/LabeledIdFilter.tsx index 1bca96d84..a2c77a029 100644 --- a/ui/v2.5/src/components/List/Filters/LabeledIdFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/LabeledIdFilter.tsx @@ -561,5 +561,7 @@ export function setObjectFilter( } out.galleries_filter = relatedFilterOutput as GalleryFilterType; break; + default: + throw new Error("Invalid filter mode"); } } diff --git a/ui/v2.5/src/components/List/Filters/PathFilter.tsx b/ui/v2.5/src/components/List/Filters/PathFilter.tsx index 97711ebef..ac44302c5 100644 --- a/ui/v2.5/src/components/List/Filters/PathFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/PathFilter.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Form } from "react-bootstrap"; import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect"; import { CriterionModifier } from "src/core/generated-graphql"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ModifierCriterion, CriterionValue, @@ -17,7 +17,7 @@ export const PathFilter: React.FC = ({ criterion, onValueChanged, }) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const libraryPaths = configuration?.general.stashes.map((s) => s.path); // don't show folder select for regex diff --git a/ui/v2.5/src/components/List/Filters/PerformersFilter.tsx b/ui/v2.5/src/components/List/Filters/PerformersFilter.tsx index 79ad18c1b..7e0dee855 100644 --- a/ui/v2.5/src/components/List/Filters/PerformersFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/PerformersFilter.tsx @@ -1,5 +1,8 @@ import React, { ReactNode, useMemo } from "react"; -import { PerformersCriterion } from "src/models/list-filter/criteria/performers"; +import { + PerformersCriterion, + PerformersCriterionOption, +} from "src/models/list-filter/criteria/performers"; import { CriterionModifier, FindPerformersForSelectQueryVariables, @@ -18,6 +21,7 @@ import { useLabeledIdFilterState, } from "./LabeledIdFilter"; import { SidebarListFilter } from "./SidebarListFilter"; +import { FormattedMessage } from "react-intl"; interface IPerformersFilter { criterion: PerformersCriterion; @@ -106,11 +110,19 @@ const PerformersFilter: React.FC = ({ export const SidebarPerformersFilter: React.FC<{ title?: ReactNode; - option: CriterionOption; + option?: CriterionOption; filter: ListFilterModel; setFilter: (f: ListFilterModel) => void; filterHook?: (f: ListFilterModel) => ListFilterModel; -}> = ({ title, option, filter, setFilter, filterHook }) => { + sectionID?: string; +}> = ({ + title = , + option = PerformersCriterionOption, + filter, + setFilter, + filterHook, + sectionID = "performers", +}) => { const state = useLabeledIdFilterState({ filter, setFilter, @@ -119,7 +131,14 @@ export const SidebarPerformersFilter: React.FC<{ useQuery: usePerformerQueryFilter, }); - return ; + return ( + + ); }; export default PerformersFilter; diff --git a/ui/v2.5/src/components/List/Filters/RatingFilter.tsx b/ui/v2.5/src/components/List/Filters/RatingFilter.tsx index 86d6a905b..8a07d54f9 100644 --- a/ui/v2.5/src/components/List/Filters/RatingFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/RatingFilter.tsx @@ -12,8 +12,11 @@ import { defaultRatingStarPrecision, defaultRatingSystemOptions, } from "src/utils/rating"; -import { ConfigurationContext } from "src/hooks/Config"; -import { RatingCriterion } from "src/models/list-filter/criteria/rating"; +import { useConfigurationContext } from "src/hooks/Config"; +import { + RatingCriterion, + RatingCriterionOption, +} from "src/models/list-filter/criteria/rating"; import { ListFilterModel } from "src/models/list-filter/filter"; import { Option, SidebarListFilter } from "./SidebarListFilter"; @@ -74,19 +77,21 @@ export const RatingFilter: React.FC = ({ interface ISidebarFilter { title?: React.ReactNode; - option: CriterionOption; + option?: CriterionOption; filter: ListFilterModel; setFilter: (f: ListFilterModel) => void; + sectionID?: string; } const any = "any"; const none = "none"; export const SidebarRatingFilter: React.FC = ({ - title, - option, + title = , + option = RatingCriterionOption, filter, setFilter, + sectionID = "rating", }) => { const intl = useIntl(); @@ -115,7 +120,7 @@ export const SidebarRatingFilter: React.FC = ({ [noneLabel] ); - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const ratingSystemOptions = config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions; @@ -191,6 +196,7 @@ export const SidebarRatingFilter: React.FC = ({ return ( <> = ({ singleValue preCandidates={ratingValue === null ? ratingStars : undefined} preSelected={ratingValue !== null ? ratingStars : undefined} + sectionID={sectionID} />
      diff --git a/ui/v2.5/src/components/List/Filters/SidebarAgeFilter.tsx b/ui/v2.5/src/components/List/Filters/SidebarAgeFilter.tsx new file mode 100644 index 000000000..3a6449ab6 --- /dev/null +++ b/ui/v2.5/src/components/List/Filters/SidebarAgeFilter.tsx @@ -0,0 +1,310 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { CriterionModifier } from "../../../core/generated-graphql"; +import { CriterionOption } from "../../../models/list-filter/criteria/criterion"; +import { NumberCriterion } from "src/models/list-filter/criteria/criterion"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { Option, SidebarListFilter } from "./SidebarListFilter"; +import { DoubleRangeInput } from "src/components/Shared/DoubleRangeInput"; +import { useDebounce } from "src/hooks/debounce"; + +interface ISidebarFilter { + title?: React.ReactNode; + option: CriterionOption; + filter: ListFilterModel; + setFilter: (f: ListFilterModel) => void; + sectionID?: string; +} + +// Age presets +const AGE_PRESETS = [ + { id: "18-25", label: "18-25", min: 18, max: 25 }, + { id: "25-35", label: "25-35", min: 25, max: 35 }, + { id: "35-45", label: "35-45", min: 35, max: 45 }, + { id: "45-60", label: "45-60", min: 45, max: 60 }, + { id: "60+", label: "60+", min: 60, max: null }, +]; + +const MAX_AGE = 60; // Maximum age for the slider +const MAX_LABEL = "60+"; // Display label for maximum age + +export const SidebarAgeFilter: React.FC = ({ + title, + option, + filter, + setFilter, + sectionID, +}) => { + const criteria = filter.criteriaFor(option.type) as NumberCriterion[]; + const criterion = criteria.length > 0 ? criteria[0] : null; + + // Get current values from criterion + const currentMin = criterion?.value?.value ?? 18; + const currentMax = criterion?.value?.value2 ?? MAX_AGE; + + const [sliderMin, setSliderMin] = useState(currentMin); + const [sliderMax, setSliderMax] = useState(currentMax); + const [minInput, setMinInput] = useState(currentMin.toString()); + const [maxInput, setMaxInput] = useState( + currentMax >= MAX_AGE ? MAX_LABEL : currentMax.toString() + ); + + // Reset slider when criterion is removed externally (via filter tag X) + useEffect(() => { + if (!criterion) { + setSliderMin(18); + setSliderMax(MAX_AGE); + setMinInput("18"); + setMaxInput(MAX_LABEL); + } + }, [criterion]); + + // Determine which preset is selected + const selectedPreset = useMemo(() => { + if (!criterion) return null; + + // Check if current values match any preset + for (const preset of AGE_PRESETS) { + if (preset.max === null) { + // For "60+" preset + if ( + criterion.modifier === CriterionModifier.GreaterThan && + criterion.value.value === preset.min + ) { + return preset.id; + } + } else { + // For range presets + if ( + criterion.modifier === CriterionModifier.Between && + criterion.value.value === preset.min && + criterion.value.value2 === preset.max + ) { + return preset.id; + } + } + } + + // Check if it's a custom range or custom GreaterThan + if ( + criterion.modifier === CriterionModifier.Between || + criterion.modifier === CriterionModifier.GreaterThan + ) { + return "custom"; + } + + return null; + }, [criterion]); + + const options: Option[] = useMemo(() => { + return AGE_PRESETS.map((preset) => ({ + id: preset.id, + label: preset.label, + className: "age-preset", + })); + }, []); + + const selected: Option[] = useMemo(() => { + if (!selectedPreset) return []; + if (selectedPreset === "custom") return []; + + const preset = AGE_PRESETS.find((p) => p.id === selectedPreset); + if (preset) { + return [ + { + id: preset.id, + label: preset.label, + className: "age-preset", + }, + ]; + } + return []; + }, [selectedPreset]); + + function onSelectPreset(item: Option) { + const preset = AGE_PRESETS.find((p) => p.id === item.id); + if (!preset) return; + + setSliderMin(preset.min); + setSliderMax(preset.max ?? MAX_AGE); + setMinInput(preset.min.toString()); + setMaxInput(preset.max === null ? MAX_LABEL : preset.max.toString()); + + const currentCriteria = filter.criteriaFor( + option.type + ) as NumberCriterion[]; + const currentCriterion = + currentCriteria.length > 0 ? currentCriteria[0] : null; + const newCriterion = currentCriterion + ? currentCriterion.clone() + : option.makeCriterion(); + + if (preset.max === null) { + // "60+" - use GreaterThan + newCriterion.modifier = CriterionModifier.GreaterThan; + newCriterion.value.value = preset.min; + newCriterion.value.value2 = undefined; + } else { + // Range preset - use Between + newCriterion.modifier = CriterionModifier.Between; + newCriterion.value.value = preset.min; + newCriterion.value.value2 = preset.max; + } + + setFilter(filter.replaceCriteria(option.type, [newCriterion])); + } + + function onUnselectPreset() { + setSliderMin(18); + setSliderMax(MAX_AGE); + setMinInput("18"); + setMaxInput(MAX_LABEL); + setFilter(filter.removeCriterion(option.type)); + } + + // Parse age input (supports formats like "25", "100+") + function parseAgeInput(input: string): number | null { + const trimmed = input.trim().toLowerCase(); + + if (trimmed === "max" || trimmed === MAX_LABEL.toLowerCase()) { + return MAX_AGE; + } + + const age = parseInt(trimmed); + if (isNaN(age) || age < 18 || age > MAX_AGE) { + return null; + } + + return age; + } + + // Filter update + function updateFilter(min: number, max: number) { + // If slider is at full range (18 to max), remove the filter entirely + if (min === 18 && max >= MAX_AGE) { + setFilter(filter.removeCriterion(option.type)); + return; + } + + const currentCriteria = filter.criteriaFor( + option.type + ) as NumberCriterion[]; + const currentCriterion = + currentCriteria.length > 0 ? currentCriteria[0] : null; + const newCriterion = currentCriterion + ? currentCriterion.clone() + : option.makeCriterion(); + + // If max is at MAX_AGE (but min > 18), use GreaterThan + if (max >= MAX_AGE) { + newCriterion.modifier = CriterionModifier.GreaterThan; + newCriterion.value.value = min; + newCriterion.value.value2 = undefined; + } else { + newCriterion.modifier = CriterionModifier.Between; + newCriterion.value.value = min; + newCriterion.value.value2 = max; + } + + setFilter(filter.replaceCriteria(option.type, [newCriterion])); + } + + const updateFilterDebounceMS = 300; + const debounceUpdateFilter = useDebounce( + updateFilter, + updateFilterDebounceMS + ); + + function handleSliderChange(min: number, max: number) { + setSliderMin(min); + setSliderMax(max); + setMinInput(min.toString()); + setMaxInput(max >= MAX_AGE ? MAX_LABEL : max.toString()); + + debounceUpdateFilter(min, max); + } + + function handleMinInputChange(value: string) { + setMinInput(value); + } + + function handleMaxInputChange(value: string) { + setMaxInput(value); + } + + function handleMinInputBlur() { + const parsed = parseAgeInput(minInput); + if (parsed !== null && parsed >= 18 && parsed < sliderMax) { + handleSliderChange(parsed, sliderMax); + } else { + // Reset to current value if invalid + setMinInput(sliderMin.toString()); + } + } + + function handleMaxInputBlur() { + const parsed = parseAgeInput(maxInput); + if (parsed !== null && parsed > sliderMin && parsed <= MAX_AGE) { + handleSliderChange(sliderMin, parsed); + } else { + // Reset to current value if invalid + setMaxInput(sliderMax >= MAX_AGE ? MAX_LABEL : sliderMax.toString()); + } + } + + const customSlider = ( +
      + handleSliderChange(min, max)} + minInput={ + handleMinInputChange(e.target.value)} + onBlur={handleMinInputBlur} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.currentTarget.blur(); + } + }} + placeholder="18" + /> + } + maxInput={ + handleMaxInputChange(e.target.value)} + onBlur={handleMaxInputBlur} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.currentTarget.blur(); + } + }} + placeholder={MAX_LABEL} + /> + } + /> +
      + ); + + return ( + + ); +}; diff --git a/ui/v2.5/src/components/List/Filters/SidebarDurationFilter.tsx b/ui/v2.5/src/components/List/Filters/SidebarDurationFilter.tsx new file mode 100644 index 000000000..29463bfa6 --- /dev/null +++ b/ui/v2.5/src/components/List/Filters/SidebarDurationFilter.tsx @@ -0,0 +1,362 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { CriterionModifier } from "../../../core/generated-graphql"; +import { CriterionOption } from "../../../models/list-filter/criteria/criterion"; +import { DurationCriterion } from "src/models/list-filter/criteria/criterion"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { Option, SidebarListFilter } from "./SidebarListFilter"; +import TextUtils from "src/utils/text"; +import { DoubleRangeInput } from "src/components/Shared/DoubleRangeInput"; +import { useDebounce } from "src/hooks/debounce"; +import { FormattedMessage } from "react-intl"; +import { DurationCriterionOption } from "src/models/list-filter/scenes"; + +interface ISidebarFilter { + title?: React.ReactNode; + option?: CriterionOption; + filter: ListFilterModel; + setFilter: (f: ListFilterModel) => void; + sectionID?: string; +} + +// Duration presets in seconds +const DURATION_PRESETS = [ + { id: "0-5", label: "0-5 min", min: 0, max: 300 }, + { id: "5-10", label: "5-10 min", min: 300, max: 600 }, + { id: "10-20", label: "10-20 min", min: 600, max: 1200 }, + { id: "20-40", label: "20-40 min", min: 1200, max: 2400 }, + { id: "40+", label: "40+ min", min: 2400, max: null }, +]; + +const MAX_DURATION = 7200; // 2 hours in seconds for the slider +const MAX_LABEL = "2+ hrs"; // Display label for maximum duration + +// Custom step values: 0, 2min (120s), 5min (300s), then 5 minute intervals +const DURATION_STEPS = [ + 0, 120, 300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000, 3300, 3600, + 3900, 4200, 4500, 4800, 5100, 5400, 5700, 6000, 6300, 6600, 6900, 7200, +]; + +// Snap a value to the nearest valid step +function snapToStep(value: number): number { + if (value <= 0) return 0; + if (value >= MAX_DURATION) return MAX_DURATION; + + // Find the closest step + let closest = DURATION_STEPS[0]; + let minDiff = Math.abs(value - closest); + + for (const step of DURATION_STEPS) { + const diff = Math.abs(value - step); + if (diff < minDiff) { + minDiff = diff; + closest = step; + } + } + + return closest; +} + +export const SidebarDurationFilter: React.FC = ({ + title = , + option = DurationCriterionOption, + filter, + setFilter, + sectionID = "duration", +}) => { + const criteria = filter.criteriaFor(option.type) as DurationCriterion[]; + const criterion = criteria.length > 0 ? criteria[0] : null; + + // Get current values from criterion + const currentMin = criterion?.value?.value ?? 0; + const currentMax = criterion?.value?.value2 ?? MAX_DURATION; + + const [sliderMin, setSliderMin] = useState(currentMin); + const [sliderMax, setSliderMax] = useState(currentMax); + const [minInput, setMinInput] = useState( + currentMin === 0 ? "0m" : TextUtils.secondsAsTimeString(currentMin) + ); + const [maxInput, setMaxInput] = useState( + currentMax >= MAX_DURATION + ? MAX_LABEL + : TextUtils.secondsAsTimeString(currentMax) + ); + + // Reset slider when criterion is removed externally (via filter tag X) + useEffect(() => { + if (!criterion) { + setSliderMin(0); + setSliderMax(MAX_DURATION); + setMinInput("0m"); + setMaxInput(MAX_LABEL); + } + }, [criterion]); + + // Determine which preset is selected + const selectedPreset = useMemo(() => { + if (!criterion) return null; + + // Check if current values match any preset + for (const preset of DURATION_PRESETS) { + if (preset.max === null) { + // For "40+ min" preset + if ( + criterion.modifier === CriterionModifier.GreaterThan && + criterion.value.value === preset.min + ) { + return preset.id; + } + } else { + // For range presets + if ( + criterion.modifier === CriterionModifier.Between && + criterion.value.value === preset.min && + criterion.value.value2 === preset.max + ) { + return preset.id; + } + } + } + + // Check if it's a custom range or custom GreaterThan + if ( + criterion.modifier === CriterionModifier.Between || + criterion.modifier === CriterionModifier.GreaterThan + ) { + return "custom"; + } + + return null; + }, [criterion]); + + const options: Option[] = useMemo(() => { + return DURATION_PRESETS.map((preset) => ({ + id: preset.id, + label: preset.label, + className: "duration-preset", + })); + }, []); + + const selected: Option[] = useMemo(() => { + if (!selectedPreset) return []; + if (selectedPreset === "custom") return []; + + const preset = DURATION_PRESETS.find((p) => p.id === selectedPreset); + if (preset) { + return [ + { + id: preset.id, + label: preset.label, + className: "duration-preset", + }, + ]; + } + return []; + }, [selectedPreset]); + + function onSelectPreset(item: Option) { + const preset = DURATION_PRESETS.find((p) => p.id === item.id); + if (!preset) return; + + const newCriterion = criterion ? criterion.clone() : option.makeCriterion(); + + if (preset.max === null) { + // "40+ min" - use GreaterThan + newCriterion.modifier = CriterionModifier.GreaterThan; + newCriterion.value.value = preset.min; + newCriterion.value.value2 = undefined; + } else { + // Range preset - use Between + newCriterion.modifier = CriterionModifier.Between; + newCriterion.value.value = preset.min; + newCriterion.value.value2 = preset.max; + } + + setSliderMin(preset.min); + setSliderMax(preset.max ?? MAX_DURATION); + setMinInput( + preset.min === 0 ? "0m" : TextUtils.secondsAsTimeString(preset.min) + ); + setMaxInput( + preset.max === null + ? MAX_LABEL + : TextUtils.secondsAsTimeString(preset.max) + ); + setFilter(filter.replaceCriteria(option.type, [newCriterion])); + } + + function onUnselectPreset() { + setFilter(filter.removeCriterion(option.type)); + setSliderMin(0); + setSliderMax(MAX_DURATION); + setMinInput("0m"); + setMaxInput(MAX_LABEL); + } + + // Parse time input (supports formats like "10", "1:30", "1:30:00", "2+ hrs") + function parseTimeInput(input: string): number | null { + const trimmed = input.trim().toLowerCase(); + + if (trimmed === "max" || trimmed === MAX_LABEL.toLowerCase()) { + return MAX_DURATION; + } + + // Try to parse as pure number (minutes) + const minutesOnly = parseFloat(trimmed); + if (!isNaN(minutesOnly) && trimmed.indexOf(":") === -1) { + return Math.round(minutesOnly * 60); + } + + // Parse HH:MM:SS or MM:SS format + const parts = trimmed.split(":").map((p) => parseInt(p)); + if (parts.some(isNaN)) { + return null; + } + + if (parts.length === 2) { + // MM:SS + return parts[0] * 60 + parts[1]; + } else if (parts.length === 3) { + // HH:MM:SS + return parts[0] * 3600 + parts[1] * 60 + parts[2]; + } + + return null; + } + + // Debounced filter update + function updateFilter(min: number, max: number) { + // If slider is at full range (0 to max), remove the filter entirely + if (min === 0 && max >= MAX_DURATION) { + setFilter(filter.removeCriterion(option.type)); + return; + } + + const newCriterion = criterion ? criterion.clone() : option.makeCriterion(); + + // If max is at MAX_DURATION (but min > 0), use GreaterThan + if (max >= MAX_DURATION) { + newCriterion.modifier = CriterionModifier.GreaterThan; + newCriterion.value.value = min; + newCriterion.value.value2 = undefined; + } else { + newCriterion.modifier = CriterionModifier.Between; + newCriterion.value.value = min; + newCriterion.value.value2 = max; + } + + setFilter(filter.replaceCriteria(option.type, [newCriterion])); + } + + const updateFilterDebounceMS = 300; + const debounceUpdateFilter = useDebounce( + updateFilter, + updateFilterDebounceMS + ); + + function handleSliderChange(min: number, max: number) { + if (min < 0 || max > MAX_DURATION || min >= max) { + return; + } + + setSliderMin(min); + setSliderMax(max); + setMinInput(min === 0 ? "0m" : TextUtils.secondsAsTimeString(min)); + setMaxInput( + max >= MAX_DURATION ? MAX_LABEL : TextUtils.secondsAsTimeString(max) + ); + + debounceUpdateFilter(min, max); + } + + function handleMinInputChange(value: string) { + setMinInput(value); + } + + function handleMaxInputChange(value: string) { + setMaxInput(value); + } + + function handleMinInputBlur() { + const parsed = parseTimeInput(minInput); + if (parsed !== null && parsed >= 0 && parsed < sliderMax) { + handleSliderChange(parsed, sliderMax); + } else { + // Reset to current value if invalid + setMinInput( + sliderMin === 0 ? "0m" : TextUtils.secondsAsTimeString(sliderMin) + ); + } + } + + function handleMaxInputBlur() { + const parsed = parseTimeInput(maxInput); + if (parsed !== null && parsed > sliderMin && parsed <= MAX_DURATION) { + handleSliderChange(sliderMin, parsed); + } else { + // Reset to current value if invalid + setMaxInput( + sliderMax >= MAX_DURATION + ? MAX_LABEL + : TextUtils.secondsAsTimeString(sliderMax) + ); + } + } + + const customSlider = ( + handleMinInputChange(e.target.value)} + onBlur={handleMinInputBlur} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.currentTarget.blur(); + } + }} + placeholder="0:00" + /> + } + maxInput={ + handleMaxInputChange(e.target.value)} + onBlur={handleMaxInputBlur} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.currentTarget.blur(); + } + }} + placeholder={MAX_LABEL} + /> + } + min={0} + max={MAX_DURATION} + value={[sliderMin, sliderMax]} + onChange={(vals) => { + handleSliderChange(snapToStep(vals[0]), snapToStep(vals[1])); + }} + /> + ); + + return ( + + ); +}; diff --git a/ui/v2.5/src/components/List/Filters/SidebarListFilter.tsx b/ui/v2.5/src/components/List/Filters/SidebarListFilter.tsx index 71a56f23d..fe9b7987c 100644 --- a/ui/v2.5/src/components/List/Filters/SidebarListFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/SidebarListFilter.tsx @@ -276,6 +276,8 @@ export const SidebarListFilter: React.FC<{ preCandidates?: React.ReactNode; postCandidates?: React.ReactNode; onOpen?: () => void; + // used to store open/closed state in SidebarStateContext + sectionID?: string; }> = ({ title, selected, @@ -292,6 +294,7 @@ export const SidebarListFilter: React.FC<{ preSelected, postSelected, onOpen, + sectionID, }) => { // TODO - sort items? @@ -325,6 +328,7 @@ export const SidebarListFilter: React.FC<{ {preSelected ?
      {preSelected}
      : null} diff --git a/ui/v2.5/src/components/List/Filters/StudiosFilter.tsx b/ui/v2.5/src/components/List/Filters/StudiosFilter.tsx index 9065f2452..3e28bd927 100644 --- a/ui/v2.5/src/components/List/Filters/StudiosFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/StudiosFilter.tsx @@ -5,7 +5,10 @@ import { useFindStudiosForSelectQuery, } from "src/core/generated-graphql"; import { HierarchicalObjectsFilter } from "./SelectableFilter"; -import { StudiosCriterion } from "src/models/list-filter/criteria/studios"; +import { + StudiosCriterion, + StudiosCriterionOption, +} from "src/models/list-filter/criteria/studios"; import { sortByRelevance } from "src/utils/query"; import { CriterionOption } from "src/models/list-filter/criteria/criterion"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -16,6 +19,7 @@ import { useLabeledIdFilterState, } from "./LabeledIdFilter"; import { SidebarListFilter } from "./SidebarListFilter"; +import { FormattedMessage } from "react-intl"; interface IStudiosFilter { criterion: StudiosCriterion; @@ -94,11 +98,19 @@ const StudiosFilter: React.FC = ({ export const SidebarStudiosFilter: React.FC<{ title?: ReactNode; - option: CriterionOption; + option?: CriterionOption; filter: ListFilterModel; setFilter: (f: ListFilterModel) => void; filterHook?: (f: ListFilterModel) => ListFilterModel; -}> = ({ title, option, filter, setFilter, filterHook }) => { + sectionID?: string; +}> = ({ + title = , + option = StudiosCriterionOption, + filter, + setFilter, + filterHook, + sectionID = "studios", +}) => { const state = useLabeledIdFilterState({ filter, setFilter, @@ -110,7 +122,14 @@ export const SidebarStudiosFilter: React.FC<{ includeSubMessageID: "subsidiary_studios", }); - return ; + return ( + + ); }; export default StudiosFilter; diff --git a/ui/v2.5/src/components/List/Filters/TagsFilter.tsx b/ui/v2.5/src/components/List/Filters/TagsFilter.tsx index e8e25db56..446a90331 100644 --- a/ui/v2.5/src/components/List/Filters/TagsFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/TagsFilter.tsx @@ -16,7 +16,11 @@ import { useLabeledIdFilterState, } from "./LabeledIdFilter"; import { SidebarListFilter } from "./SidebarListFilter"; -import { TagsCriterion } from "src/models/list-filter/criteria/tags"; +import { + TagsCriterion, + TagsCriterionOption, +} from "src/models/list-filter/criteria/tags"; +import { FormattedMessage } from "react-intl"; interface ITagsFilter { criterion: TagsCriterion; @@ -99,11 +103,19 @@ const TagsFilter: React.FC = ({ criterion, setCriterion }) => { export const SidebarTagsFilter: React.FC<{ title?: ReactNode; - option: CriterionOption; + option?: CriterionOption; filter: ListFilterModel; setFilter: (f: ListFilterModel) => void; filterHook?: (f: ListFilterModel) => ListFilterModel; -}> = ({ title, option, filter, setFilter, filterHook }) => { + sectionID?: string; +}> = ({ + title = , + option = TagsCriterionOption, + filter, + setFilter, + filterHook, + sectionID = "tags", +}) => { const state = useLabeledIdFilterState({ filter, setFilter, @@ -114,7 +126,14 @@ export const SidebarTagsFilter: React.FC<{ includeSubMessageID: "sub_tags", }); - return ; + return ( + + ); }; export default TagsFilter; diff --git a/ui/v2.5/src/components/List/ItemList.tsx b/ui/v2.5/src/components/List/ItemList.tsx index 4ffeff766..67d09e721 100644 --- a/ui/v2.5/src/components/List/ItemList.tsx +++ b/ui/v2.5/src/components/List/ItemList.tsx @@ -1,7 +1,6 @@ import React, { PropsWithChildren, useCallback, - useContext, useEffect, useMemo, useState, @@ -43,7 +42,9 @@ import { IItemListOperation, } from "./FilteredListToolbar"; import { PagedList } from "./PagedList"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; +import { useZoomKeybinds } from "./ZoomSlider"; +import { DisplayMode } from "src/models/list-filter/types"; interface IFilteredItemList { filterStateProps: IFilterStateHook; @@ -55,7 +56,7 @@ export function useFilteredItemList< T extends QueryResult, E extends IHasID = IHasID >(props: IFilteredItemList) { - const { configuration: config } = useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); // States const filterState = useFilterState({ @@ -72,7 +73,7 @@ export function useFilteredItemList< const { result, items, totalCount, pages } = queryResult; const listSelect = useListSelect(items); - const { onSelectAll, onSelectNone } = listSelect; + const { onSelectAll, onSelectNone, onInvertSelection } = listSelect; const modalState = useModal(); const { showModal, closeModal } = modalState; @@ -98,6 +99,7 @@ export function useFilteredItemList< onChangePage: setPage, onSelectAll, onSelectNone, + onInvertSelection, pages, showEditFilter, }); @@ -111,9 +113,8 @@ export function useFilteredItemList< }; } -interface IItemListProps { +interface IItemListProps { view?: View; - zoomable?: boolean; otherOperations?: IItemListOperation[]; renderContent: ( result: T, @@ -123,7 +124,7 @@ interface IItemListProps { onChangePage: (page: number) => void, pageCount: number ) => React.ReactNode; - renderMetadataByline?: (data: T) => React.ReactNode; + renderMetadataByline?: (data: T, metadataInfo?: M) => React.ReactNode; renderEditDialog?: ( selected: E[], onClose: (applied: boolean) => void @@ -140,12 +141,11 @@ interface IItemListProps { renderToolbar?: (props: IFilteredListToolbar) => React.ReactNode; } -export const ItemList = ( - props: IItemListProps +export const ItemList = ( + props: IItemListProps ) => { const { view, - zoomable, otherOperations, renderContent, renderEditDialog, @@ -156,8 +156,8 @@ export const ItemList = ( } = props; const { filter, setFilter: updateFilter } = useFilter(); - const { effectiveFilter, result, cachedResult, totalCount } = - useQueryResultContext(); + const { effectiveFilter, result, metadataInfo, cachedResult, totalCount } = + useQueryResultContext(); const listSelect = useListContext(); const { selectedIds, @@ -165,6 +165,7 @@ export const ItemList = ( onSelectChange, onSelectAll, onSelectNone, + onInvertSelection, } = listSelect; // scroll to the top of the page when the page changes @@ -175,8 +176,8 @@ export const ItemList = ( const metadataByline = useMemo(() => { if (cachedResult.loading) return ""; - return renderMetadataByline?.(cachedResult) ?? ""; - }, [renderMetadataByline, cachedResult]); + return renderMetadataByline?.(cachedResult, metadataInfo) ?? ""; + }, [renderMetadataByline, cachedResult, metadataInfo]); const pages = Math.ceil(totalCount / filter.itemsPerPage); @@ -213,10 +214,20 @@ export const ItemList = ( onChangePage, onSelectAll, onSelectNone, + onInvertSelection, pages, showEditFilter, }); + const zoomable = + filter.displayMode === DisplayMode.Grid || + filter.displayMode === DisplayMode.Wall; + + useZoomKeybinds({ + zoomIndex: zoomable ? filter.zoomIndex : undefined, + onChangeZoom: (zoom) => updateFilter(filter.setZoom(zoom)), + }); + useEffect(() => { if (addKeybinds) { const unbindExtras = addKeybinds(result, effectiveFilter, selectedIds); @@ -361,11 +372,16 @@ export const ItemList = ( ); }; -interface IItemListContextProps { +interface IItemListContextProps< + T extends QueryResult, + E extends IHasID, + M = unknown +> { filterMode: GQL.FilterMode; defaultSort?: string; defaultFilter?: ListFilterModel; useResult: (filter: ListFilterModel) => T; + useMetadataInfo?: (filter: ListFilterModel) => M; getCount: (data: T) => number; getItems: (data: T) => E[]; filterHook?: (filter: ListFilterModel) => ListFilterModel; @@ -376,14 +392,19 @@ interface IItemListContextProps { // Provides the contexts for the ItemList component. Includes functionality to scroll // to top on page change. -export const ItemListContext = ( - props: PropsWithChildren> +export const ItemListContext = < + T extends QueryResult, + E extends IHasID, + M = unknown +>( + props: PropsWithChildren> ) => { const { filterMode, defaultSort, defaultFilter: providedDefaultFilter, useResult, + useMetadataInfo, getCount, getItems, view, @@ -393,7 +414,7 @@ export const ItemListContext = ( children, } = props; - const { configuration: config } = useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const emptyFilter = useMemo( () => @@ -409,12 +430,7 @@ export const ItemListContext = ( new ListFilterModel(filterMode, config, { defaultSortBy: defaultSort }) ); - const { defaultFilter, loading: defaultFilterLoading } = useDefaultFilter( - emptyFilter, - view - ); - - if (defaultFilterLoading) return null; + const { defaultFilter } = useDefaultFilter(emptyFilter, view); return ( @@ -422,6 +438,7 @@ export const ItemListContext = ( diff --git a/ui/v2.5/src/components/List/ListFilter.tsx b/ui/v2.5/src/components/List/ListFilter.tsx index 99bae365e..ff3be0360 100644 --- a/ui/v2.5/src/components/List/ListFilter.tsx +++ b/ui/v2.5/src/components/List/ListFilter.tsx @@ -1,4 +1,3 @@ -import cloneDeep from "lodash-es/cloneDeep"; import React, { useCallback, useEffect, @@ -23,20 +22,18 @@ import { import { Icon } from "../Shared/Icon"; import { ListFilterModel } from "src/models/list-filter/filter"; import useFocus from "src/utils/focus"; -import { FormattedMessage, useIntl } from "react-intl"; -import { SavedFilterDropdown } from "./SavedFilterList"; +import { useIntl } from "react-intl"; import { faCaretDown, faCaretUp, faCheck, faRandom, } from "@fortawesome/free-solid-svg-icons"; -import { FilterButton } from "./Filters/FilterButton"; import { useDebounce } from "src/hooks/debounce"; -import { View } from "./views"; import { ClearableInput } from "../Shared/ClearableInput"; import { useStopWheelScroll } from "src/utils/form"; import { ISortByOption } from "src/models/list-filter/filter-options"; +import { useConfigurationContext } from "src/hooks/Config"; export function useDebouncedSearchInput( filter: ListFilterModel, @@ -249,14 +246,24 @@ export const SortBySelect: React.FC<{ onReshuffleRandomSort, }) => { const intl = useIntl(); + const { configuration } = useConfigurationContext(); + const { sfwContentMode } = configuration.interface; const currentSortBy = options.find((o) => o.value === sortBy); + const currentSortByMessageID = currentSortBy + ? !sfwContentMode + ? currentSortBy.messageID + : currentSortBy.sfwMessageID ?? currentSortBy.messageID + : ""; function renderSortByOptions() { return options .map((o) => { + const messageID = !sfwContentMode + ? o.messageID + : o.sfwMessageID ?? o.messageID; return { - message: intl.formatMessage({ id: o.messageID }), + message: intl.formatMessage({ id: messageID }), value: o.value, }; }) @@ -267,6 +274,7 @@ export const SortBySelect: React.FC<{ key={option.value} className="bg-secondary text-white" eventKey={option.value} + data-value={option.value} > {option.message} @@ -274,11 +282,11 @@ export const SortBySelect: React.FC<{ } return ( - + {currentSortBy - ? intl.formatMessage({ id: currentSortBy.messageID }) + ? intl.formatMessage({ id: currentSortByMessageID }) : ""} @@ -318,115 +326,3 @@ export const SortBySelect: React.FC<{ ); }; - -interface IListFilterProps { - onFilterUpdate: (newFilter: ListFilterModel) => void; - filter: ListFilterModel; - view?: View; - openFilterDialog: () => void; - withSidebar?: boolean; -} - -export const ListFilter: React.FC = ({ - onFilterUpdate, - filter, - openFilterDialog, - view, - withSidebar, -}) => { - const filterOptions = filter.options; - - useEffect(() => { - Mousetrap.bind("r", () => onReshuffleRandomSort()); - - return () => { - Mousetrap.unbind("r"); - }; - }); - - function onChangePageSize(pp: number) { - const newFilter = cloneDeep(filter); - newFilter.itemsPerPage = pp; - newFilter.currentPage = 1; - onFilterUpdate(newFilter); - } - - function onChangeSortDirection() { - const newFilter = cloneDeep(filter); - if (filter.sortDirection === SortDirectionEnum.Asc) { - newFilter.sortDirection = SortDirectionEnum.Desc; - } else { - newFilter.sortDirection = SortDirectionEnum.Asc; - } - - onFilterUpdate(newFilter); - } - - function onChangeSortBy(eventKey: string | null) { - const newFilter = cloneDeep(filter); - newFilter.sortBy = eventKey ?? undefined; - newFilter.currentPage = 1; - onFilterUpdate(newFilter); - } - - function onReshuffleRandomSort() { - const newFilter = cloneDeep(filter); - newFilter.currentPage = 1; - newFilter.randomSeed = -1; - onFilterUpdate(newFilter); - } - - function render() { - return ( - <> - {!withSidebar && ( -
      - -
      - )} - - {!withSidebar && ( - - { - onFilterUpdate(f); - }} - view={view} - /> - - - - } - > - openFilterDialog()} - count={filter.count()} - /> - - - )} - - - - - - ); - } - - return render(); -}; diff --git a/ui/v2.5/src/components/List/ListOperationButtons.tsx b/ui/v2.5/src/components/List/ListOperationButtons.tsx index 6bb31339a..314c28bf8 100644 --- a/ui/v2.5/src/components/List/ListOperationButtons.tsx +++ b/ui/v2.5/src/components/List/ListOperationButtons.tsx @@ -1,37 +1,41 @@ -import React, { PropsWithChildren, useEffect } from "react"; -import { - Button, - ButtonGroup, - Dropdown, - OverlayTrigger, - Tooltip, -} from "react-bootstrap"; +import React, { PropsWithChildren, useEffect, useMemo } from "react"; +import { Button, ButtonGroup, Dropdown } from "react-bootstrap"; import Mousetrap from "mousetrap"; import { FormattedMessage, useIntl } from "react-intl"; import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { Icon } from "../Shared/Icon"; import { faEllipsisH, + faPencil, faPencilAlt, + faPlay, + faPlus, faTrash, } from "@fortawesome/free-solid-svg-icons"; import cx from "classnames"; +import { createPortal } from "react-dom"; export const OperationDropdown: React.FC< PropsWithChildren<{ className?: string; + menuPortalTarget?: HTMLElement; + menuClassName?: string; }> -> = ({ className, children }) => { +> = ({ className, menuPortalTarget, menuClassName, children }) => { if (!children) return null; + const menu = ( + + {children} + + ); + return ( - - {children} - + {menuPortalTarget ? createPortal(menu, menuPortalTarget) : menu} ); }; @@ -62,6 +66,7 @@ export interface IListFilterOperation { interface IListOperationButtonsProps { onSelectAll?: () => void; onSelectNone?: () => void; + onInvertSelection?: () => void; onEdit?: () => void; onDelete?: () => void; itemsSelected?: boolean; @@ -71,6 +76,7 @@ interface IListOperationButtonsProps { export const ListOperationButtons: React.FC = ({ onSelectAll, onSelectNone, + onInvertSelection, onEdit, onDelete, itemsSelected, @@ -81,6 +87,7 @@ export const ListOperationButtons: React.FC = ({ useEffect(() => { Mousetrap.bind("s a", () => onSelectAll?.()); Mousetrap.bind("s n", () => onSelectNone?.()); + Mousetrap.bind("s i", () => onInvertSelection?.()); Mousetrap.bind("e", () => { if (itemsSelected) { @@ -97,13 +104,21 @@ export const ListOperationButtons: React.FC = ({ return () => { Mousetrap.unbind("s a"); Mousetrap.unbind("s n"); + Mousetrap.unbind("s i"); Mousetrap.unbind("e"); Mousetrap.unbind("d d"); }; - }); + }, [ + onSelectAll, + onSelectNone, + onInvertSelection, + itemsSelected, + onEdit, + onDelete, + ]); - function maybeRenderButtons() { - const buttons = (otherOperations ?? []).filter((o) => { + const buttons = useMemo(() => { + const ret = (otherOperations ?? []).filter((o) => { if (!o.icon) { return false; } @@ -114,16 +129,17 @@ export const ListOperationButtons: React.FC = ({ return o.isDisplayed(); }); + if (itemsSelected) { if (onEdit) { - buttons.push({ + ret.push({ icon: faPencilAlt, text: intl.formatMessage({ id: "actions.edit" }), onClick: onEdit, }); } if (onDelete) { - buttons.push({ + ret.push({ icon: faTrash, text: intl.formatMessage({ id: "actions.delete" }), onClick: onDelete, @@ -132,59 +148,76 @@ export const ListOperationButtons: React.FC = ({ } } - if (buttons.length > 0) { - return ( - - {buttons.map((button) => { - return ( - {button.text}} - key={button.text} - > - - - ); - })} - - ); - } - } + return ret; + }, [otherOperations, itemsSelected, onEdit, onDelete, intl]); - function renderSelectAll() { - if (onSelectAll) { - return ( - onSelectAll?.()} - > - - - ); - } - } + const operationButtons = useMemo(() => { + return ( + <> + {buttons.map((button) => { + return ( + + ); + })} + + ); + }, [buttons]); - function renderSelectNone() { - if (onSelectNone) { - return ( - onSelectNone?.()} - > - - - ); + const moreDropdown = useMemo(() => { + function renderSelectAll() { + if (onSelectAll) { + return ( + onSelectAll?.()} + > + + + ); + } } - } - function renderMore() { - const options = [renderSelectAll(), renderSelectNone()].filter((o) => o); + function renderSelectNone() { + if (onSelectNone) { + return ( + onSelectNone?.()} + > + + + ); + } + } + + function renderInvertSelection() { + if (onInvertSelection) { + return ( + onInvertSelection?.()} + > + + + ); + } + } + + const options = [ + renderSelectAll(), + renderSelectNone(), + renderInvertSelection(), + ].filter((o) => o); if (otherOperations) { otherOperations @@ -218,13 +251,124 @@ export const ListOperationButtons: React.FC = ({ {options.length > 0 ? options : undefined} ); + }, [otherOperations, onSelectAll, onSelectNone, onInvertSelection]); + + // don't render anything if there are no buttons or operations + if (buttons.length === 0 && !moreDropdown) { + return null; } return ( <> - {maybeRenderButtons()} - - {renderMore()} + + {operationButtons} + {moreDropdown} + ); }; + +interface IListOperations { + text: string; + onClick: () => void; + isDisplayed?: () => boolean; + className?: string; +} + +export const ListOperations: React.FC<{ + items: number; + hasSelection?: boolean; + operations?: IListOperations[]; + onEdit?: () => void; + onDelete?: () => void; + onPlay?: () => void; + onCreateNew?: () => void; + entityType?: string; + operationsClassName?: string; + operationsMenuClassName?: string; +}> = ({ + items, + hasSelection = false, + operations = [], + onEdit, + onDelete, + onPlay, + onCreateNew, + entityType, + operationsClassName = "list-operations", + operationsMenuClassName, +}) => { + const intl = useIntl(); + + return ( +
      + + {!!items && onPlay && ( + + )} + {!hasSelection && onCreateNew && ( + + )} + + {hasSelection && (onEdit || onDelete) && ( + <> + {onEdit && ( + + )} + {onDelete && ( + + )} + + )} + + {operations.length > 0 && ( + + {operations.map((o) => { + if (o.isDisplayed && !o.isDisplayed()) { + return null; + } + + return ( + + ); + })} + + )} + +
      + ); +}; diff --git a/ui/v2.5/src/components/List/ListProvider.tsx b/ui/v2.5/src/components/List/ListProvider.tsx index 2e8854586..8b9ee7bfb 100644 --- a/ui/v2.5/src/components/List/ListProvider.tsx +++ b/ui/v2.5/src/components/List/ListProvider.tsx @@ -63,6 +63,7 @@ const emptyState: IListContextState = { onSelectChange: () => {}, onSelectAll: () => {}, onSelectNone: () => {}, + onInvertSelection: () => {}, items: [], hasSelection: false, selectedItems: [], @@ -80,21 +81,25 @@ export function useListContextOptional() { interface IQueryResultContextOptions< T extends QueryResult, - E extends IHasID = IHasID + E extends IHasID = IHasID, + M = unknown > { filterHook?: (filter: ListFilterModel) => ListFilterModel; useResult: (filter: ListFilterModel) => T; + useMetadataInfo?: (filter: ListFilterModel) => M; getCount: (data: T) => number; getItems: (data: T) => E[]; } export interface IQueryResultContextState< T extends QueryResult = QueryResult, - E extends IHasID = IHasID + E extends IHasID = IHasID, + M = unknown > { effectiveFilter: ListFilterModel; result: T; cachedResult: T; + metadataInfo?: M; items: E[]; totalCount: number; } @@ -104,15 +109,23 @@ export const QueryResultStateContext = export const QueryResultContext = < T extends QueryResult, - E extends IHasID = IHasID + E extends IHasID = IHasID, + M = unknown >( - props: IQueryResultContextOptions & { + props: IQueryResultContextOptions & { children?: - | ((props: IQueryResultContextState) => React.ReactNode) + | ((props: IQueryResultContextState) => React.ReactNode) | React.ReactNode; } ) => { - const { filterHook, useResult, getItems, getCount, children } = props; + const { + filterHook, + useResult, + useMetadataInfo, + getItems, + getCount, + children, + } = props; const { filter } = useFilter(); const effectiveFilter = useMemo(() => { @@ -122,9 +135,16 @@ export const QueryResultContext = < return filter; }, [filter, filterHook]); - const result = useResult(effectiveFilter); + // metadata filter is the effective filter with the sort, page size and page number removed + const metadataFilter = useMemo( + () => effectiveFilter.metadataInfo(), + [effectiveFilter] + ); - // use cached query result for pagination and metadata rendering + const result = useResult(effectiveFilter); + const metadataInfo = useMetadataInfo?.(metadataFilter); + + // use cached query result for pagination const cachedResult = useCachedQueryResult(effectiveFilter, result); const items = useMemo(() => getItems(result), [getItems, result]); @@ -133,12 +153,13 @@ export const QueryResultContext = < [getCount, cachedResult] ); - const state: IQueryResultContextState = { + const state: IQueryResultContextState = { effectiveFilter, result, cachedResult, items, totalCount, + metadataInfo, }; return ( @@ -154,7 +175,8 @@ export const QueryResultContext = < export function useQueryResultContext< T extends QueryResult, - E extends IHasID = IHasID + E extends IHasID = IHasID, + M = unknown >() { const context = React.useContext(QueryResultStateContext); @@ -164,5 +186,5 @@ export function useQueryResultContext< ); } - return context as IQueryResultContextState; + return context as IQueryResultContextState; } diff --git a/ui/v2.5/src/components/List/ListViewOptions.tsx b/ui/v2.5/src/components/List/ListViewOptions.tsx index 1ea928983..b681e086d 100644 --- a/ui/v2.5/src/components/List/ListViewOptions.tsx +++ b/ui/v2.5/src/components/List/ListViewOptions.tsx @@ -1,8 +1,16 @@ import React, { useEffect, useRef, useState } from "react"; import Mousetrap from "mousetrap"; -import { Button, Dropdown, Overlay, Popover } from "react-bootstrap"; +import { + Button, + ButtonGroup, + Dropdown, + Overlay, + OverlayTrigger, + Popover, + Tooltip, +} from "react-bootstrap"; import { DisplayMode } from "src/models/list-filter/types"; -import { useIntl } from "react-intl"; +import { IntlShape, useIntl } from "react-intl"; import { Icon } from "../Shared/Icon"; import { faChevronDown, @@ -53,6 +61,10 @@ function getLabelId(option: DisplayMode) { return `display_mode.${displayModeId}`; } +function getLabel(intl: IntlShape, option: DisplayMode) { + return intl.formatMessage({ id: getLabelId(option) }); +} + export const ListViewOptions: React.FC = ({ zoomIndex, onSetZoom, @@ -60,9 +72,6 @@ export const ListViewOptions: React.FC = ({ onSetDisplayMode, displayModeOptions, }) => { - const minZoom = 0; - const maxZoom = 3; - const intl = useIntl(); const overlayTarget = useRef(null); @@ -84,18 +93,20 @@ export const ListViewOptions: React.FC = ({ onSetDisplayMode(DisplayMode.Wall); } }); + Mousetrap.bind("v t", () => { + if (displayModeOptions.includes(DisplayMode.Tagger)) { + onSetDisplayMode(DisplayMode.Tagger); + } + }); return () => { Mousetrap.unbind("v g"); Mousetrap.unbind("v l"); Mousetrap.unbind("v w"); + Mousetrap.unbind("v t"); }; }); - function getLabel(option: DisplayMode) { - return intl.formatMessage({ id: getLabelId(option) }); - } - function onChangeZoom(v: number) { if (onSetZoom) { onSetZoom(v); @@ -110,7 +121,7 @@ export const ListViewOptions: React.FC = ({ variant="secondary" title={intl.formatMessage( { id: "display_mode.label_current" }, - { current: getLabel(displayMode) } + { current: getLabel(intl, displayMode) } )} onClick={() => setShowOptions(!showOptions)} > @@ -134,8 +145,6 @@ export const ListViewOptions: React.FC = ({ displayMode === DisplayMode.Wall) ? (
      @@ -150,7 +159,7 @@ export const ListViewOptions: React.FC = ({ onSetDisplayMode(option); }} > - {getLabel(option)} + {getLabel(intl, option)} ))}
      @@ -161,3 +170,48 @@ export const ListViewOptions: React.FC = ({ ); }; + +export const ListViewButtonGroup: React.FC = ({ + zoomIndex, + onSetZoom, + displayMode, + onSetDisplayMode, + displayModeOptions, +}) => { + const intl = useIntl(); + + return ( + <> + {displayModeOptions.length > 1 && ( + + {displayModeOptions.map((option) => ( + + {getLabel(intl, option)} + + } + > + + + ))} + + )} +
      + {onSetZoom && + zoomIndex !== undefined && + (displayMode === DisplayMode.Grid || + displayMode === DisplayMode.Wall) ? ( + + ) : null} +
      + + ); +}; diff --git a/ui/v2.5/src/components/List/Pagination.tsx b/ui/v2.5/src/components/List/Pagination.tsx index e117b532e..bfa6697ee 100644 --- a/ui/v2.5/src/components/List/Pagination.tsx +++ b/ui/v2.5/src/components/List/Pagination.tsx @@ -44,7 +44,7 @@ const PageCount: React.FC<{ useStopWheelScroll(pageInput); const pageOptions = useMemo(() => { - const maxPagesToShow = 10; + const maxPagesToShow = 1000; const min = Math.max(1, currentPage - maxPagesToShow / 2); const max = Math.min(min + maxPagesToShow, totalPages); const pages = []; diff --git a/ui/v2.5/src/components/List/SavedFilterList.tsx b/ui/v2.5/src/components/List/SavedFilterList.tsx index cbeeaa70a..df1d6136a 100644 --- a/ui/v2.5/src/components/List/SavedFilterList.tsx +++ b/ui/v2.5/src/components/List/SavedFilterList.tsx @@ -30,12 +30,15 @@ import { faBookmark, faSave, faTimes } from "@fortawesome/free-solid-svg-icons"; import { AlertModal } from "../Shared/Alert"; import cx from "classnames"; import { TruncatedInlineText } from "../Shared/TruncatedText"; +import { OperationButton } from "../Shared/OperationButton"; +import { createPortal } from "react-dom"; const ExistingSavedFilterList: React.FC<{ name: string; - setName: (name: string) => void; - existing: { name: string; id: string }[]; -}> = ({ name, setName, existing }) => { + onSelect: (value: SavedFilterDataFragment) => void; + savedFilters: SavedFilterDataFragment[]; + disabled?: boolean; +}> = ({ name, onSelect, savedFilters: existing, disabled = false }) => { const filtered = useMemo(() => { if (!name) return existing; @@ -51,7 +54,8 @@ const ExistingSavedFilterList: React.FC<{ @@ -64,7 +68,8 @@ const ExistingSavedFilterList: React.FC<{ export const SaveFilterDialog: React.FC<{ mode: FilterMode; onClose: (name?: string, id?: string) => void; -}> = ({ mode, onClose }) => { + isSaving?: boolean; +}> = ({ mode, onClose, isSaving = false }) => { const intl = useIntl(); const [filterName, setFilterName] = useState(""); @@ -79,6 +84,74 @@ export const SaveFilterDialog: React.FC<{ return ( + + + + + + + + + setFilterName(e.target.value)} + disabled={isSaving} + /> + + + setFilterName(f.name)} + savedFilters={data?.findSavedFilters ?? []} + /> + + {!!overwritingFilter && ( + + + + )} + + + + onClose(filterName, overwritingFilter?.id)} + > + {intl.formatMessage({ id: "actions.save" })} + + + + ); +}; + +export const LoadFilterDialog: React.FC<{ + mode: FilterMode; + onClose: (filter?: SavedFilterDataFragment) => void; +}> = ({ mode, onClose }) => { + const intl = useIntl(); + const [filterName, setFilterName] = useState(""); + + const { data } = useFindSavedFilters(mode); + + return ( + + + + @@ -94,31 +167,14 @@ export const SaveFilterDialog: React.FC<{ onClose(f)} + savedFilters={data?.findSavedFilters ?? []} /> - - {!!overwritingFilter && ( - - - - )} - ); @@ -166,7 +222,7 @@ const OverwriteAlert: React.FC<{ void; view?: View; + menuPortalTarget?: Element | DocumentFragment; } export const SavedFilterList: React.FC = ({ @@ -786,8 +843,15 @@ export const SavedFilterDropdown: React.FC = (props) => { )); SavedFilterDropdownRef.displayName = "SavedFilterDropdown"; + const menu = ( + + ); + return ( - + = (props) => { - + {props.menuPortalTarget + ? createPortal(menu, props.menuPortalTarget) + : menu} ); }; diff --git a/ui/v2.5/src/components/List/ZoomSlider.tsx b/ui/v2.5/src/components/List/ZoomSlider.tsx index dff8e4f57..093b5ec7a 100644 --- a/ui/v2.5/src/components/List/ZoomSlider.tsx +++ b/ui/v2.5/src/components/List/ZoomSlider.tsx @@ -2,19 +2,14 @@ import React, { useEffect } from "react"; import Mousetrap from "mousetrap"; import { Form } from "react-bootstrap"; -export interface IZoomSelectProps { - minZoom: number; - maxZoom: number; - zoomIndex: number; - onChangeZoom: (v: number) => void; -} +const minZoom = 0; +const maxZoom = 3; -export const ZoomSelect: React.FC = ({ - minZoom, - maxZoom, - zoomIndex, - onChangeZoom, -}) => { +export function useZoomKeybinds(props: { + zoomIndex: number | undefined; + onChangeZoom: (v: number) => void; +}) { + const { zoomIndex, onChangeZoom } = props; useEffect(() => { Mousetrap.bind("+", () => { if (zoomIndex !== undefined && zoomIndex < maxZoom) { @@ -32,7 +27,17 @@ export const ZoomSelect: React.FC = ({ Mousetrap.unbind("-"); }; }); +} +export interface IZoomSelectProps { + zoomIndex: number; + onChangeZoom: (v: number) => void; +} + +export const ZoomSelect: React.FC = ({ + zoomIndex, + onChangeZoom, +}) => { return ( div > :not(:first-child) { + margin-left: 0.25rem; + } + } + + .search-term-row { + align-items: center; + display: flex; + gap: 0.5rem; + justify-content: space-between; + margin-bottom: 0.5rem; + margin-left: 1.5rem; + margin-right: 1rem; + + .search-term-input { + flex-basis: 75%; + } + + @include media-breakpoint-down(xs) { + flex-wrap: wrap; + + > span { + width: 100%; + } + + .search-term-input { + flex-basis: 100%; + } + } + } + .filter-tags { border-top: 1px solid rgb(16 22 26 / 40%); padding: 1rem 1rem 0 1rem; @@ -706,7 +748,7 @@ input[type="range"].zoom-slider { background-color: #202b33; position: sticky; top: 0; - z-index: 100; + z-index: 1; } td:first-child { @@ -875,8 +917,15 @@ input[type="range"].zoom-slider { } .filtered-list-toolbar { + align-items: center; + background-color: $body-bg; + gap: 0.5rem; justify-content: center; - margin-bottom: 0.5rem; + + // offset the main padding + margin-top: -0.5rem; + padding-bottom: 0.5rem; + padding-top: 0.5rem; & > .btn-group { flex-wrap: wrap; @@ -892,23 +941,13 @@ input[type="range"].zoom-slider { } } - .btn.display-mode-select { - margin-left: 0.5rem; + // set the width of the zoom-slider-container to prevent buttons moving when + // the slider appears/disappears + .zoom-slider-container { + min-width: 60px; } } -.sidebar-pane .filtered-list-toolbar { - flex-wrap: nowrap; - - & > .btn-group { - align-items: baseline; - } -} - -.search-term-input { - margin-right: 0.5rem; -} - .custom-field-filter { align-items: center; display: flex; @@ -940,14 +979,6 @@ input[type="range"].zoom-slider { } .sidebar { - // make controls slightly larger on mobile - @include media-breakpoint-down(xs) { - .btn, - .form-control { - font-size: 1.25rem; - } - } - .sidebar-search-container { display: flex; margin-bottom: 0.5rem; @@ -986,18 +1017,22 @@ input[type="range"].zoom-slider { } } -.pagination-footer { +.pagination-footer-container { background-color: transparent; bottom: $navbar-height; - margin: auto; - padding: 0.5rem 1rem 0.75rem; position: sticky; - width: fit-content; z-index: 10; @include media-breakpoint-up(sm) { bottom: 0; } +} + +.pagination-footer { + margin: auto; + padding: 0.5rem 1rem 0.75rem; + + width: fit-content; .pagination.btn-group { box-shadow: 0 8px 10px 2px rgb(0 0 0 / 30%); @@ -1012,3 +1047,120 @@ input[type="range"].zoom-slider { } } } + +// on very large screens, offset the margins to center the pagination controls +@media (min-width: 1800px) { + .sidebar-pane:not(.hide-sidebar) { + .filter-tags, + .pagination-index-container, + .pagination-footer-container { + margin-left: -$sidebar-width; + margin-right: 0; + } + } +} + +// hide sidebar Edit Filter button on larger screens +@include media-breakpoint-up(md) { + .sidebar .edit-filter-button { + display: none; + } +} + +// hide the search input field if the sidebar is open on smaller screens +@media (min-width: 576px) and (max-width: 1400px) { + .sidebar-pane:not(.hide-sidebar) .filtered-list-toolbar .search-term-input { + display: none; + } +} + +#more-criteria-popover { + box-shadow: 0 8px 10px 2px rgb(0 0 0 / 30%); + max-width: 400px; + padding: 0.25rem; +} + +// Duration slider styles +.duration-slider, +.age-slider-container { + padding: 0.5rem 0 1rem; + width: 100%; +} + +.duration-label-input, +.age-label-input { + background: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; + color: $text-color; + font-size: 0.875rem; + font-weight: 500; + padding: 0.125rem 0.25rem; + width: 4rem; + + &:hover { + border-color: $secondary; + } + + &:focus { + border-color: $primary; + outline: none; + } +} + +.duration-preset { + cursor: pointer; +} + +.selected-items-info { + align-items: center; + border: 1px solid $secondary; + display: flex; + gap: 0.25rem; + justify-content: flex-end; +} + +.scene-list-toolbar .selected-items-info, +.gallery-list-toolbar .selected-items-info { + justify-content: flex-start; +} + +// modify margins for toolbar within sidebar pane to accommodate toggle button +.sidebar-pane .filtered-list-toolbar { + margin-left: 40px; + margin-right: 40px; +} + +// on very large screens, offset the margins to center the toolbar +@media (min-width: 1800px) { + .sidebar-pane:not(.hide-sidebar) { + .filtered-list-toolbar { + margin-left: -$sidebar-width; + margin-right: 0; + } + } +} + +.item-list-container .filtered-list-toolbar.has-selection { + border-radius: 0.5rem; + margin-left: auto; + margin-right: auto; + padding-left: 0.5rem; + padding-right: 0.5rem; + position: sticky; + top: $navbar-height; + width: fit-content; + z-index: 10; + + @include media-breakpoint-down(xs) { + top: 0; + } +} + +.detail-body .filtered-list-toolbar.has-selection { + top: calc($sticky-detail-header-height + $navbar-height); + + @include media-breakpoint-down(xs) { + top: 0; + } +} diff --git a/ui/v2.5/src/components/List/util.ts b/ui/v2.5/src/components/List/util.ts index bb85145e7..707346848 100644 --- a/ui/v2.5/src/components/List/util.ts +++ b/ui/v2.5/src/components/List/util.ts @@ -1,17 +1,24 @@ -import { useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import Mousetrap from "mousetrap"; import { ListFilterModel } from "src/models/list-filter/filter"; import { useHistory, useLocation } from "react-router-dom"; import { isEqual, isFunction } from "lodash-es"; import { QueryResult } from "@apollo/client"; import { IHasID } from "src/utils/data"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { View } from "./views"; import { usePrevious } from "src/hooks/state"; import * as GQL from "src/core/generated-graphql"; import { DisplayMode } from "src/models/list-filter/types"; import { Criterion } from "src/models/list-filter/criteria/criterion"; +function locationEquals( + loc1: ReturnType | undefined, + loc2: ReturnType +) { + return loc1 && loc1.pathname === loc2.pathname && loc1.search === loc2.search; +} + export function useFilterURL( filter: ListFilterModel, setFilter: React.Dispatch>, @@ -24,6 +31,7 @@ export function useFilterURL( const history = useHistory(); const location = useLocation(); + const prevLocation = usePrevious(location); // when the filter changes, update the URL const updateFilter = useCallback( @@ -47,7 +55,8 @@ export function useFilterURL( // and updates the filter accordingly. useEffect(() => { // don't apply if active is false - if (!active) return; + // also don't apply if location is unchanged + if (!active || locationEquals(prevLocation, location)) return; // re-init to load default filter on empty new query params if (!location.search) { @@ -73,7 +82,8 @@ export function useFilterURL( }); }, [ active, - location.search, + prevLocation, + location, defaultFilter, setFilter, updateFilter, @@ -84,7 +94,7 @@ export function useFilterURL( } export function useDefaultFilter(emptyFilter: ListFilterModel, view?: View) { - const { configuration: config, loading } = useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const defaultFilter = useMemo(() => { if (view && config?.ui.defaultFilters?.[view]) { @@ -104,9 +114,9 @@ export function useDefaultFilter(emptyFilter: ListFilterModel, view?: View) { } }, [view, config?.ui.defaultFilters, emptyFilter]); - const retFilter = loading ? undefined : defaultFilter ?? emptyFilter; + const retFilter = defaultFilter ?? emptyFilter; - return { defaultFilter: retFilter, loading }; + return { defaultFilter: retFilter }; } function useEmptyFilter(props: { @@ -148,14 +158,14 @@ export function useFilterState( const emptyFilter = useEmptyFilter({ filterMode, defaultSort, config }); - const { defaultFilter, loading } = useDefaultFilter(emptyFilter, view); + const { defaultFilter } = useDefaultFilter(emptyFilter, view); const { setFilter } = useFilterURL(filter, setFilterState, { defaultFilter, active: useURL, }); - return { loading, filter, setFilter }; + return { filter, setFilter }; } export function useFilterOperations(props: { @@ -196,9 +206,12 @@ export function useFilterOperations(props: { [setFilter] ); - const clearAllCriteria = useCallback(() => { - setFilter((cv) => cv.clearCriteria()); - }, [setFilter]); + const clearAllCriteria = useCallback( + (includeSearchTerm = false) => { + setFilter((cv) => cv.clearCriteria(includeSearchTerm)); + }, + [setFilter] + ); return { setPage, @@ -216,6 +229,7 @@ export function useListKeyboardShortcuts(props: { pages?: number; onSelectAll?: () => void; onSelectNone?: () => void; + onInvertSelection?: () => void; }) { const { currentPage, @@ -224,6 +238,7 @@ export function useListKeyboardShortcuts(props: { pages = 0, onSelectAll, onSelectNone, + onInvertSelection, } = props; // set up hotkeys @@ -285,12 +300,14 @@ export function useListKeyboardShortcuts(props: { useEffect(() => { Mousetrap.bind("s a", () => onSelectAll?.()); Mousetrap.bind("s n", () => onSelectNone?.()); + Mousetrap.bind("s i", () => onInvertSelection?.()); return () => { Mousetrap.unbind("s a"); Mousetrap.unbind("s n"); + Mousetrap.unbind("s i"); }; - }, [onSelectAll, onSelectNone]); + }, [onSelectAll, onSelectNone, onInvertSelection]); } export function useListSelect(items: T[]) { @@ -407,6 +424,14 @@ export function useListSelect(items: T[]) { setLastClickedId(undefined); } + function onInvertSelection() { + setItemsSelected((prevSelected) => { + const selectedSet = new Set(prevSelected.map((item) => item.id)); + return items.filter((item) => !selectedSet.has(item.id)); + }); + setLastClickedId(undefined); + } + // TODO - this is for backwards compatibility const getSelected = useCallback(() => itemsSelected, [itemsSelected]); @@ -420,6 +445,7 @@ export function useListSelect(items: T[]) { onSelectChange, onSelectAll, onSelectNone, + onInvertSelection, hasSelection, }; } diff --git a/ui/v2.5/src/components/MainNavbar.tsx b/ui/v2.5/src/components/MainNavbar.tsx index 59f8e51aa..ac1be2c13 100644 --- a/ui/v2.5/src/components/MainNavbar.tsx +++ b/ui/v2.5/src/components/MainNavbar.tsx @@ -11,7 +11,7 @@ import { MessageDescriptor, useIntl, } from "react-intl"; -import { Nav, Navbar, Button, Fade } from "react-bootstrap"; +import { Nav, Navbar, Button } from "react-bootstrap"; import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { LinkContainer } from "react-router-bootstrap"; import { Link, NavLink, useLocation, useHistory } from "react-router-dom"; @@ -19,7 +19,7 @@ import Mousetrap from "mousetrap"; import SessionUtils from "src/utils/session"; import { Icon } from "src/components/Shared/Icon"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ManualStateContext } from "./Help/context"; import { SettingsButton } from "./SettingsButton"; import { @@ -132,7 +132,6 @@ const allMenuItems: IMenuItem[] = [ href: "/galleries", icon: faImages, hotkey: "g l", - userCreatable: true, }, { name: "performers", @@ -181,7 +180,7 @@ const MainNavbarUtilityItems = PatchComponent( export const MainNavbar: React.FC = () => { const history = useHistory(); const location = useLocation(); - const { configuration, loading } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const { openManual } = React.useContext(ManualStateContext); const [expanded, setExpanded] = useState(false); @@ -359,35 +358,31 @@ export const MainNavbar: React.FC = () => { ref={navbarRef} > - - <> - - {menuItems.map(({ href, icon, message }) => ( - - - - - - ))} - - - - + + {menuItems.map(({ href, icon, message }) => ( + + + + + + ))} + + diff --git a/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx b/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx index 71fcbedd9..677ac3aa1 100644 --- a/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx +++ b/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx @@ -27,6 +27,8 @@ import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput"; import { faPencilAlt } from "@fortawesome/free-solid-svg-icons"; import * as FormUtils from "src/utils/form"; import { CountrySelect } from "../Shared/CountrySelect"; +import { useConfigurationContext } from "src/hooks/Config"; +import cx from "classnames"; interface IListOperationProps { selected: GQL.SlimPerformerDataFragment[]; @@ -61,6 +63,10 @@ export const EditPerformersDialog: React.FC = ( ) => { const intl = useIntl(); const Toast = useToast(); + + const { configuration } = useConfigurationContext(); + const { sfwContentMode } = configuration.interface; + const [tagIds, setTagIds] = useState({ mode: GQL.BulkUpdateIdMode.Add, }); @@ -204,7 +210,7 @@ export const EditPerformersDialog: React.FC = ( setter: (newValue: string | undefined) => void ) { return ( - + @@ -218,9 +224,13 @@ export const EditPerformersDialog: React.FC = ( } function render() { + // sfw class needs to be set because it is outside body + return ( = ( }} isRunning={isUpdating} > - + {FormUtils.renderLabel({ title: intl.formatMessage({ id: "rating" }), })} @@ -322,7 +332,7 @@ export const EditPerformersDialog: React.FC = ( setPenisLength(v) )} - + diff --git a/ui/v2.5/src/components/Performers/GenderIcon.tsx b/ui/v2.5/src/components/Performers/GenderIcon.tsx index 516e70dbd..6f40a2206 100644 --- a/ui/v2.5/src/components/Performers/GenderIcon.tsx +++ b/ui/v2.5/src/components/Performers/GenderIcon.tsx @@ -3,6 +3,7 @@ import { faVenus, faTransgenderAlt, faMars, + faNonBinary, } from "@fortawesome/free-solid-svg-icons"; import * as GQL from "src/core/generated-graphql"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -13,21 +14,34 @@ interface IIconProps { className?: string; } +function genderIcon(gender: GQL.GenderEnum) { + switch (gender) { + case GQL.GenderEnum.Male: + return faMars; + case GQL.GenderEnum.Female: + return faVenus; + case GQL.GenderEnum.NonBinary: + return faNonBinary; + default: + return faTransgenderAlt; + } +} + const GenderIcon: React.FC = ({ gender, className }) => { const intl = useIntl(); if (gender) { - const icon = - gender === GQL.GenderEnum.Male - ? faMars - : gender === GQL.GenderEnum.Female - ? faVenus - : faTransgenderAlt; + const icon = genderIcon(gender); + + // new version of fontawesome doesn't seem to support titles on icons, so adding it + // to a span instead return ( - + + + ); } return null; diff --git a/ui/v2.5/src/components/Performers/PerformerCard.tsx b/ui/v2.5/src/components/Performers/PerformerCard.tsx index 02e2a68fd..5f7a26d42 100644 --- a/ui/v2.5/src/components/Performers/PerformerCard.tsx +++ b/ui/v2.5/src/components/Performers/PerformerCard.tsx @@ -6,7 +6,6 @@ import NavUtils from "src/utils/navigation"; import TextUtils from "src/utils/text"; import { GridCard } from "../Shared/GridCard/GridCard"; import { CountryFlag } from "../Shared/CountryFlag"; -import { SweatDrops } from "../Shared/SweatDrops"; import { HoverPopover } from "../Shared/HoverPopover"; import { Icon } from "../Shared/Icon"; import { TagLink } from "../Shared/TagLink"; @@ -17,12 +16,16 @@ import { } from "src/models/list-filter/criteria/criterion"; import { PopoverCountButton } from "../Shared/PopoverCountButton"; import GenderIcon from "./GenderIcon"; -import { faTag } from "@fortawesome/free-solid-svg-icons"; +import { faLink, faTag } from "@fortawesome/free-solid-svg-icons"; +import { faInstagram, faTwitter } from "@fortawesome/free-brands-svg-icons"; import { RatingBanner } from "../Shared/RatingBanner"; import { usePerformerUpdate } from "src/core/StashService"; import { ILabeledId } from "src/models/list-filter/types"; import { FavoriteIcon } from "../Shared/FavoriteIcon"; import { PatchComponent } from "src/patch"; +import { ExternalLinksButton } from "../Shared/ExternalLinksButton"; +import { useConfigurationContext } from "src/hooks/Config"; +import { OCounterButton } from "../Shared/CountButton"; export interface IPerformerCardExtraCriteria { scenes?: ModifierCriterion[]; @@ -100,16 +103,7 @@ const PerformerCardPopovers: React.FC = PatchComponent( function maybeRenderOCounter() { if (!performer.o_counter) return; - return ( -
      - -
      - ); + return ; } function maybeRenderTagPopoverButton() { @@ -176,6 +170,8 @@ const PerformerCardPopovers: React.FC = PatchComponent( const PerformerCardOverlays: React.FC = PatchComponent( "PerformerCard.Overlays", ({ performer }) => { + const { configuration } = useConfigurationContext(); + const uiConfig = configuration?.ui; const [updatePerformer] = usePerformerUpdate(); function onToggleFavorite(v: boolean) { @@ -215,6 +211,63 @@ const PerformerCardOverlays: React.FC = PatchComponent( } } + function maybeRenderLinks() { + if (!uiConfig?.showLinksOnPerformerCard) { + return; + } + + if (performer.urls && performer.urls.length > 0) { + const twitter = performer.urls.filter((u) => + u.match(/https?:\/\/(?:www\.)?(?:twitter|x).com\//) + ); + const instagram = performer.urls.filter((u) => + u.match(/https?:\/\/(?:www\.)?instagram.com\//) + ); + const others = performer.urls.filter( + (u) => !twitter.includes(u) && !instagram.includes(u) + ); + + return ( +
      + {twitter.length > 0 && ( + + )} + {instagram.length > 0 && ( + + )} + {others.length > 0 && ( + + )} +
      + ); + } + } + return ( <> = PatchComponent( className="hide-not-favorite" /> {maybeRenderRatingBanner()} + {maybeRenderLinks()} {maybeRenderFlag()} ); diff --git a/ui/v2.5/src/components/Performers/PerformerCardGrid.tsx b/ui/v2.5/src/components/Performers/PerformerCardGrid.tsx index f8c02d5a7..9d10c0dd1 100644 --- a/ui/v2.5/src/components/Performers/PerformerCardGrid.tsx +++ b/ui/v2.5/src/components/Performers/PerformerCardGrid.tsx @@ -5,6 +5,7 @@ import { useCardWidth, useContainerDimensions, } from "../Shared/GridCard/GridCard"; +import { PatchComponent } from "src/patch"; interface IPerformerCardGrid { performers: GQL.PerformerDataFragment[]; @@ -16,32 +17,29 @@ interface IPerformerCardGrid { const zoomWidths = [240, 300, 375, 470]; -export const PerformerCardGrid: React.FC = ({ - performers, - selectedIds, - zoomIndex, - onSelectChange, - extraCriteria, -}) => { - const [componentRef, { width: containerWidth }] = useContainerDimensions(); - const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); +export const PerformerCardGrid: React.FC = PatchComponent( + "PerformerCardGrid", + ({ performers, selectedIds, zoomIndex, onSelectChange, extraCriteria }) => { + const [componentRef, { width: containerWidth }] = useContainerDimensions(); + const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); - return ( -
      - {performers.map((p) => ( - 0} - selected={selectedIds.has(p.id)} - onSelectedChanged={(selected: boolean, shiftKey: boolean) => - onSelectChange(p.id, selected, shiftKey) - } - extraCriteria={extraCriteria} - /> - ))} -
      - ); -}; + return ( +
      + {performers.map((p) => ( + 0} + selected={selectedIds.has(p.id)} + onSelectedChanged={(selected: boolean, shiftKey: boolean) => + onSelectChange(p.id, selected, shiftKey) + } + extraCriteria={extraCriteria} + /> + ))} +
      + ); + } +); diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index 03530c52e..92a563a81 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState } from "react"; -import { Tabs, Tab, Col, Row } from "react-bootstrap"; -import { useIntl } from "react-intl"; +import { Button, Tabs, Tab, Col, Row } from "react-bootstrap"; +import { FormattedMessage, useIntl } from "react-intl"; import { useHistory, Redirect, RouteComponentProps } from "react-router-dom"; import { Helmet } from "react-helmet"; import cx from "classnames"; @@ -16,7 +16,7 @@ import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; import { ErrorMessage } from "src/components/Shared/ErrorMessage"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { useToast } from "src/hooks/Toast"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { CompressedPerformerDetailsPanel, @@ -28,6 +28,7 @@ import { PerformerGroupsPanel } from "./PerformerGroupsPanel"; import { PerformerImagesPanel } from "./PerformerImagesPanel"; import { PerformerAppearsWithPanel } from "./performerAppearsWithPanel"; import { PerformerEditPanel } from "./PerformerEditPanel"; +import { PerformerMergeModal } from "../PerformerMergeDialog"; import { PerformerSubmitButton } from "./PerformerSubmitButton"; import { useRatingKeybinds } from "src/hooks/keybinds"; import { DetailImage } from "src/components/Shared/DetailImage"; @@ -47,6 +48,8 @@ import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage"; import { LightboxLink } from "src/hooks/Lightbox/LightboxLink"; import { PatchComponent } from "src/patch"; import { ILightboxImage } from "src/hooks/Lightbox/types"; +import { goBackOrReplace } from "src/utils/history"; +import { OCounterButton } from "src/components/Shared/CountButton"; interface IProps { performer: GQL.PerformerDataFragment; @@ -238,7 +241,7 @@ const PerformerPage: React.FC = PatchComponent( const intl = useIntl(); // Configuration settings - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const uiConfig = configuration?.ui; const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; const enableBackgroundImage = @@ -248,6 +251,7 @@ const PerformerPage: React.FC = PatchComponent( const [collapsed, setCollapsed] = useState(!showAllDetails); const [isEditing, setIsEditing] = useState(false); + const [isMerging, setIsMerging] = useState(false); const [image, setImage] = useState(); const [encodingImage, setEncodingImage] = useState(false); const loadStickyHeader = useLoadStickyHeader(); @@ -283,6 +287,33 @@ const PerformerPage: React.FC = PatchComponent( } } + function renderMergeButton() { + return ( + + ); + } + + function renderMergeDialog() { + if (!performer.id) return; + return ( + { + setIsMerging(false); + if (mergedId !== undefined && mergedId !== performer.id) { + // By default, the merge destination is the current performer, but + // the user can change it, in which case we need to redirect. + history.replace(`/performers/${mergedId}`); + } + }} + performers={[performer]} + /> + ); + } + useRatingKeybinds( true, configuration?.ui.ratingSystemOptions?.type, @@ -330,7 +361,7 @@ const PerformerPage: React.FC = PatchComponent( return; } - history.goBack(); + goBackOrReplace(history, "/performers"); } function toggleEditing(value?: boolean) { @@ -422,12 +453,17 @@ const PerformerPage: React.FC = PatchComponent( - setRating(value)} - clickToRate - withoutContext - /> +
      + setRating(value)} + clickToRate + withoutContext + /> + {!!performer.o_counter && ( + + )} +
      {!isEditing && ( = PatchComponent( onImageChange={() => {}} classNames="mb-2" customButtons={ -
      - -
      + <> + {renderMergeButton()} +
      + +
      + } > @@ -492,6 +531,7 @@ const PerformerPage: React.FC = PatchComponent(
      + {renderMergeDialog()}
      ); } diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx index 7726ed9bc..e6d77761e 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx @@ -23,12 +23,14 @@ const PerformerCreate: React.FC = () => { const [createPerformer] = usePerformerCreate(); - async function onSave(input: GQL.PerformerCreateInput) { + async function onSave(input: GQL.PerformerCreateInput, andNew?: boolean) { const result = await createPerformer({ variables: { input }, }); if (result.data?.performerCreate) { - history.push(`/performers/${result.data.performerCreate.id}`); + if (!andNew) { + history.push(`/performers/${result.data.performerCreate.id}`); + } Toast.success( intl.formatMessage( { id: "toast.created_entity" }, diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx index d01709287..95e03ff8b 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -89,7 +89,10 @@ export const PerformerDetailsPanel: React.FC = } title={ !fullWidth - ? TextUtils.formatDate(intl, performer.birthdate ?? undefined) + ? TextUtils.formatFuzzyDate( + intl, + performer.birthdate ?? undefined + ) : "" } fullWidth={fullWidth} @@ -218,7 +221,7 @@ export const CompressedPerformerDetailsPanel: React.FC = / ; isVisible: boolean; - onSubmit: (performer: GQL.PerformerCreateInput) => Promise; + onSubmit: ( + performer: GQL.PerformerCreateInput, + andNew?: boolean + ) => Promise; onCancel?: () => void; setImage: (image?: string | null) => void; setEncodingImage: (loading: boolean) => void; @@ -88,6 +92,8 @@ export const PerformerEditPanel: React.FC = ({ // Editing state const [scraper, setScraper] = useState(); const [isScraperModalOpen, setIsScraperModalOpen] = useState(false); + const [isStashIDSearchOpen, setIsStashIDSearchOpen] = + useState(false); // Network state const [isLoading, setIsLoading] = useState(false); @@ -97,14 +103,14 @@ export const PerformerEditPanel: React.FC = ({ const [scrapedPerformer, setScrapedPerformer] = useState(); - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const intl = useIntl(); const schema = yup.object({ name: yup.string().required(), disambiguation: yup.string().ensure(), - alias_list: yupUniqueAliases(intl, "name"), + alias_list: yupRequiredStringArray(intl).defined(), gender: yupInputEnum(GQL.GenderEnum).nullable().defined(), birthdate: yupDateString(intl), death_date: yupDateString(intl), @@ -342,10 +348,10 @@ export const PerformerEditPanel: React.FC = ({ ImageUtils.onImageChange(event, onImageLoad); } - async function onSave(input: InputValues) { + async function onSave(input: InputValues, andNew?: boolean) { setIsLoading(true); try { - await onSubmit(input); + await onSubmit(input, andNew); formik.resetForm(); } catch (e) { Toast.error(e); @@ -353,6 +359,15 @@ export const PerformerEditPanel: React.FC = ({ setIsLoading(false); } + async function onSaveAndNewClick() { + const { values } = formik; + const input = { + ...schema.cast(values), + custom_fields: customFieldInput(isNew, values.custom_fields), + }; + onSave(input, true); + } + // set up hotkeys useEffect(() => { if (isVisible) { @@ -569,6 +584,14 @@ export const PerformerEditPanel: React.FC = ({ setScraper(undefined); } + function onStashIDSelected(item?: GQL.StashIdInput) { + if (!item) return; + formik.setFieldValue( + "stash_ids", + addUpdateStashID(formik.values.stash_ids, item) + ); + } + function renderButtons(classNames: string) { return (
      @@ -592,17 +615,33 @@ export const PerformerEditPanel: React.FC = ({
      - + {isNew ? ( + formik.submitForm()} + > + onSaveAndNewClick()}> + + + + ) : ( + + )}
      ); } @@ -659,6 +698,20 @@ export const PerformerEditPanel: React.FC = ({ <> {renderScrapeModal()} {maybeRenderScrapeDialog()} + {isStashIDSearchOpen && ( + s.endpoint + )} + onSelectItem={(item) => { + onStashIDSelected(item); + setIsStashIDSearchOpen(false); + }} + initialQuery={performer.name ?? ""} + /> + )} = ({ {renderInputField("name")} {renderInputField("disambiguation")} - {renderStringListField("alias_list", "aliases")} + {renderStringListField("alias_list", "aliases", { orderable: false })} {renderSelectField("gender", stringGenderMap)} @@ -701,7 +754,21 @@ export const PerformerEditPanel: React.FC = ({ {renderInputField("details", "textarea")} {renderTagsField()} - {renderStashIDsField("stash_ids", "performers")} + {renderStashIDsField( + "stash_ids", + "performers", + "stash_ids", + undefined, + + )}
      diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerGalleriesPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerGalleriesPanel.tsx index 5a9d0b81d..44b0401e9 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerGalleriesPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerGalleriesPanel.tsx @@ -1,6 +1,6 @@ import React from "react"; import * as GQL from "src/core/generated-graphql"; -import { GalleryList } from "src/components/Galleries/GalleryList"; +import { FilteredGalleryList } from "src/components/Galleries/GalleryList"; import { usePerformerFilterHook } from "src/core/performers"; import { View } from "src/components/List/views"; import { PatchComponent } from "src/patch"; @@ -14,7 +14,7 @@ export const PerformerGalleriesPanel: React.FC = PatchComponent("PerformerGalleriesPanel", ({ active, performer }) => { const filterHook = usePerformerFilterHook(performer); return ( - , onChange: (value: ScrapeResult) => void ) { return ( renderScrapedGender(result)} - renderNewField={() => - renderScrapedGender(result, true, (value) => - onChange(result.cloneWithValue(value)) - ) - } + originalField={renderScrapedGender(result)} + newField={renderScrapedGender(result, true, (value) => + onChange(result.cloneWithValue(value)) + )} onChange={onChange} /> ); @@ -105,7 +104,7 @@ function renderScrapedCircumcised( ); } -function renderScrapedCircumcisedRow( +export function renderScrapedCircumcisedRow( title: string, result: ScrapeResult, onChange: (value: ScrapeResult) => void @@ -113,13 +112,12 @@ function renderScrapedCircumcisedRow( return ( renderScrapedCircumcised(result)} - renderNewField={() => - renderScrapedCircumcised(result, true, (value) => - onChange(result.cloneWithValue(value)) - ) - } + originalField={renderScrapedCircumcised(result)} + newField={renderScrapedCircumcised(result, true, (value) => + onChange(result.cloneWithValue(value)) + )} onChange={onChange} /> ); @@ -146,6 +144,22 @@ export const PerformerScrapeDialog: React.FC = ( return; } + // #6257 - it is possible (though unsupported) to have multiple stash IDs for the same + // endpoint; in that case, we should prefer the one matching the scraped remote site ID + // if it exists + const stashIDs = (props.performer.stash_ids ?? []).filter( + (s) => s.endpoint === endpoint + ); + if (stashIDs.length > 1 && props.scraped.remote_site_id) { + const matchingID = stashIDs.find( + (s) => s.stash_id === props.scraped.remote_site_id + ); + if (matchingID) { + return matchingID.stash_id; + } + } + + // otherwise, return the first stash ID for the endpoint return props.performer.stash_ids?.find((s) => s.endpoint === endpoint) ?.stash_id; } @@ -300,9 +314,10 @@ export const PerformerScrapeDialog: React.FC = ( ) ); - const { tags, newTags, scrapedTagsRow } = useScrapedTags( + const { tags, newTags, scrapedTagsRow, linkDialog } = useScrapedTags( props.performerTags, - props.scraped.tags + props.scraped.tags, + endpoint ); const [image, setImage] = useState>( @@ -385,16 +400,19 @@ export const PerformerScrapeDialog: React.FC = ( return ( <> setName(value)} /> setDisambiguation(value)} /> setAliases(value)} @@ -405,46 +423,55 @@ export const PerformerScrapeDialog: React.FC = ( (value) => setGender(value) )} setBirthdate(value)} /> setDeathDate(value)} /> setEthnicity(value)} /> setCountry(value)} /> setHairColor(value)} /> setEyeColor(value)} /> setWeight(value)} /> setHeight(value)} /> setPenisLength(value)} @@ -455,42 +482,50 @@ export const PerformerScrapeDialog: React.FC = ( (value) => setCircumcised(value) )} setMeasurements(value)} /> setFakeTits(value)} /> setCareerLength(value)} /> setTattoos(value)} /> setPiercings(value)} /> setURLs(value)} /> setDetails(value)} /> {scrapedTagsRow} = ( onChange={(value) => setImage(value)} /> = ( ); } + if (linkDialog) { + return linkDialog; + } + return ( { props.onClose(apply ? makeNewScrapedItem() : undefined); }} - /> + > + {renderScrapeRows()} + ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerList.tsx b/ui/v2.5/src/components/Performers/PerformerList.tsx index d11de6d96..ef465fb38 100644 --- a/ui/v2.5/src/components/Performers/PerformerList.tsx +++ b/ui/v2.5/src/components/Performers/PerformerList.tsx @@ -21,7 +21,10 @@ import { EditPerformersDialog } from "./EditPerformersDialog"; import { cmToImperial, cmToInches, kgToLbs } from "src/utils/units"; import TextUtils from "src/utils/text"; import { PerformerCardGrid } from "./PerformerCardGrid"; +import { PerformerMergeModal } from "./PerformerMergeDialog"; import { View } from "../List/views"; +import { IItemListOperation } from "../List/FilteredListToolbar"; +import { PatchComponent } from "src/patch"; function getItems(result: GQL.FindPerformersQueryResult) { return result?.data?.findPerformers?.performers ?? []; @@ -159,184 +162,223 @@ interface IPerformerList { view?: View; alterQuery?: boolean; extraCriteria?: IPerformerCardExtraCriteria; + extraOperations?: IItemListOperation[]; } -export const PerformerList: React.FC = ({ - filterHook, - view, - alterQuery, - extraCriteria, -}) => { - const intl = useIntl(); - const history = useHistory(); - const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); - const [isExportAll, setIsExportAll] = useState(false); +export const PerformerList: React.FC = PatchComponent( + "PerformerList", + ({ filterHook, view, alterQuery, extraCriteria, extraOperations = [] }) => { + const intl = useIntl(); + const history = useHistory(); + const [mergePerformers, setMergePerformers] = useState< + GQL.SelectPerformerDataFragment[] | undefined + >(undefined); + const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); + const [isExportAll, setIsExportAll] = useState(false); - const filterMode = GQL.FilterMode.Performers; + const filterMode = GQL.FilterMode.Performers; - const otherOperations = [ - { - text: intl.formatMessage({ id: "actions.open_random" }), - onClick: openRandom, - }, - { - text: intl.formatMessage({ id: "actions.export" }), - onClick: onExport, - isDisplayed: showWhenSelected, - }, - { - text: intl.formatMessage({ id: "actions.export_all" }), - onClick: onExportAll, - }, - ]; + const otherOperations = [ + ...extraOperations, + { + text: intl.formatMessage({ id: "actions.open_random" }), + onClick: openRandom, + }, + { + text: `${intl.formatMessage({ id: "actions.merge" })}…`, + onClick: merge, + isDisplayed: showWhenSelected, + }, + { + text: intl.formatMessage({ id: "actions.export" }), + onClick: onExport, + isDisplayed: showWhenSelected, + }, + { + text: intl.formatMessage({ id: "actions.export_all" }), + onClick: onExportAll, + }, + ]; - function addKeybinds( - result: GQL.FindPerformersQueryResult, - filter: ListFilterModel - ) { - Mousetrap.bind("p r", () => { - openRandom(result, filter); - }); + function addKeybinds( + result: GQL.FindPerformersQueryResult, + filter: ListFilterModel + ) { + Mousetrap.bind("p r", () => { + openRandom(result, filter); + }); - return () => { - Mousetrap.unbind("p r"); - }; - } + return () => { + Mousetrap.unbind("p r"); + }; + } - async function openRandom( - result: GQL.FindPerformersQueryResult, - filter: ListFilterModel - ) { - if (result.data?.findPerformers) { - const { count } = result.data.findPerformers; - const index = Math.floor(Math.random() * count); - const filterCopy = cloneDeep(filter); - filterCopy.itemsPerPage = 1; - filterCopy.currentPage = index + 1; - const singleResult = await queryFindPerformers(filterCopy); - if (singleResult.data.findPerformers.performers.length === 1) { - const { id } = singleResult.data.findPerformers.performers[0]!; - history.push(`/performers/${id}`); + async function openRandom( + result: GQL.FindPerformersQueryResult, + filter: ListFilterModel + ) { + if (result.data?.findPerformers) { + const { count } = result.data.findPerformers; + const index = Math.floor(Math.random() * count); + const filterCopy = cloneDeep(filter); + filterCopy.itemsPerPage = 1; + filterCopy.currentPage = index + 1; + const singleResult = await queryFindPerformers(filterCopy); + if (singleResult.data.findPerformers.performers.length === 1) { + const { id } = singleResult.data.findPerformers.performers[0]!; + history.push(`/performers/${id}`); + } } } - } - async function onExport() { - setIsExportAll(false); - setIsExportDialogOpen(true); - } + async function merge( + result: GQL.FindPerformersQueryResult, + filter: ListFilterModel, + selectedIds: Set + ) { + const selected = + result.data?.findPerformers.performers.filter((p) => + selectedIds.has(p.id) + ) ?? []; + setMergePerformers(selected); + } - async function onExportAll() { - setIsExportAll(true); - setIsExportDialogOpen(true); - } + async function onExport() { + setIsExportAll(false); + setIsExportDialogOpen(true); + } - function renderContent( - result: GQL.FindPerformersQueryResult, - filter: ListFilterModel, - selectedIds: Set, - onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void - ) { - function maybeRenderPerformerExportDialog() { - if (isExportDialogOpen) { - return ( - <> - , + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void + ) { + function renderMergeDialog() { + if (mergePerformers) { + return ( + { + setMergePerformers(undefined); + if (mergedId) { + history.push(`/performers/${mergedId}`); + } }} - onClose={() => setIsExportDialogOpen(false)} + show /> - - ); + ); + } } + + function maybeRenderPerformerExportDialog() { + if (isExportDialogOpen) { + return ( + <> + setIsExportDialogOpen(false)} + /> + + ); + } + } + + function renderPerformers() { + if (!result.data?.findPerformers) return; + + if (filter.displayMode === DisplayMode.Grid) { + return ( + + ); + } + if (filter.displayMode === DisplayMode.List) { + return ( + + ); + } + if (filter.displayMode === DisplayMode.Tagger) { + return ( + + ); + } + } + + return ( + <> + {renderMergeDialog()} + {maybeRenderPerformerExportDialog()} + {renderPerformers()} + + ); } - function renderPerformers() { - if (!result.data?.findPerformers) return; + function renderEditDialog( + selectedPerformers: GQL.SlimPerformerDataFragment[], + onClose: (applied: boolean) => void + ) { + return ( + + ); + } - if (filter.displayMode === DisplayMode.Grid) { - return ( - - ); - } - if (filter.displayMode === DisplayMode.List) { - return ( - - ); - } - if (filter.displayMode === DisplayMode.Tagger) { - return ( - - ); - } + function renderDeleteDialog( + selectedPerformers: GQL.SlimPerformerDataFragment[], + onClose: (confirmed: boolean) => void + ) { + return ( + + ); } return ( - <> - {maybeRenderPerformerExportDialog()} - {renderPerformers()} - - ); - } - - function renderEditDialog( - selectedPerformers: GQL.SlimPerformerDataFragment[], - onClose: (applied: boolean) => void - ) { - return ( - - ); - } - - function renderDeleteDialog( - selectedPerformers: GQL.SlimPerformerDataFragment[], - onClose: (confirmed: boolean) => void - ) { - return ( - - ); - } - - return ( - - - - ); -}; + selectable + > + + + ); + } +); diff --git a/ui/v2.5/src/components/Performers/PerformerListTable.tsx b/ui/v2.5/src/components/Performers/PerformerListTable.tsx index 6a3818824..58538e7e2 100644 --- a/ui/v2.5/src/components/Performers/PerformerListTable.tsx +++ b/ui/v2.5/src/components/Performers/PerformerListTable.tsx @@ -116,7 +116,7 @@ export const PerformerListTable: React.FC = ( diff --git a/ui/v2.5/src/components/Performers/PerformerMergeDialog.tsx b/ui/v2.5/src/components/Performers/PerformerMergeDialog.tsx new file mode 100644 index 000000000..834d2ac76 --- /dev/null +++ b/ui/v2.5/src/components/Performers/PerformerMergeDialog.tsx @@ -0,0 +1,876 @@ +import { Form, Col, Row, Button } from "react-bootstrap"; +import React, { useEffect, useMemo, useState } from "react"; +import * as GQL from "src/core/generated-graphql"; +import { Icon } from "../Shared/Icon"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; +import { + circumcisedToString, + stringToCircumcised, +} from "src/utils/circumcised"; +import * as FormUtils from "src/utils/form"; +import { genderToString, stringToGender } from "src/utils/gender"; +import ImageUtils from "src/utils/image"; +import { + mutatePerformerMerge, + queryFindPerformersByID, +} from "src/core/StashService"; +import { FormattedMessage, useIntl } from "react-intl"; +import { useToast } from "src/hooks/Toast"; +import { faExchangeAlt, faSignInAlt } from "@fortawesome/free-solid-svg-icons"; +import { ScrapeDialog } from "../Shared/ScrapeDialog/ScrapeDialog"; +import { + ScrapedImageRow, + ScrapedInputGroupRow, + ScrapedStringListRow, + ScrapedTextAreaRow, +} from "../Shared/ScrapeDialog/ScrapeDialogRow"; +import { ModalComponent } from "../Shared/Modal"; +import { sortStoredIdObjects } from "src/utils/data"; +import { + ObjectListScrapeResult, + ScrapeResult, + ZeroableScrapeResult, + hasScrapedValues, +} from "../Shared/ScrapeDialog/scrapeResult"; +import { ScrapedTagsRow } from "../Shared/ScrapeDialog/ScrapedObjectsRow"; +import { + renderScrapedGenderRow, + renderScrapedCircumcisedRow, +} from "./PerformerDetails/PerformerScrapeDialog"; +import { PerformerSelect } from "./PerformerSelect"; +import { uniq } from "lodash-es"; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +type CustomFieldScrapeResults = Map>; + +// There are a bunch of similar functions in PerformerScrapeDialog, but since we don't support +// scraping custom fields, this one is only needed here. The `renderScraped` naming is kept the same +// for consistency. +function renderScrapedCustomFieldRows( + results: CustomFieldScrapeResults, + onChange: (newCustomFields: CustomFieldScrapeResults) => void +) { + return ( + <> + {Array.from(results.entries()).map(([field, result]) => { + const fieldName = `custom_${field}`; + return ( + { + const newResults = new Map(results); + newResults.set(field, newResult); + onChange(newResults); + }} + /> + ); + })} + + ); +} + +type MergeOptions = { + values: GQL.PerformerUpdateInput; +}; + +interface IPerformerMergeDetailsProps { + sources: GQL.PerformerDataFragment[]; + dest: GQL.PerformerDataFragment; + onClose: (options?: MergeOptions) => void; +} + +const PerformerMergeDetails: React.FC = ({ + sources, + dest, + onClose, +}) => { + const intl = useIntl(); + + const [loading, setLoading] = useState(true); + + const [name, setName] = useState>( + new ScrapeResult(dest.name) + ); + const [disambiguation, setDisambiguation] = useState>( + new ScrapeResult(dest.disambiguation) + ); + const [aliases, setAliases] = useState>( + new ScrapeResult(dest.alias_list) + ); + const [birthdate, setBirthdate] = useState>( + new ScrapeResult(dest.birthdate) + ); + const [deathDate, setDeathDate] = useState>( + new ScrapeResult(dest.death_date) + ); + const [ethnicity, setEthnicity] = useState>( + new ScrapeResult(dest.ethnicity) + ); + const [country, setCountry] = useState>( + new ScrapeResult(dest.country) + ); + const [hairColor, setHairColor] = useState>( + new ScrapeResult(dest.hair_color) + ); + const [eyeColor, setEyeColor] = useState>( + new ScrapeResult(dest.eye_color) + ); + const [height, setHeight] = useState>( + new ScrapeResult(dest.height_cm?.toString()) + ); + const [weight, setWeight] = useState>( + new ScrapeResult(dest.weight?.toString()) + ); + const [penisLength, setPenisLength] = useState>( + new ScrapeResult(dest.penis_length?.toString()) + ); + const [measurements, setMeasurements] = useState>( + new ScrapeResult(dest.measurements) + ); + const [fakeTits, setFakeTits] = useState>( + new ScrapeResult(dest.fake_tits) + ); + const [careerLength, setCareerLength] = useState>( + new ScrapeResult(dest.career_length) + ); + const [tattoos, setTattoos] = useState>( + new ScrapeResult(dest.tattoos) + ); + const [piercings, setPiercings] = useState>( + new ScrapeResult(dest.piercings) + ); + const [urls, setURLs] = useState>( + new ScrapeResult(dest.urls) + ); + const [gender, setGender] = useState>( + new ScrapeResult(genderToString(dest.gender)) + ); + const [circumcised, setCircumcised] = useState>( + new ScrapeResult(circumcisedToString(dest.circumcised)) + ); + const [details, setDetails] = useState>( + new ScrapeResult(dest.details) + ); + const [tags, setTags] = useState>( + new ObjectListScrapeResult( + sortStoredIdObjects(dest.tags.map(idToStoredID)) + ) + ); + + const [image, setImage] = useState>( + new ScrapeResult(dest.image_path) + ); + + const [customFields, setCustomFields] = useState( + new Map() + ); + + function idToStoredID(o: { id: string; name: string }) { + return { + stored_id: o.id, + name: o.name, + }; + } + + // calculate the values for everything + // uses the first set value for single value fields, and combines all + useEffect(() => { + async function loadImages() { + const src = sources.find((s) => s.image_path); + if (!dest.image_path || !src) return; + + setLoading(true); + + const destData = await ImageUtils.imageToDataURL(dest.image_path); + const srcData = await ImageUtils.imageToDataURL(src.image_path!); + + // keep destination image by default + const useNewValue = false; + setImage(new ScrapeResult(destData, srcData, useNewValue)); + + setLoading(false); + } + + setName( + new ScrapeResult(dest.name, sources.find((s) => s.name)?.name, !dest.name) + ); + setDisambiguation( + new ScrapeResult( + dest.disambiguation, + sources.find((s) => s.disambiguation)?.disambiguation, + !dest.disambiguation + ) + ); + + // default alias list should be the existing aliases, plus the names of all sources, + // plus all source aliases, deduplicated + const allAliases = uniq( + dest.alias_list.concat( + sources.map((s) => s.name), + sources.flatMap((s) => s.alias_list) + ) + ); + + setAliases( + new ScrapeResult(dest.alias_list, allAliases, !!allAliases.length) + ); + setBirthdate( + new ScrapeResult( + dest.birthdate, + sources.find((s) => s.birthdate)?.birthdate, + !dest.birthdate + ) + ); + setDeathDate( + new ScrapeResult( + dest.death_date, + sources.find((s) => s.death_date)?.death_date, + !dest.death_date + ) + ); + setEthnicity( + new ScrapeResult( + dest.ethnicity, + sources.find((s) => s.ethnicity)?.ethnicity, + !dest.ethnicity + ) + ); + setCountry( + new ScrapeResult( + dest.country, + sources.find((s) => s.country)?.country, + !dest.country + ) + ); + setHairColor( + new ScrapeResult( + dest.hair_color, + sources.find((s) => s.hair_color)?.hair_color, + !dest.hair_color + ) + ); + setEyeColor( + new ScrapeResult( + dest.eye_color, + sources.find((s) => s.eye_color)?.eye_color, + !dest.eye_color + ) + ); + setHeight( + new ScrapeResult( + dest.height_cm?.toString(), + sources.find((s) => s.height_cm)?.height_cm?.toString(), + !dest.height_cm + ) + ); + setWeight( + new ScrapeResult( + dest.weight?.toString(), + sources.find((s) => s.weight)?.weight?.toString(), + !dest.weight + ) + ); + + setPenisLength( + new ScrapeResult( + dest.penis_length?.toString(), + sources.find((s) => s.penis_length)?.penis_length?.toString(), + !dest.penis_length + ) + ); + setMeasurements( + new ScrapeResult( + dest.measurements, + sources.find((s) => s.measurements)?.measurements, + !dest.measurements + ) + ); + setFakeTits( + new ScrapeResult( + dest.fake_tits, + sources.find((s) => s.fake_tits)?.fake_tits, + !dest.fake_tits + ) + ); + setCareerLength( + new ScrapeResult( + dest.career_length, + sources.find((s) => s.career_length)?.career_length, + !dest.career_length + ) + ); + setTattoos( + new ScrapeResult( + dest.tattoos, + sources.find((s) => s.tattoos)?.tattoos, + !dest.tattoos + ) + ); + setPiercings( + new ScrapeResult( + dest.piercings, + sources.find((s) => s.piercings)?.piercings, + !dest.piercings + ) + ); + setURLs( + new ScrapeResult( + dest.urls, + sources.find((s) => s.urls)?.urls, + !dest.urls?.length + ) + ); + setGender( + new ScrapeResult( + genderToString(dest.gender), + sources.find((s) => s.gender)?.gender + ? genderToString(sources.find((s) => s.gender)?.gender) + : undefined, + !dest.gender + ) + ); + setCircumcised( + new ScrapeResult( + circumcisedToString(dest.circumcised), + sources.find((s) => s.circumcised)?.circumcised + ? circumcisedToString(sources.find((s) => s.circumcised)?.circumcised) + : undefined, + !dest.circumcised + ) + ); + setDetails( + new ScrapeResult( + dest.details, + sources.find((s) => s.details)?.details, + !dest.details + ) + ); + setImage( + new ScrapeResult( + dest.image_path, + sources.find((s) => s.image_path)?.image_path, + !dest.image_path + ) + ); + + const customFieldNames = new Set(Object.keys(dest.custom_fields)); + + for (const s of sources) { + for (const n of Object.keys(s.custom_fields)) { + customFieldNames.add(n); + } + } + + setCustomFields( + new Map( + Array.from(customFieldNames) + .sort() + .map((field) => { + return [ + field, + new ScrapeResult( + dest.custom_fields?.[field], + sources.find((s) => s.custom_fields?.[field])?.custom_fields?.[ + field + ], + dest.custom_fields?.[field] === undefined + ), + ]; + }) + ) + ); + + loadImages(); + }, [sources, dest]); + + const hasCustomFieldValues = useMemo(() => { + return hasScrapedValues(Array.from(customFields.values())); + }, [customFields]); + + // ensure this is updated if fields are changed + const hasValues = useMemo(() => { + return ( + hasCustomFieldValues || + hasScrapedValues([ + name, + disambiguation, + aliases, + birthdate, + deathDate, + ethnicity, + country, + hairColor, + eyeColor, + height, + weight, + penisLength, + measurements, + fakeTits, + careerLength, + tattoos, + piercings, + urls, + gender, + circumcised, + details, + tags, + image, + ]) + ); + }, [ + name, + disambiguation, + aliases, + birthdate, + deathDate, + ethnicity, + country, + hairColor, + eyeColor, + height, + weight, + penisLength, + measurements, + fakeTits, + careerLength, + tattoos, + piercings, + urls, + gender, + circumcised, + details, + tags, + image, + hasCustomFieldValues, + ]); + + function renderScrapeRows() { + if (loading) { + return ( +
      + +
      + ); + } + + if (!hasValues) { + return ( +
      + +
      + ); + } + + return ( + <> + setName(value)} + /> + setDisambiguation(value)} + /> + setAliases(value)} + /> + setBirthdate(value)} + /> + setDeathDate(value)} + /> + setEthnicity(value)} + /> + setCountry(value)} + /> + setHairColor(value)} + /> + setEyeColor(value)} + /> + setHeight(value)} + /> + setWeight(value)} + /> + setPenisLength(value)} + /> + setMeasurements(value)} + /> + setFakeTits(value)} + /> + setCareerLength(value)} + /> + setTattoos(value)} + /> + setPiercings(value)} + /> + setURLs(value)} + /> + {renderScrapedGenderRow( + intl.formatMessage({ id: "gender" }), + gender, + (value) => setGender(value) + )} + {renderScrapedCircumcisedRow( + intl.formatMessage({ id: "circumcised" }), + circumcised, + (value) => setCircumcised(value) + )} + setTags(value)} + /> + setDetails(value)} + /> + setImage(value)} + /> + {hasCustomFieldValues && + renderScrapedCustomFieldRows(customFields, (newCustomFields) => + setCustomFields(newCustomFields) + )} + + ); + } + + function createValues(): MergeOptions { + // only set the cover image if it's different from the existing cover image + const coverImage = image.useNewValue ? image.getNewValue() : undefined; + + return { + values: { + id: dest.id, + name: name.getNewValue(), + disambiguation: disambiguation.getNewValue(), + alias_list: aliases + .getNewValue() + ?.map((s) => s.trim()) + .filter((s) => s.length > 0), + birthdate: birthdate.getNewValue(), + death_date: deathDate.getNewValue(), + ethnicity: ethnicity.getNewValue(), + country: country.getNewValue(), + hair_color: hairColor.getNewValue(), + eye_color: eyeColor.getNewValue(), + height_cm: height.getNewValue() + ? parseFloat(height.getNewValue()!) + : undefined, + weight: weight.getNewValue() + ? parseFloat(weight.getNewValue()!) + : undefined, + penis_length: penisLength.getNewValue() + ? parseFloat(penisLength.getNewValue()!) + : undefined, + measurements: measurements.getNewValue(), + fake_tits: fakeTits.getNewValue(), + career_length: careerLength.getNewValue(), + tattoos: tattoos.getNewValue(), + piercings: piercings.getNewValue(), + urls: urls.getNewValue(), + gender: stringToGender(gender.getNewValue()), + circumcised: stringToCircumcised(circumcised.getNewValue()), + tag_ids: tags.getNewValue()?.map((t) => t.stored_id!), + details: details.getNewValue(), + image: coverImage, + custom_fields: { + partial: Object.fromEntries( + Array.from(customFields.entries()).flatMap(([field, v]) => + v.useNewValue ? [[field, v.getNewValue()]] : [] + ) + ), + }, + }, + }; + } + + const dialogTitle = intl.formatMessage({ + id: "actions.merge", + }); + + const destinationLabel = !hasValues + ? "" + : intl.formatMessage({ id: "dialogs.merge.destination" }); + const sourceLabel = !hasValues + ? "" + : intl.formatMessage({ id: "dialogs.merge.source" }); + + return ( + { + if (!apply) { + onClose(); + } else { + onClose(createValues()); + } + }} + > + {renderScrapeRows()} + + ); +}; + +interface IPerformerMergeModalProps { + show: boolean; + onClose: (mergedId?: string) => void; + performers: GQL.SelectPerformerDataFragment[]; +} + +export const PerformerMergeModal: React.FC = ({ + show, + onClose, + performers, +}) => { + const [sourcePerformers, setSourcePerformers] = useState< + GQL.SelectPerformerDataFragment[] + >([]); + const [destPerformer, setDestPerformer] = useState< + GQL.SelectPerformerDataFragment[] + >([]); + + const [loadedSources, setLoadedSources] = useState< + GQL.PerformerDataFragment[] + >([]); + const [loadedDest, setLoadedDest] = useState(); + + const [running, setRunning] = useState(false); + const [secondStep, setSecondStep] = useState(false); + + const intl = useIntl(); + const Toast = useToast(); + + const title = intl.formatMessage({ + id: "actions.merge", + }); + + useEffect(() => { + if (performers.length > 0) { + // set the first performer as the destination, others as source + setDestPerformer([performers[0]]); + + if (performers.length > 1) { + setSourcePerformers(performers.slice(1)); + } + } + }, [performers]); + + async function loadPerformers() { + const performerIDs = sourcePerformers.map((s) => parseInt(s.id)); + performerIDs.push(parseInt(destPerformer[0].id)); + const query = await queryFindPerformersByID(performerIDs); + const { performers: loadedPerformers } = query.data.findPerformers; + + setLoadedDest(loadedPerformers.find((s) => s.id === destPerformer[0].id)); + setLoadedSources( + loadedPerformers.filter((s) => s.id !== destPerformer[0].id) + ); + setSecondStep(true); + } + + async function onMerge(options: MergeOptions) { + const { values } = options; + try { + setRunning(true); + const result = await mutatePerformerMerge( + destPerformer[0].id, + sourcePerformers.map((s) => s.id), + values + ); + if (result.data?.performerMerge) { + Toast.success(intl.formatMessage({ id: "toast.merged_performers" })); + onClose(destPerformer[0].id); + } + onClose(); + } catch (e) { + Toast.error(e); + } finally { + setRunning(false); + } + } + + function canMerge() { + return sourcePerformers.length > 0 && destPerformer.length !== 0; + } + + function switchPerformers() { + if (sourcePerformers.length && destPerformer.length) { + const newDest = sourcePerformers[0]; + setSourcePerformers([...sourcePerformers.slice(1), destPerformer[0]]); + setDestPerformer([newDest]); + } + } + + if (secondStep && destPerformer.length > 0) { + return ( + { + setSecondStep(false); + if (values) { + onMerge(values); + } else { + onClose(); + } + }} + /> + ); + } + + return ( + loadPerformers(), + }} + disabled={!canMerge()} + cancel={{ + variant: "secondary", + onClick: () => onClose(), + }} + isRunning={running} + > +
      +
      + + {FormUtils.renderLabel({ + title: intl.formatMessage({ id: "dialogs.merge.source" }), + labelProps: { + column: true, + sm: 3, + xl: 12, + }, + })} + + setSourcePerformers(items)} + values={sourcePerformers} + menuPortalTarget={document.body} + /> + + + + + + + {FormUtils.renderLabel({ + title: intl.formatMessage({ + id: "dialogs.merge.destination", + }), + labelProps: { + column: true, + sm: 3, + xl: 12, + }, + })} + + setDestPerformer(items)} + values={destPerformer} + menuPortalTarget={document.body} + /> + + +
      +
      +
      + ); +}; diff --git a/ui/v2.5/src/components/Performers/PerformerPopover.tsx b/ui/v2.5/src/components/Performers/PerformerPopover.tsx index fa5b60e69..04cbf8e87 100644 --- a/ui/v2.5/src/components/Performers/PerformerPopover.tsx +++ b/ui/v2.5/src/components/Performers/PerformerPopover.tsx @@ -4,7 +4,7 @@ import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { HoverPopover } from "../Shared/HoverPopover"; import { useFindPerformer } from "../../core/StashService"; import { PerformerCard } from "./PerformerCard"; -import { ConfigurationContext } from "../../hooks/Config"; +import { useConfigurationContext } from "../../hooks/Config"; import { Placement } from "react-bootstrap/esm/Overlay"; interface IPeromerPopoverCardProps { @@ -49,7 +49,7 @@ export const PerformerPopover: React.FC = ({ placement = "top", target, }) => { - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const showPerformerCardOnHover = config?.ui.showTagCardOnHover ?? true; diff --git a/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx b/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx index 3c094f7ad..13bba1e99 100644 --- a/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx +++ b/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx @@ -7,6 +7,7 @@ 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 { PatchComponent } from "src/patch"; interface IProps { isTouch: boolean; @@ -14,41 +15,44 @@ interface IProps { header: string; } -export const PerformerRecommendationRow: React.FC = (props) => { - const result = useFindPerformers(props.filter); - const cardCount = result.data?.findPerformers.count; +export const PerformerRecommendationRow: React.FC = PatchComponent( + "PerformerRecommendationRow", + (props) => { + const result = useFindPerformers(props.filter); + const cardCount = result.data?.findPerformers.count; - if (!result.loading && !cardCount) { - return null; - } + if (!result.loading && !cardCount) { + return null; + } - return ( - - - - } - > - + + + } > - {result.loading - ? [...Array(props.filter.itemsPerPage)].map((i) => ( -
      - )) - : result.data?.findPerformers.performers.map((p) => ( - - ))} -
      -
      - ); -}; + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
      + )) + : result.data?.findPerformers.performers.map((p) => ( + + ))} +
      + + ); + } +); diff --git a/ui/v2.5/src/components/Performers/PerformerSelect.tsx b/ui/v2.5/src/components/Performers/PerformerSelect.tsx index d31dc3ec7..f10519897 100644 --- a/ui/v2.5/src/components/Performers/PerformerSelect.tsx +++ b/ui/v2.5/src/components/Performers/PerformerSelect.tsx @@ -13,7 +13,7 @@ import { queryFindPerformersByIDForSelect, queryFindPerformersForSelect, } from "src/core/StashService"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { useIntl } from "react-intl"; import { defaultMaxOptionsShown } from "src/core/config"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -82,12 +82,12 @@ const _PerformerSelect: React.FC< > = (props) => { const [createPerformer] = usePerformerCreate(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const intl = useIntl(); const maxOptionsShown = configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; const defaultCreatable = - !configuration?.interface.disableDropdownCreate.performer ?? true; + !configuration?.interface.disableDropdownCreate.performer; async function loadPerformers(input: string): Promise { const filter = new ListFilterModel(GQL.FilterMode.Performers); diff --git a/ui/v2.5/src/components/Performers/styles.scss b/ui/v2.5/src/components/Performers/styles.scss index f76816f05..17ca3a737 100644 --- a/ui/v2.5/src/components/Performers/styles.scss +++ b/ui/v2.5/src/components/Performers/styles.scss @@ -35,12 +35,33 @@ .rating-number .form-control { width: inherit; } + + // The following min-width declarations prevent + // the performer's O-Count from moving around + // when hovering over rating stars + .rating-stars-precision-full .star-rating-number { + min-width: 0.75rem; + } + + .rating-stars-precision-half .star-rating-number, + .rating-stars-precision-tenth .star-rating-number { + min-width: 1.45rem; + } + + .rating-stars-precision-quarter .star-rating-number { + min-width: 2rem; + } } .alias { font-weight: bold; } + .quality-group { + display: inline-flex; + margin-top: 0.25rem; + } + // the detail element ids are the same as field type name // which don't follow the correct convention /* stylelint-disable selector-class-pattern */ @@ -86,6 +107,10 @@ .thumbnail-section { position: relative; + + .instagram { + color: pink; + } } &-image { @@ -168,17 +193,21 @@ display: flex; } -.fa-mars { - color: #89cff0; -} +.gender-icon { + &[data-gender="FEMALE"], + &[data-gender="TRANSGENDER_FEMALE"] { + color: #f38cac; + } -.fa-venus { - color: #f38cac; -} + &[data-gender="MALE"], + &[data-gender="TRANSGENDER_MALE"] { + color: #89cff0; + } -.fa-transgender, -.fa-transgender-alt { - color: #c8a2c8; + &[data-gender="NON_BINARY"], + &[data-gender="INTERSEX"] { + color: #c8a2c8; + } } .performer-height .height-imperial, @@ -277,3 +306,11 @@ overflow-y: auto; padding-right: 1.5rem; } + +.performer-merge-dialog .custom-field { + // ensure we don't catch the destination/source labels + & > .form-label, + .form-control { + font-family: "Courier New", Courier, monospace; + } +} diff --git a/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx b/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx index 2d8114935..d396a01f4 100644 --- a/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx +++ b/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx @@ -79,7 +79,24 @@ export const SceneDuplicateChecker: React.FC = () => { }, }); - const scenes = data?.findDuplicateScenes ?? []; + const getGroupTotalSize = (group: GQL.SlimSceneDataFragment[]) => { + // Sum all file sizes across all scenes in the group + return group.reduce((groupTotal, scene) => { + const sceneTotal = scene.files.reduce( + (fileTotal, file) => fileTotal + file.size, + 0 + ); + return groupTotal + sceneTotal; + }, 0); + }; + + const scenes = useMemo(() => { + const groups = data?.findDuplicateScenes ?? []; + // Sort by total file size descending (largest groups first) + return [...groups].sort((a, b) => { + return getGroupTotalSize(b) - getGroupTotalSize(a); + }); + }, [data?.findDuplicateScenes]); const { data: missingPhash } = GQL.useFindScenesQuery({ variables: { diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 4440f80df..36df653ba 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -1,7 +1,6 @@ import React, { KeyboardEvent, useCallback, - useContext, useEffect, useMemo, useRef, @@ -17,21 +16,25 @@ import "./live"; import "./PlaylistButtons"; import "./source-selector"; import "./persist-volume"; +import "./autostart-button"; import MarkersPlugin, { type IMarker } from "./markers"; void MarkersPlugin; import "./vtt-thumbnails"; import "./big-buttons"; import "./track-activity"; import "./vrmode"; +import "./media-session"; +import "./wake-sentinel"; import cx from "classnames"; import { useSceneSaveActivity, useSceneIncrementPlayCount, + useConfigureInterface, } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; import { ScenePlayerScrubber } from "./ScenePlayerScrubber"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ConnectionState, InteractiveContext, @@ -240,7 +243,7 @@ export const ScenePlayer: React.FC = PatchComponent( onNext, onPrevious, }) => { - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const interfaceConfig = configuration?.interface; const uiConfig = configuration?.ui; const videoRef = useRef(null); @@ -248,6 +251,7 @@ export const ScenePlayer: React.FC = PatchComponent( const sceneId = useRef(); const [sceneSaveActivity] = useSceneSaveActivity(); const [sceneIncrementPlayCount] = useSceneIncrementPlayCount(); + const [updateInterfaceConfig] = useConfigureInterface(); const [time, setTime] = useState(0); const [ready, setReady] = useState(false); @@ -360,7 +364,7 @@ export const ScenePlayer: React.FC = PatchComponent( }, nativeControlsForTouch: false, playbackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], - inactivityTimeout: 2000, + inactivityTimeout: 700, preload: "none", playsinline: true, techOrder: ["chromecast", "html5"], @@ -370,7 +374,9 @@ export const ScenePlayer: React.FC = PatchComponent( }, }, plugins: { - airPlay: {}, + airPlay: { + addButtonToControlBar: uiConfig?.enableChromecast ?? false, + }, chromecast: {}, vttThumbnails: { showTimestamp: true, @@ -386,6 +392,9 @@ export const ScenePlayer: React.FC = PatchComponent( skipButtons: {}, trackActivity: {}, vrMenu: {}, + autostartButton: { + enabled: interfaceConfig?.autostartVideo ?? false, + }, abLoopPlugin: { start: 0, end: false, @@ -396,6 +405,8 @@ export const ScenePlayer: React.FC = PatchComponent( pauseBeforeLooping: false, createButtons: uiConfig?.showAbLoopControls ?? false, }, + mediaSession: {}, + wakeSentinel: {}, }, }; @@ -429,7 +440,10 @@ export const ScenePlayer: React.FC = PatchComponent( }; // empty deps - only init once // showAbLoopControls is necessary to re-init the player when the config changes - }, [uiConfig?.showAbLoopControls]); + // Note: interfaceConfig?.autostartVideo is intentionally excluded to prevent + // player re-initialization when toggling autostart (which would interrupt playback) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [uiConfig?.showAbLoopControls, uiConfig?.enableChromecast]); useEffect(() => { const player = getPlayer(); @@ -670,11 +684,6 @@ export const ScenePlayer: React.FC = PatchComponent( } } - auto.current = - autoplay || - (interfaceConfig?.autostartVideo ?? false) || - _initialTimestamp > 0; - const alwaysStartFromBeginning = uiConfig?.alwaysStartFromBeginning ?? false; const resumeTime = scene.resume_time ?? 0; @@ -693,6 +702,15 @@ export const ScenePlayer: React.FC = PatchComponent( player.load(); player.focus(); + // Check the autostart button plugin for user preference + const autostartButton = player.autostartButton(); + const buttonEnabled = autostartButton.getEnabled(); + auto.current = + autoplay || + buttonEnabled || + (interfaceConfig?.autostartVideo ?? false) || + _initialTimestamp > 0; + player.ready(() => { player.vttThumbnails().src(scene.paths.vtt ?? null); @@ -836,6 +854,30 @@ export const ScenePlayer: React.FC = PatchComponent( sceneSaveActivity, ]); + // Sync autostart button with config changes + useEffect(() => { + const player = getPlayer(); + if (!player) return; + + async function updateAutoStart(enabled: boolean) { + await updateInterfaceConfig({ + variables: { + input: { + autostartVideo: enabled, + }, + }, + }); + } + + const autostartButton = player.autostartButton(); + if (autostartButton) { + autostartButton.syncWithConfig( + interfaceConfig?.autostartVideo ?? false + ); + autostartButton.updateAutoStart = updateAutoStart; + } + }, [getPlayer, updateInterfaceConfig, interfaceConfig?.autostartVideo]); + useEffect(() => { const player = getPlayer(); if (!player) return; @@ -873,15 +915,40 @@ export const ScenePlayer: React.FC = PatchComponent( return () => player.off("ended"); }, [getPlayer, onComplete]); + // set up mediaSession plugin + useEffect(() => { + const player = getPlayer(); + if (!player) return; + + // set up mediasession plugin + // get performer names as array + const performers = scene?.performers.map((p) => p.name).join(", "); + player + .mediaSession() + .setMetadata( + scene?.title ?? "Stash", + scene?.studio?.name ?? performers ?? "Stash", + scene.paths.screenshot || "" + ); + }, [getPlayer, scene]); + + const pausedBeforeScrubber = useRef(true); + function onScrubberScroll() { - if (started.current) { - getPlayer()?.pause(); + const player = getPlayer(); + if (started.current && player) { + pausedBeforeScrubber.current = player.paused(); + player.pause(); } } function onScrubberSeek(seconds: number) { - if (started.current) { - getPlayer()?.currentTime(seconds); + const player = getPlayer(); + if (started.current && player) { + player.currentTime(seconds); + if (!pausedBeforeScrubber.current) { + player.play(); + } } else { setTime(seconds); } diff --git a/ui/v2.5/src/components/ScenePlayer/autostart-button.ts b/ui/v2.5/src/components/ScenePlayer/autostart-button.ts new file mode 100644 index 000000000..f5a35a63f --- /dev/null +++ b/ui/v2.5/src/components/ScenePlayer/autostart-button.ts @@ -0,0 +1,126 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import videojs, { VideoJsPlayer } from "video.js"; + +interface IAutostartButtonOptions { + enabled?: boolean; +} + +interface AutostartButtonOptions extends videojs.ComponentOptions { + autostartEnabled: boolean; +} + +class AutostartButton extends videojs.getComponent("Button") { + private autostartEnabled: boolean; + + constructor(player: VideoJsPlayer, options: AutostartButtonOptions) { + super(player, options); + this.autostartEnabled = options.autostartEnabled; + this.updateIcon(); + } + + buildCSSClass() { + return `vjs-autostart-button ${super.buildCSSClass()}`; + } + + private updateIcon() { + this.removeClass("vjs-icon-play-circle"); + this.removeClass("vjs-icon-cancel"); + + if (this.autostartEnabled) { + this.addClass("vjs-icon-play-circle"); + this.controlText(this.localize("Auto-start enabled (click to disable)")); + } else { + this.addClass("vjs-icon-cancel"); + this.controlText(this.localize("Auto-start disabled (click to enable)")); + } + } + + handleClick(event: Event) { + // Prevent the click from bubbling up and affecting the video player + event.stopPropagation(); + + this.autostartEnabled = !this.autostartEnabled; + this.updateIcon(); + this.trigger("autostartchanged", { enabled: this.autostartEnabled }); + } + + public setEnabled(enabled: boolean) { + this.autostartEnabled = enabled; + this.updateIcon(); + } +} + +class AutostartButtonPlugin extends videojs.getPlugin("plugin") { + private button: AutostartButton; + private autostartEnabled: boolean; + updateAutoStart: (enabled: boolean) => Promise = () => { + return Promise.resolve(); + }; + + constructor(player: VideoJsPlayer, options?: IAutostartButtonOptions) { + super(player, options); + + this.autostartEnabled = options?.enabled ?? false; + + this.button = new AutostartButton(player, { + autostartEnabled: this.autostartEnabled, + }); + + player.ready(() => { + this.ready(); + }); + } + + private ready() { + // Add button to control bar, before the fullscreen button + const { controlBar } = this.player; + const fullscreenToggle = controlBar.getChild("fullscreenToggle"); + if (fullscreenToggle) { + controlBar.addChild(this.button); + controlBar.el().insertBefore(this.button.el(), fullscreenToggle.el()); + } else { + controlBar.addChild(this.button); + } + + // Listen for changes + this.button.on("autostartchanged", (_, data: { enabled: boolean }) => { + this.autostartEnabled = data.enabled; + this.updateAutoStart(this.autostartEnabled); + }); + } + + public isEnabled(): boolean { + return this.autostartEnabled; + } + + public getEnabled(): boolean { + return this.autostartEnabled; + } + + public setEnabled(enabled: boolean) { + this.autostartEnabled = enabled; + this.button.setEnabled(enabled); + } + + public syncWithConfig(configEnabled: boolean) { + // Sync button state with external config changes + if (this.autostartEnabled !== configEnabled) { + this.setEnabled(configEnabled); + } + } +} + +// Register the plugin with video.js. +videojs.registerComponent("AutostartButton", AutostartButton); +videojs.registerPlugin("autostartButton", AutostartButtonPlugin); + +declare module "video.js" { + interface VideoJsPlayer { + autostartButton: () => AutostartButtonPlugin; + } + interface VideoJsPlayerPluginOptions { + autostartButton?: IAutostartButtonOptions; + } +} + +export default AutostartButtonPlugin; diff --git a/ui/v2.5/src/components/ScenePlayer/markers.ts b/ui/v2.5/src/components/ScenePlayer/markers.ts index 83a695c1b..55e0c7cd2 100644 --- a/ui/v2.5/src/components/ScenePlayer/markers.ts +++ b/ui/v2.5/src/components/ScenePlayer/markers.ts @@ -5,6 +5,7 @@ export interface IMarker { title: string; seconds: number; end_seconds?: number | null; + primaryTag: { name: string }; } interface IMarkersOptions { @@ -85,8 +86,13 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { markerSet.dot.toggleAttribute("marker-tooltip-shown", true); // Set background color based on tag (if available) - if (marker.title && this.tagColors[marker.title]) { - markerSet.dot.style.backgroundColor = this.tagColors[marker.title]; + if ( + marker.primaryTag && + marker.primaryTag.name && + this.tagColors[marker.primaryTag.name] + ) { + markerSet.dot.style.backgroundColor = + this.tagColors[marker.primaryTag.name]; } markerSet.dot.addEventListener("mouseenter", () => { this.showMarkerTooltip(marker.title); @@ -136,24 +142,33 @@ class MarkersPlugin extends videojs.getPlugin("plugin") { const rangeDiv = videojs.dom.createEl("div") as HTMLDivElement; rangeDiv.className = "vjs-marker-range"; - // start/end percent is relative to the parent element, which is the vjs-progress-control - // vjs-progress-control has 15px margins on each side - const left = seekBar.clientWidth * (marker.seconds / duration) + 15; + // Use percentage-based positioning for proper scaling in fullscreen mode + // The range marker is inside vjs-progress-control, but needs to align with + // vjs-progress-holder which has 15px margins on each side. + // We use calc() to combine percentage positioning with the fixed margin offset. + const startPercent = (marker.seconds / duration) * 100; + const widthPercent = + ((marker.end_seconds - marker.seconds) / duration) * 100; - // minimum width of 8px - const width = Math.max( - seekBar.clientWidth * ((marker.end_seconds - marker.seconds) / duration), - 8 - ); + // left: 15px margin + percentage of the progress holder width + // Since progress-holder has margin: 0 15px, we need calc(15px + X% of remaining width) + // The progress-holder width is (100% - 30px), so the actual left position is: + // 15px + startPercent% * (100% - 30px) = 15px + startPercent% * 100% - startPercent% * 30px + rangeDiv.style.left = `calc(15px + ${startPercent}% - ${ + startPercent * 0.3 + }px)`; - rangeDiv.style.left = `${left}px`; - rangeDiv.style.width = `${width}px`; + rangeDiv.style.width = `calc(${widthPercent}% - ${widthPercent * 0.3}px)`; rangeDiv.style.bottom = `${layer * this.layerHeight}px`; // Adjust height based on layer rangeDiv.style.display = "none"; // Initially hidden // Set background color based on tag (if available) - if (marker.title && this.tagColors[marker.title]) { - rangeDiv.style.backgroundColor = this.tagColors[marker.title]; + if ( + marker.primaryTag && + marker.primaryTag.name && + this.tagColors[marker.primaryTag.name] + ) { + rangeDiv.style.backgroundColor = this.tagColors[marker.primaryTag.name]; } markerSet.range = rangeDiv; diff --git a/ui/v2.5/src/components/ScenePlayer/media-session.ts b/ui/v2.5/src/components/ScenePlayer/media-session.ts new file mode 100644 index 000000000..b3ce2d0ea --- /dev/null +++ b/ui/v2.5/src/components/ScenePlayer/media-session.ts @@ -0,0 +1,71 @@ +import videojs, { VideoJsPlayer } from "video.js"; + +class MediaSessionPlugin extends videojs.getPlugin("plugin") { + constructor(player: VideoJsPlayer) { + super(player); + + player.ready(() => { + player.addClass("vjs-media-session"); + this.setActionHandlers(); + }); + + player.on("play", () => { + this.updatePlaybackState(); + }); + + player.on("pause", () => { + this.updatePlaybackState(); + }); + this.updatePlaybackState(); + } + + // manually set poster since it's only set on useEffect + public setMetadata(title: string, artist: string, poster: string): void { + if ("mediaSession" in navigator) { + navigator.mediaSession.metadata = new MediaMetadata({ + title, + artist, + artwork: [ + { + src: poster || this.player.poster() || "", + type: "image/jpeg", + }, + ], + }); + } + } + + private updatePlaybackState(): void { + if ("mediaSession" in navigator) { + const playbackState = this.player.paused() ? "paused" : "playing"; + navigator.mediaSession.playbackState = playbackState; + } + } + + private setActionHandlers(): void { + // method initialization + navigator.mediaSession.setActionHandler("play", () => { + this.player.play(); + }); + navigator.mediaSession.setActionHandler("pause", () => { + this.player.pause(); + }); + navigator.mediaSession.setActionHandler("nexttrack", () => { + this.player.skipButtons()?.handleForward(); + }); + navigator.mediaSession.setActionHandler("previoustrack", () => { + this.player.skipButtons()?.handleBackward(); + }); + } +} + +videojs.registerPlugin("mediaSession", MediaSessionPlugin); + +/* eslint-disable @typescript-eslint/naming-convention */ +declare module "video.js" { + interface VideoJsPlayer { + mediaSession: () => MediaSessionPlugin; + } +} + +export default MediaSessionPlugin; diff --git a/ui/v2.5/src/components/ScenePlayer/styles.scss b/ui/v2.5/src/components/ScenePlayer/styles.scss index 95b1df8c7..fc143a873 100644 --- a/ui/v2.5/src/components/ScenePlayer/styles.scss +++ b/ui/v2.5/src/components/ScenePlayer/styles.scss @@ -100,6 +100,57 @@ $sceneTabWidth: 450px; width: 1.6em; } + .vjs-autostart-button { + cursor: pointer; + + &.vjs-icon-play-circle::before { + align-items: center; + background-color: rgba(255, 255, 255, 0.9); + border-radius: 50%; + color: rgba(80, 80, 80, 0.9); + content: "\f101"; + font-size: 1em; + line-height: 1; + margin-left: 1rem; + padding: 0.3em; + position: relative; + z-index: 2; + } + + &.vjs-icon-cancel::before { + align-items: center; + background-color: rgba(80, 80, 80, 0.9); + border-radius: 50%; + color: #fff; + content: "\f103"; + font-size: 1em; + line-height: 1; + margin-right: 1rem; + padding: 0.3em; + position: relative; + z-index: 2; + } + + &.vjs-icon-play-circle::after, + &.vjs-icon-cancel::after { + background-color: rgb(255 255 255 / 70%); + border-radius: 8px; + content: ""; + height: 2.5rem; + left: 50%; + opacity: 0.7; + position: absolute; + top: 50%; + transform: translate(-50%, -50%) rotate(90deg); + width: 1rem; + z-index: 1; + } + + &:hover { + text-shadow: 0 0 1em rgba(255, 255, 255, 0.75); + } + } + .vjs-touch-overlay .vjs-play-control { z-index: 1; } @@ -271,6 +322,7 @@ $sceneTabWidth: 450px; border-radius: 2px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); height: 8px; + min-width: 8px; position: absolute; transform: translateY(-28px); transition: none; @@ -343,9 +395,16 @@ $sceneTabWidth: 450px; } } } + @media (max-width: 576px) { + .vjs-control-bar { + .vjs-autostart-button { + display: none; + } + } + } // make controls a little more compact on smaller screens - @media (max-width: 576px) { + @media (max-width: 768px) { .vjs-control-bar { .vjs-control { width: 2.5em; diff --git a/ui/v2.5/src/components/ScenePlayer/track-activity.ts b/ui/v2.5/src/components/ScenePlayer/track-activity.ts index a3846dc87..4e7c1ce76 100644 --- a/ui/v2.5/src/components/ScenePlayer/track-activity.ts +++ b/ui/v2.5/src/components/ScenePlayer/track-activity.ts @@ -44,6 +44,10 @@ class TrackActivityPlugin extends videojs.getPlugin("plugin") { player.on("dispose", () => { this.stop(); }); + + player.on("ended", () => { + this.stop(); + }); } private start() { diff --git a/ui/v2.5/src/components/ScenePlayer/util.ts b/ui/v2.5/src/components/ScenePlayer/util.ts index a63ab6a2e..21ed99b62 100644 --- a/ui/v2.5/src/components/ScenePlayer/util.ts +++ b/ui/v2.5/src/components/ScenePlayer/util.ts @@ -1,6 +1,27 @@ -import videojs from "video.js"; +import videojs, { VideoJsPlayer } from "video.js"; export const VIDEO_PLAYER_ID = "VideoJsPlayer"; -export const getPlayerPosition = () => - videojs.getPlayer(VIDEO_PLAYER_ID)?.currentTime(); +export const getPlayer = () => videojs.getPlayer(VIDEO_PLAYER_ID); + +export const getPlayerPosition = () => getPlayer()?.currentTime(); + +export type AbLoopOptions = { + start: number; + end: number | false; + enabled?: boolean; +}; + +export type AbLoopPluginApi = { + getOptions: () => AbLoopOptions; + setOptions: (options: AbLoopOptions) => void; +}; + +export const getAbLoopPlugin = () => { + const player = getPlayer(); + if (!player) return null; + const { abLoopPlugin } = player as VideoJsPlayer & { + abLoopPlugin?: AbLoopPluginApi; + }; + return abLoopPlugin ?? null; +}; diff --git a/ui/v2.5/src/components/ScenePlayer/wake-sentinel.ts b/ui/v2.5/src/components/ScenePlayer/wake-sentinel.ts new file mode 100644 index 000000000..a51c050f5 --- /dev/null +++ b/ui/v2.5/src/components/ScenePlayer/wake-sentinel.ts @@ -0,0 +1,65 @@ +import videojs, { VideoJsPlayer } from "video.js"; + +class WakeSentinelPlugin extends videojs.getPlugin("plugin") { + public wakeLock: WakeLockSentinel | null = null; + public wakeLockFail: boolean = false; + constructor(player: VideoJsPlayer) { + super(player); + + // listen for visibility change events + document.addEventListener("visibilitychange", async () => { + if (document.visibilityState === "visible") { + // reacquire the wake lock when the page becomes visible + await this.acquireWakeLock(); + } + }); + + // acquire wake lock on ready and play + player.ready(async () => { + player.addClass("vjs-wake-sentinel"); + await this.acquireWakeLock(true); + }); + player.on("play", () => this.acquireWakeLock()); + + // release wake lock on pause, dispose and end + player.on("pause", () => this.releaseWakeLock()); + player.on("dispose", () => this.releaseWakeLock()); + player.on("ended", () => this.releaseWakeLock()); + } + + private async releaseWakeLock(): Promise { + this.wakeLock?.release().then(() => (this.wakeLock = null)); + } + + private async acquireWakeLock(log = false): Promise { + // if wake lock failed, don't even try + if (this.wakeLockFail) return; + // check for wake lock on startup + if ("wakeLock" in navigator) { + try { + this.wakeLock = await navigator.wakeLock.request("screen"); + } catch (err) { + if (log) console.error("Failed to obtain Screen Wake Lock:", err); + this.wakeLockFail = true; + } + } else { + if (log) { + console.warn( + "Screen Wake Lock API not supported. Secure context (https or localhost) and modern browser required." + ); + } + this.wakeLockFail = true; + } + } +} + +videojs.registerPlugin("wakeSentinel", WakeSentinelPlugin); + +/* eslint-disable @typescript-eslint/naming-convention */ +declare module "video.js" { + interface VideoJsPlayer { + wakeSentinel: () => WakeSentinelPlugin; + } +} + +export default WakeSentinelPlugin; diff --git a/ui/v2.5/src/components/Scenes/DeleteScenesDialog.tsx b/ui/v2.5/src/components/Scenes/DeleteScenesDialog.tsx index 88f133a80..56cbd69b0 100644 --- a/ui/v2.5/src/components/Scenes/DeleteScenesDialog.tsx +++ b/ui/v2.5/src/components/Scenes/DeleteScenesDialog.tsx @@ -4,7 +4,7 @@ import { useScenesDestroy } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; import { ModalComponent } from "src/components/Shared/Modal"; import { useToast } from "src/hooks/Toast"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { FormattedMessage, useIntl } from "react-intl"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import { objectPath } from "src/core/files"; @@ -34,7 +34,7 @@ export const DeleteScenesDialog: React.FC = ( { count: props.selected.length, singularEntity, pluralEntity } ); - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const [deleteFile, setDeleteFile] = useState( config?.defaults.deleteFile ?? false @@ -94,6 +94,11 @@ export const DeleteScenesDialog: React.FC = ( } }); + const deleteTrashPath = config?.general.deleteTrashPath; + const deleteAlertId = deleteTrashPath + ? "dialogs.delete_alert_to_trash" + : "dialogs.delete_alert"; + return (

      @@ -103,7 +108,7 @@ export const DeleteScenesDialog: React.FC = ( singularEntity: intl.formatMessage({ id: "file" }), pluralEntity: intl.formatMessage({ id: "files" }), }} - id="dialogs.delete_alert" + id={deleteAlertId} />

        diff --git a/ui/v2.5/src/components/Scenes/EditSceneMarkersDialog.tsx b/ui/v2.5/src/components/Scenes/EditSceneMarkersDialog.tsx new file mode 100644 index 000000000..bb1d8067b --- /dev/null +++ b/ui/v2.5/src/components/Scenes/EditSceneMarkersDialog.tsx @@ -0,0 +1,200 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { Form } from "react-bootstrap"; +import { FormattedMessage, useIntl } from "react-intl"; +import { useBulkSceneMarkerUpdate } from "src/core/StashService"; +import * as GQL from "src/core/generated-graphql"; +import { ModalComponent } from "../Shared/Modal"; +import { useToast } from "src/hooks/Toast"; +import { MultiSet } from "../Shared/MultiSet"; +import { + getAggregateState, + getAggregateStateObject, +} from "src/utils/bulkUpdate"; +import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput"; +import { faPencilAlt } from "@fortawesome/free-solid-svg-icons"; +import { TagSelect } from "../Shared/Select"; + +interface IListOperationProps { + selected: GQL.SceneMarkerDataFragment[]; + onClose: (applied: boolean) => void; +} + +const scenemarkerFields = ["title"]; + +export const EditSceneMarkersDialog: React.FC = ( + props: IListOperationProps +) => { + const intl = useIntl(); + const Toast = useToast(); + + const [updateInput, setUpdateInput] = + useState({ + ids: props.selected.map((scenemarker) => { + return scenemarker.id; + }), + }); + + const [tagIds, setTagIds] = useState({ + mode: GQL.BulkUpdateIdMode.Add, + }); + + const [updateSceneMarkers] = useBulkSceneMarkerUpdate(); + + // Network state + const [isUpdating, setIsUpdating] = useState(false); + + const aggregateState = useMemo(() => { + const updateState: Partial = {}; + const state = props.selected; + let updateTagIds: string[] = []; + let first = true; + + state.forEach((scenemarker: GQL.SceneMarkerDataFragment) => { + getAggregateStateObject( + updateState, + scenemarker, + scenemarkerFields, + first + ); + + // sceneMarker data fragment doesn't have primary_tag_id, so handle separately + updateState.primary_tag_id = getAggregateState( + updateState.primary_tag_id, + scenemarker.primary_tag.id, + first + ); + + const thisTagIDs = (scenemarker.tags ?? []).map((p) => p.id).sort(); + + updateTagIds = getAggregateState(updateTagIds, thisTagIDs, first) ?? []; + + first = false; + }); + + return { state: updateState, tagIds: updateTagIds }; + }, [props.selected]); + + // update initial state from aggregate + useEffect(() => { + setUpdateInput((current) => ({ ...current, ...aggregateState.state })); + }, [aggregateState]); + + function setUpdateField(input: Partial) { + setUpdateInput((current) => ({ ...current, ...input })); + } + + function getSceneMarkerInput(): GQL.BulkSceneMarkerUpdateInput { + const sceneMarkerInput: GQL.BulkSceneMarkerUpdateInput = { + ...updateInput, + tag_ids: tagIds, + }; + + return sceneMarkerInput; + } + + async function onSave() { + setIsUpdating(true); + try { + await updateSceneMarkers({ + variables: { + input: getSceneMarkerInput(), + }, + }); + Toast.success( + intl.formatMessage( + { id: "toast.updated_entity" }, + { + entity: intl.formatMessage({ id: "markers" }).toLocaleLowerCase(), + } + ) + ); + props.onClose(true); + } catch (e) { + Toast.error(e); + } + setIsUpdating(false); + } + + function renderTextField( + name: string, + value: string | undefined | null, + setter: (newValue: string | undefined) => void, + area: boolean = false + ) { + return ( + + + + + setter(newValue)} + unsetDisabled={props.selected.length < 2} + as={area ? "textarea" : undefined} + /> + + ); + } + + function render() { + return ( + props.onClose(false), + text: intl.formatMessage({ id: "actions.cancel" }), + variant: "secondary", + }} + isRunning={isUpdating} + > +
        + {renderTextField("title", updateInput.title, (newValue) => + setUpdateField({ title: newValue }) + )} + + + + + + setUpdateField({ primary_tag_id: t[0]?.id })} + ids={ + updateInput.primary_tag_id ? [updateInput.primary_tag_id] : [] + } + /> + + + + + + + setTagIds((v) => ({ ...v, ids: itemIDs }))} + onSetMode={(newMode) => + setTagIds((v) => ({ ...v, mode: newMode })) + } + existingIds={aggregateState.tagIds ?? []} + ids={tagIds.ids ?? []} + mode={tagIds.mode} + menuPortalTarget={document.body} + /> + +
        +
        + ); + } + + return render(); +}; diff --git a/ui/v2.5/src/components/Scenes/SceneCard.tsx b/ui/v2.5/src/components/Scenes/SceneCard.tsx index 99b910f67..2cb4a9af3 100644 --- a/ui/v2.5/src/components/Scenes/SceneCard.tsx +++ b/ui/v2.5/src/components/Scenes/SceneCard.tsx @@ -6,12 +6,11 @@ import * as GQL from "src/core/generated-graphql"; import { Icon } from "../Shared/Icon"; import { GalleryLink, TagLink, SceneMarkerLink } from "../Shared/TagLink"; import { HoverPopover } from "../Shared/HoverPopover"; -import { SweatDrops } from "../Shared/SweatDrops"; import { TruncatedText } from "../Shared/TruncatedText"; import NavUtils from "src/utils/navigation"; import TextUtils from "src/utils/text"; import { SceneQueue } from "src/models/sceneQueue"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; import { GridCard } from "../Shared/GridCard/GridCard"; import { RatingBanner } from "../Shared/RatingBanner"; @@ -30,6 +29,7 @@ import { PatchComponent } from "src/patch"; import { StudioOverlay } from "../Shared/GridCard/StudioOverlay"; import { GroupTag } from "../Groups/GroupTag"; import { FileSize } from "../Shared/FileSize"; +import { OCounterButton } from "../Shared/CountButton"; interface IScenePreviewProps { isPortrait: boolean; @@ -218,16 +218,7 @@ const SceneCardPopovers = PatchComponent( function maybeRenderOCounter() { if (props.scene.o_counter) { - return ( -
        - -
        - ); + return ; } } @@ -353,7 +344,7 @@ const SceneCardImage = PatchComponent( "SceneCard.Image", (props: ISceneCardProps) => { const history = useHistory(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const cont = configuration?.interface.continuePlaylistDefault ?? false; const file = useMemo( @@ -437,7 +428,7 @@ const SceneCardImage = PatchComponent( export const SceneCard = PatchComponent( "SceneCard", (props: ISceneCardProps) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const file = useMemo( () => (props.scene.files.length > 0 ? props.scene.files[0] : undefined), diff --git a/ui/v2.5/src/components/Scenes/SceneCardGrid.tsx b/ui/v2.5/src/components/Scenes/SceneCardGrid.tsx new file mode 100644 index 000000000..f60b412d3 --- /dev/null +++ b/ui/v2.5/src/components/Scenes/SceneCardGrid.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import * as GQL from "src/core/generated-graphql"; +import { SceneQueue } from "src/models/sceneQueue"; +import { SceneCard } from "./SceneCard"; +import { + useCardWidth, + useContainerDimensions, +} from "../Shared/GridCard/GridCard"; +import { PatchComponent } from "src/patch"; + +interface ISceneCardGrid { + scenes: GQL.SlimSceneDataFragment[]; + queue?: SceneQueue; + selectedIds: Set; + zoomIndex: number; + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; + fromGroupId?: string; +} + +const zoomWidths = [280, 340, 480, 640]; + +export const SceneCardGrid: React.FC = PatchComponent( + "SceneCardGrid", + ({ scenes, queue, selectedIds, zoomIndex, onSelectChange, fromGroupId }) => { + const [componentRef, { width: containerWidth }] = useContainerDimensions(); + + const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); + + return ( +
        + {scenes.map((scene, index) => ( + 0} + selected={selectedIds.has(scene.id)} + onSelectedChanged={(selected: boolean, shiftKey: boolean) => + onSelectChange(scene.id, selected, shiftKey) + } + fromGroupId={fromGroupId} + /> + ))} +
        + ); + } +); diff --git a/ui/v2.5/src/components/Scenes/SceneCardsGrid.tsx b/ui/v2.5/src/components/Scenes/SceneCardsGrid.tsx deleted file mode 100644 index 03b907938..000000000 --- a/ui/v2.5/src/components/Scenes/SceneCardsGrid.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from "react"; -import * as GQL from "src/core/generated-graphql"; -import { SceneQueue } from "src/models/sceneQueue"; -import { SceneCard } from "./SceneCard"; -import { - useCardWidth, - useContainerDimensions, -} from "../Shared/GridCard/GridCard"; - -interface ISceneCardsGrid { - scenes: GQL.SlimSceneDataFragment[]; - queue?: SceneQueue; - selectedIds: Set; - zoomIndex: number; - onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; - fromGroupId?: string; -} - -const zoomWidths = [280, 340, 480, 640]; - -export const SceneCardsGrid: React.FC = ({ - scenes, - queue, - selectedIds, - zoomIndex, - onSelectChange, - fromGroupId, -}) => { - const [componentRef, { width: containerWidth }] = useContainerDimensions(); - - const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); - - return ( -
        - {scenes.map((scene, index) => ( - 0} - selected={selectedIds.has(scene.id)} - onSelectedChanged={(selected: boolean, shiftKey: boolean) => - onSelectChange(scene.id, selected, shiftKey) - } - fromGroupId={fromGroupId} - /> - ))} -
        - ); -}; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx index b17dfb6bb..3701f4138 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx @@ -29,7 +29,7 @@ export const ExternalPlayerButton: React.FC = ({ const streamURL = new URL(stream); if (isAndroid) { const scheme = streamURL.protocol.slice(0, -1); - streamURL.hash = `Intent;action=android.intent.action.VIEW;scheme=${scheme};type=video/mp4;S.title=${encodeURI( + streamURL.hash = `Intent;action=android.intent.action.VIEW;scheme=${scheme};type=video/mp4;S.title=${encodeURIComponent( title )};end`; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx index 8fdb7dfd7..d8963df4d 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx @@ -1,10 +1,11 @@ -import { faBan, faMinus } from "@fortawesome/free-solid-svg-icons"; +import { faBan, faMinus, faThumbsUp } from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, ButtonGroup, Dropdown, DropdownButton } from "react-bootstrap"; import { useIntl } from "react-intl"; import { Icon } from "src/components/Shared/Icon"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { SweatDrops } from "src/components/Shared/SweatDrops"; +import { useConfigurationContext } from "src/hooks/Config"; export interface IOCounterButtonProps { value: number; @@ -17,6 +18,12 @@ export const OCounterButton: React.FC = ( props: IOCounterButtonProps ) => { const intl = useIntl(); + const { configuration } = useConfigurationContext(); + const { sfwContentMode } = configuration.interface; + + const icon = !sfwContentMode ? : ; + const messageID = !sfwContentMode ? "o_count" : "o_count_sfw"; + const [loading, setLoading] = useState(false); async function increment() { @@ -44,9 +51,9 @@ export const OCounterButton: React.FC = ( className="minimal pr-1" onClick={increment} variant="secondary" - title={intl.formatMessage({ id: "o_counter" })} + title={intl.formatMessage({ id: messageID })} > - + {icon} {props.value} ); diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx index 11c805ec6..d5a32fc31 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx @@ -4,18 +4,24 @@ import * as GQL from "src/core/generated-graphql"; import { Button, Badge, Card } from "react-bootstrap"; import TextUtils from "src/utils/text"; import { markerTitle } from "src/core/markers"; +import { useConfigurationContext } from "src/hooks/Config"; interface IPrimaryTags { sceneMarkers: GQL.SceneMarkerDataFragment[]; onClickMarker: (marker: GQL.SceneMarkerDataFragment) => void; + onLoopMarker: (marker: GQL.SceneMarkerDataFragment) => void; onEdit: (marker: GQL.SceneMarkerDataFragment) => void; } export const PrimaryTags: React.FC = ({ sceneMarkers, onClickMarker, + onLoopMarker, onEdit, }) => { + const { configuration } = useConfigurationContext(); + const showAbLoopControls = configuration?.ui?.showAbLoopControls; + if (!sceneMarkers?.length) return
        ; const primaryTagNames: Record = {}; @@ -52,10 +58,21 @@ export const PrimaryTags: React.FC = ({
        -
        - {TextUtils.formatTimestampRange( - marker.seconds, - marker.end_seconds ?? undefined +
        +
        + {TextUtils.formatTimestampRange( + marker.seconds, + marker.end_seconds ?? undefined + )} +
        + {showAbLoopControls && marker.end_seconds != null && ( + )}
        {tags}
        diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index c4088654a..435b9dce2 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -3,12 +3,11 @@ import React, { useEffect, useState, useMemo, - useContext, useRef, useLayoutEffect, } from "react"; -import { FormattedDate, FormattedMessage, useIntl } from "react-intl"; -import { Link, RouteComponentProps } from "react-router-dom"; +import { FormattedMessage, useIntl } from "react-intl"; +import { useHistory, Link, RouteComponentProps } from "react-router-dom"; import { Helmet } from "react-helmet"; import * as GQL from "src/core/generated-graphql"; import { @@ -32,8 +31,11 @@ import SceneQueue, { QueuedScene } from "src/models/sceneQueue"; import { ListFilterModel } from "src/models/list-filter/filter"; import Mousetrap from "mousetrap"; import { OrganizedButton } from "./OrganizedButton"; -import { ConfigurationContext } from "src/hooks/Config"; -import { getPlayerPosition } from "src/components/ScenePlayer/util"; +import { useConfigurationContext } from "src/hooks/Config"; +import { + getAbLoopPlugin, + getPlayerPosition, +} from "src/components/ScenePlayer/util"; import { faEllipsisV, faChevronRight, @@ -51,6 +53,9 @@ import { lazyComponent } from "src/utils/lazyComponent"; import cx from "classnames"; import { TruncatedText } from "src/components/Shared/TruncatedText"; import { PatchComponent, PatchContainerComponent } from "src/patch"; +import { SceneMergeModal } from "../SceneMergeDialog"; +import { goBackOrReplace } from "src/utils/history"; +import { FormattedDate } from "src/components/Shared/Date"; const SubmitStashBoxDraft = lazyComponent( () => import("src/components/Dialogs/SubmitDraft") @@ -181,9 +186,10 @@ const ScenePage: React.FC = PatchComponent("ScenePage", (props) => { const Toast = useToast(); const intl = useIntl(); + const history = useHistory(); const [updateScene] = useSceneUpdate(); const [generateScreenshot] = useSceneGenerateScreenshot(); - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const [showDraftModal, setShowDraftModal] = useState(false); const boxes = configuration?.general?.stashBoxes ?? []; @@ -204,6 +210,7 @@ const ScenePage: React.FC = PatchComponent("ScenePage", (props) => { const [activeTabKey, setActiveTabKey] = useState("scene-details-panel"); + const [isMerging, setIsMerging] = useState(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false); @@ -247,6 +254,12 @@ const ScenePage: React.FC = PatchComponent("ScenePage", (props) => { Mousetrap.bind("p p", () => onQueuePrevious()); Mousetrap.bind("p r", () => onQueueRandom()); Mousetrap.bind(",", () => setCollapsed(!collapsed)); + Mousetrap.bind("c c", () => { + onGenerateScreenshot(getPlayerPosition()); + }); + Mousetrap.bind("c d", () => { + onGenerateScreenshot(); + }); return () => { Mousetrap.unbind("a"); @@ -260,6 +273,8 @@ const ScenePage: React.FC = PatchComponent("ScenePage", (props) => { Mousetrap.unbind("p p"); Mousetrap.unbind("p r"); Mousetrap.unbind(","); + Mousetrap.unbind("c c"); + Mousetrap.unbind("c d"); }; }); @@ -299,9 +314,53 @@ const ScenePage: React.FC = PatchComponent("ScenePage", (props) => { }; function onClickMarker(marker: GQL.SceneMarkerDataFragment) { + const abLoopPlugin = getAbLoopPlugin(); + const opts = abLoopPlugin?.getOptions(); + const start = opts?.start; + const end = opts?.end; + + const hasLoopRange = + opts?.enabled && + typeof start === "number" && + typeof end === "number" && + Number.isFinite(start) && + Number.isFinite(end); + + if ( + abLoopPlugin && + opts && + hasLoopRange && + (marker.seconds < Math.min(start as number, end as number) || + marker.seconds > Math.max(start as number, end as number)) + ) { + abLoopPlugin.setOptions({ + ...opts, + enabled: false, + }); + } + setTimestamp(marker.seconds); } + function onLoopMarker(marker: GQL.SceneMarkerDataFragment) { + if (marker.end_seconds == null) return; + + setTimestamp(marker.seconds); + const start = Math.min(marker.seconds, marker.end_seconds); + const end = Math.max(marker.seconds, marker.end_seconds); + const abLoopPlugin = getAbLoopPlugin(); + const opts = abLoopPlugin?.getOptions(); + + if (opts && abLoopPlugin) { + abLoopPlugin.setOptions({ + ...opts, + start, + end, + enabled: true, + }); + } + } + async function onRescan() { await mutateMetadataScan({ paths: [objectPath(scene)], @@ -338,6 +397,24 @@ const ScenePage: React.FC = PatchComponent("ScenePage", (props) => { } } + function maybeRenderMergeDialog() { + if (!scene.id) return; + return ( + { + setIsMerging(false); + if (mergedId !== undefined && mergedId !== scene.id) { + // By default, the merge destination is the current scene, but + // the user can change it, in which case we need to redirect. + history.replace(`/scenes/${mergedId}`); + } + }} + scenes={[{ id: scene.id, title: objectTitle(scene) }]} + /> + ); + } + function maybeRenderDeleteDialog() { if (isDeleteAlertOpen) { return ( @@ -385,7 +462,7 @@ const ScenePage: React.FC = PatchComponent("ScenePage", (props) => { className="bg-secondary text-white" onClick={() => setIsGenerateDialogOpen(true)} > - + = PatchComponent("ScenePage", (props) => { )} + setIsMerging(true)} + > + + ... + = PatchComponent("ScenePage", (props) => { @@ -579,6 +665,7 @@ const ScenePage: React.FC = PatchComponent("ScenePage", (props) => { {title} {maybeRenderSceneGenerateDialog()} + {maybeRenderMergeDialog()} {maybeRenderDeleteDialog()}
        = PatchComponent("ScenePage", (props) => {
        - {!!scene.date && ( - - )} + {!!scene.date && } > = ({ match, }) => { const { id } = match.params; - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const { data, loading, error } = useFindScene(id); const [scene, setScene] = useState(); @@ -909,7 +990,7 @@ const SceneLoader: React.FC> = ({ ) { loadScene(queueScenes[currentQueueIndex + 1].id); } else { - history.goBack(); + goBackOrReplace(history, "/scenes"); } } diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneCreate.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneCreate.tsx index 707740605..8e3807c83 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneCreate.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneCreate.tsx @@ -57,14 +57,16 @@ const SceneCreate: React.FC = () => { return ; } - async function onSave(input: GQL.SceneCreateInput) { + async function onSave(input: GQL.SceneCreateInput, andNew?: boolean) { const fileID = query.get("file_id") ?? undefined; const result = await mutateCreateScene({ ...input, file_ids: fileID ? [fileID] : undefined, }); if (result.data?.sceneCreate?.id) { - history.push(`/scenes/${result.data.sceneCreate.id}`); + if (!andNew) { + history.push(`/scenes/${result.data.sceneCreate.id}`); + } Toast.success( intl.formatMessage( { id: "toast.created_entity" }, diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index 69b378787..54bf5b573 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -1,6 +1,14 @@ import React, { useEffect, useState, useMemo } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { Button, Form, Col, Row, ButtonGroup } from "react-bootstrap"; +import { + Button, + Dropdown, + Form, + Col, + Row, + ButtonGroup, + SplitButton, +} from "react-bootstrap"; import Mousetrap from "mousetrap"; import * as GQL from "src/core/generated-graphql"; import * as yup from "yup"; @@ -16,12 +24,12 @@ import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { ImageInput } from "src/components/Shared/ImageInput"; import { useToast } from "src/hooks/Toast"; import ImageUtils from "src/utils/image"; -import { getStashIDs } from "src/utils/stashIds"; +import { addUpdateStashID, getStashIDs } from "src/utils/stashIds"; import { useFormik } from "formik"; import { Prompt } from "react-router-dom"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { IGroupEntry, SceneGroupTable } from "./SceneGroupTable"; -import { faSearch } from "@fortawesome/free-solid-svg-icons"; +import { faSearch, faPlus } from "@fortawesome/free-solid-svg-icons"; import { objectTitle } from "src/core/files"; import { galleryTitle } from "src/core/galleries"; import { lazyComponent } from "src/utils/lazyComponent"; @@ -41,6 +49,7 @@ import { Gallery, GallerySelect } from "src/components/Galleries/GallerySelect"; import { Group } from "src/components/Groups/GroupSelect"; import { useTagsEdit } from "src/hooks/tagsEdit"; import { ScraperMenu } from "src/components/Shared/ScraperMenu"; +import StashBoxIDSearchModal from "src/components/Shared/StashBoxIDSearchModal"; const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog")); const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal")); @@ -50,7 +59,7 @@ interface IProps { initialCoverImage?: string; isNew?: boolean; isVisible: boolean; - onSubmit: (input: GQL.SceneCreateInput) => Promise; + onSubmit: (input: GQL.SceneCreateInput, andNew?: boolean) => Promise; onDelete?: () => void; } @@ -77,6 +86,8 @@ export const SceneEditPanel: React.FC = ({ const [scraper, setScraper] = useState(); const [isScraperQueryModalOpen, setIsScraperQueryModalOpen] = useState(false); + const [isStashIDSearchOpen, setIsStashIDSearchOpen] = + useState(false); const [scrapedScene, setScrapedScene] = useState(); const [endpoint, setEndpoint] = useState(); @@ -103,7 +114,7 @@ export const SceneEditPanel: React.FC = ({ setStudio(scene.studio ?? null); }, [scene.studio]); - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); // Network state const [isLoading, setIsLoading] = useState(false); @@ -265,10 +276,10 @@ export const SceneEditPanel: React.FC = ({ formik.setFieldValue("groups", newGroups); } - async function onSave(input: InputValues) { + async function onSave(input: InputValues, andNew?: boolean) { setIsLoading(true); try { - await onSubmit(input); + await onSubmit(input, andNew); formik.resetForm(); } catch (e) { Toast.error(e); @@ -276,6 +287,11 @@ export const SceneEditPanel: React.FC = ({ setIsLoading(false); } + async function onSaveAndNewClick() { + const input = schema.cast(formik.values); + onSave(input, true); + } + const encodingImage = ImageUtils.usePasteImage(onImageLoad); function onImageLoad(imageData: string) { @@ -286,6 +302,10 @@ export const SceneEditPanel: React.FC = ({ ImageUtils.onImageChange(event, onImageLoad); } + function onResetCover() { + formik.setFieldValue("cover_image", null); + } + async function onScrapeClicked(s: GQL.ScraperSourceInput) { setIsLoading(true); try { @@ -547,6 +567,14 @@ export const SceneEditPanel: React.FC = ({ } } + function onStashIDSelected(item?: GQL.StashIdInput) { + if (!item) return; + formik.setFieldValue( + "stash_ids", + addUpdateStashID(formik.values.stash_ids, item) + ); + } + const image = useMemo(() => { if (encodingImage) { return ( @@ -591,6 +619,19 @@ export const SceneEditPanel: React.FC = ({ xl: 12, }, }; + const urlProps = isNew + ? splitProps + : { + labelProps: { + column: true, + md: 3, + lg: 12, + }, + fieldProps: { + md: 9, + lg: 12, + }, + }; const { renderField, renderInputField, @@ -696,19 +737,48 @@ export const SceneEditPanel: React.FC = ({ {renderScrapeQueryModal()} {maybeRenderScrapeDialog()} + {isStashIDSearchOpen && ( + s.endpoint + )} + onSelectItem={(item) => { + onStashIDSelected(item); + setIsStashIDSearchOpen(false); + }} + initialQuery={scene.title ?? ""} + /> + )}
        - + {isNew ? ( + formik.submitForm()} + > + onSaveAndNewClick()}> + + + + ) : ( + + )} {onDelete && ( )} @@ -775,6 +860,7 @@ export const SceneEditPanel: React.FC = ({ isEditing onImageChange={onCoverImageChange} onImageURL={onImageLoad} + onReset={scene.id ? onResetCover : undefined} /> diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneHistoryPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneHistoryPanel.tsx index 1ac9dd5a2..2ba587a2b 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneHistoryPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneHistoryPanel.tsx @@ -21,6 +21,7 @@ import { useSceneResetActivity, } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; +import { useConfigurationContext } from "src/hooks/Config"; import { useToast } from "src/hooks/Toast"; import { TextField } from "src/utils/field"; import TextUtils from "src/utils/text"; @@ -172,6 +173,9 @@ export const SceneHistoryPanel: React.FC = ({ scene }) => { const intl = useIntl(); const Toast = useToast(); + const { configuration } = useConfigurationContext(); + const { sfwContentMode } = configuration.interface; + const [dialogs, setDialogs] = React.useState({ playHistory: false, oHistory: false, @@ -299,6 +303,9 @@ export const SceneHistoryPanel: React.FC = ({ scene }) => { } function maybeRenderDialogs() { + const clearHistoryMessageID = sfwContentMode + ? "dialogs.clear_o_history_confirm_sfw" + : "dialogs.clear_play_history_confirm"; return ( <> = ({ scene }) => { /> handleClearODates()} onCancel={() => setDialogPartial({ oHistory: false })} @@ -351,6 +358,11 @@ export const SceneHistoryPanel: React.FC = ({ scene }) => { ) as string[]; const oHistory = (scene.o_history ?? []).filter((h) => h != null) as string[]; + const oHistoryMessageID = sfwContentMode ? "o_history_sfw" : "o_history"; + const noneMessageID = sfwContentMode + ? "odate_recorded_no_sfw" + : "odate_recorded_no"; + return (
        {maybeRenderDialogs()} @@ -401,7 +413,7 @@ export const SceneHistoryPanel: React.FC = ({ scene }) => {
        - + @@ -427,7 +439,7 @@ export const SceneHistoryPanel: React.FC = ({ scene }) => {
        handleDeleteODate(t)} /> diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx index 1670bcc7b..cbb2ad4bb 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx @@ -11,7 +11,10 @@ import { } from "src/core/StashService"; import { DurationInput } from "src/components/Shared/DurationInput"; import { MarkerTitleSuggest } from "src/components/Shared/Select"; -import { getPlayerPosition } from "src/components/ScenePlayer/util"; +import { + getAbLoopPlugin, + getPlayerPosition, +} from "src/components/ScenePlayer/util"; import { useToast } from "src/hooks/Toast"; import isEqual from "lodash-es/isEqual"; import { formikUtils } from "src/utils/form"; @@ -61,16 +64,39 @@ export const SceneMarkerForm: React.FC = ({ }); // useMemo to only run getPlayerPosition when the input marker actually changes - const initialValues = useMemo( - () => ({ + const initialValues = useMemo(() => { + if (!marker) { + const abLoopPlugin = getAbLoopPlugin(); + const opts = abLoopPlugin?.getOptions(); + const start = opts?.start; + const end = opts?.end; + const hasAbLoop = Number.isFinite(start); + + if (hasAbLoop) { + const current = Math.round(getPlayerPosition() ?? 0); + const rawEnd = + Number.isFinite(end) && (end as number) > 0 ? (end as number) : null; + const endSeconds = + rawEnd !== null ? rawEnd : Math.max(start as number, current); + + return { + title: "", + seconds: start as number, + end_seconds: endSeconds, + primary_tag_id: "", + tag_ids: [], + }; + } + } + + return { title: marker?.title ?? "", seconds: marker?.seconds ?? Math.round(getPlayerPosition() ?? 0), end_seconds: marker?.end_seconds ?? null, primary_tag_id: marker?.primary_tag.id ?? "", tag_ids: marker?.tags.map((tag) => tag.id) ?? [], - }), - [marker] - ); + }; + }, [marker]); type InputValues = yup.InferType; @@ -96,7 +122,9 @@ export const SceneMarkerForm: React.FC = ({ useEffect(() => { setPrimaryTag( - marker?.primary_tag ? { ...marker.primary_tag, aliases: [] } : undefined + marker?.primary_tag + ? { ...marker.primary_tag, aliases: [], stash_ids: [] } + : undefined ); }, [marker?.primary_tag]); @@ -105,6 +133,7 @@ export const SceneMarkerForm: React.FC = ({ marker?.tags.map((t) => ({ ...t, aliases: [], + stash_ids: [], })) ?? [] ); }, [marker?.tags]); diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx index 331c58c78..28a6e4d98 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx @@ -11,12 +11,14 @@ interface ISceneMarkersPanelProps { sceneId: string; isVisible: boolean; onClickMarker: (marker: GQL.SceneMarkerDataFragment) => void; + onLoopMarker: (marker: GQL.SceneMarkerDataFragment) => void; } export const SceneMarkersPanel: React.FC = ({ sceneId, isVisible, onClickMarker, + onLoopMarker, }) => { const { data, loading } = GQL.useFindSceneMarkerTagsQuery({ variables: { id: sceneId }, @@ -70,6 +72,7 @@ export const SceneMarkersPanel: React.FC = ({
        diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx index d6acfabfb..9b9a6bc40 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx @@ -1,12 +1,12 @@ import React, { useState } from "react"; import * as GQL from "src/core/generated-graphql"; import { - ScrapeDialog, ScrapedInputGroupRow, ScrapedTextAreaRow, ScrapedImageRow, ScrapedStringListRow, -} from "src/components/Shared/ScrapeDialog/ScrapeDialog"; +} from "src/components/Shared/ScrapeDialog/ScrapeDialogRow"; +import { ScrapeDialog } from "src/components/Shared/ScrapeDialog/ScrapeDialog"; import { useIntl } from "react-intl"; import { uniq } from "lodash-es"; import { Performer } from "src/components/Performers/PerformerSelect"; @@ -131,9 +131,10 @@ export const SceneScrapeDialog: React.FC = ({ scraped.groups?.filter((t) => !t.stored_id) ?? [] ); - const { tags, newTags, scrapedTagsRow } = useScrapedTags( + const { tags, newTags, scrapedTagsRow, linkDialog } = useScrapedTags( sceneTags, - scraped.tags + scraped.tags, + endpoint ); const [details, setDetails] = useState>( @@ -148,6 +149,7 @@ export const SceneScrapeDialog: React.FC = ({ scrapeResult: studio, setScrapeResult: setStudio, setNewObject: setNewStudio, + endpoint, }); const createNewPerformer = useCreateScrapedPerformer({ @@ -155,6 +157,7 @@ export const SceneScrapeDialog: React.FC = ({ setScrapeResult: setPerformers, newObjects: newPerformers, setNewObjects: setNewPerformers, + endpoint, }); const createNewGroup = useCreateScrapedGroup({ @@ -162,6 +165,7 @@ export const SceneScrapeDialog: React.FC = ({ setScrapeResult: setGroups, newObjects: newGroups, setNewObjects: setNewGroups, + endpoint, }); const intl = useIntl(); @@ -214,32 +218,38 @@ export const SceneScrapeDialog: React.FC = ({ return ( <> setTitle(value)} /> setCode(value)} /> setURLs(value)} /> setDate(value)} /> setDirector(value)} /> setStudio(value)} @@ -247,6 +257,7 @@ export const SceneScrapeDialog: React.FC = ({ onCreateNew={createNewStudio} /> setPerformers(value)} @@ -255,6 +266,7 @@ export const SceneScrapeDialog: React.FC = ({ ageFromDate={date.useNewValue ? date.newValue : date.originalValue} /> setGroups(value)} @@ -263,17 +275,20 @@ export const SceneScrapeDialog: React.FC = ({ /> {scrapedTagsRow} setDetails(value)} /> setStashID(value)} /> = ({ ); } + if (linkDialog) { + return linkDialog; + } + return ( { onClose(apply ? makeNewScrapedItem() : undefined); }} - /> + > + {renderScrapeRows()} + ); }; diff --git a/ui/v2.5/src/components/Scenes/SceneList.tsx b/ui/v2.5/src/components/Scenes/SceneList.tsx index a59d2c395..2f74fc7e4 100644 --- a/ui/v2.5/src/components/Scenes/SceneList.tsx +++ b/ui/v2.5/src/components/Scenes/SceneList.tsx @@ -1,7 +1,7 @@ -import React, { useCallback, useContext, useEffect, useMemo } from "react"; +import React, { useCallback, useEffect, useMemo } from "react"; import cloneDeep from "lodash-es/cloneDeep"; import { FormattedMessage, useIntl } from "react-intl"; -import { useHistory } from "react-router-dom"; +import { useHistory, useLocation } from "react-router-dom"; import Mousetrap from "mousetrap"; import * as GQL from "src/core/generated-graphql"; import { queryFindScenes, useFindScenes } from "src/core/StashService"; @@ -15,17 +15,10 @@ import { EditScenesDialog } from "./EditScenesDialog"; import { DeleteScenesDialog } from "./DeleteScenesDialog"; import { GenerateDialog } from "../Dialogs/GenerateDialog"; import { ExportDialog } from "../Shared/ExportDialog"; -import { SceneCardsGrid } from "./SceneCardsGrid"; +import { SceneCardGrid } from "./SceneCardGrid"; import { TaggerContext } from "../Tagger/context"; import { IdentifyDialog } from "../Dialogs/IdentifyDialog/IdentifyDialog"; -import { ConfigurationContext } from "src/hooks/Config"; -import { - faPencil, - faPlay, - faPlus, - faTimes, - faTrash, -} from "@fortawesome/free-solid-svg-icons"; +import { useConfigurationContext } from "src/hooks/Config"; import { SceneMergeModal } from "./SceneMergeDialog"; import { objectTitle } from "src/core/files"; import TextUtils from "src/utils/text"; @@ -33,36 +26,37 @@ import { View } from "../List/views"; import { FileSize } from "../Shared/FileSize"; import { LoadedContent } from "../List/PagedList"; import { useCloseEditDelete, useFilterOperations } from "../List/util"; -import { - OperationDropdown, - OperationDropdownItem, -} from "../List/ListOperationButtons"; +import { ListOperations } from "../List/ListOperationButtons"; import { useFilteredItemList } from "../List/ItemList"; -import { FilterTags } from "../List/FilterTags"; -import { Sidebar, SidebarPane, useSidebarState } from "../Shared/Sidebar"; +import { + Sidebar, + SidebarPane, + SidebarPaneContent, + SidebarStateContext, + useSidebarState, +} from "../Shared/Sidebar"; import { SidebarPerformersFilter } from "../List/Filters/PerformersFilter"; import { SidebarStudiosFilter } from "../List/Filters/StudiosFilter"; -import { PerformersCriterionOption } from "src/models/list-filter/criteria/performers"; -import { StudiosCriterionOption } from "src/models/list-filter/criteria/studios"; -import { TagsCriterionOption } from "src/models/list-filter/criteria/tags"; import { SidebarTagsFilter } from "../List/Filters/TagsFilter"; import cx from "classnames"; -import { RatingCriterionOption } from "src/models/list-filter/criteria/rating"; import { SidebarRatingFilter } from "../List/Filters/RatingFilter"; import { OrganizedCriterionOption } from "src/models/list-filter/criteria/organized"; +import { HasMarkersCriterionOption } from "src/models/list-filter/criteria/has-markers"; import { SidebarBooleanFilter } from "../List/Filters/BooleanFilter"; +import { PerformerAgeCriterionOption } from "src/models/list-filter/scenes"; +import { SidebarAgeFilter } from "../List/Filters/SidebarAgeFilter"; +import { SidebarDurationFilter } from "../List/Filters/SidebarDurationFilter"; import { FilteredSidebarHeader, useFilteredSidebarKeybinds, } from "../List/Filters/FilterSidebar"; -import { PatchContainerComponent } from "src/patch"; +import { PatchComponent, PatchContainerComponent } from "src/patch"; import { Pagination, PaginationIndex } from "../List/Pagination"; -import { Button, ButtonGroup, ButtonToolbar } from "react-bootstrap"; -import { FilterButton } from "../List/Filters/FilterButton"; -import { Icon } from "../Shared/Icon"; -import { ListViewOptions } from "../List/ListViewOptions"; -import { PageSizeSelector, SortBySelect } from "../List/ListFilter"; -import { Criterion } from "src/models/list-filter/criteria/criterion"; +import { Button } from "react-bootstrap"; +import useFocus from "src/utils/focus"; +import { useZoomKeybinds } from "../List/ZoomSlider"; +import { FilteredListToolbar } from "../List/FilteredListToolbar"; +import { FilterTags } from "../List/FilterTags"; function renderMetadataByline(result: GQL.FindScenesQueryResult) { const duration = result?.data?.findScenes?.duration; @@ -96,7 +90,7 @@ function renderMetadataByline(result: GQL.FindScenesQueryResult) { function usePlayScene() { const history = useHistory(); - const { configuration: config } = useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const cont = config?.interface.continuePlaylistDefault ?? false; const autoPlay = config?.interface.autostartVideoOnPlaySelected ?? false; @@ -189,50 +183,65 @@ const SceneList: React.FC<{ selectedIds: Set; onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; fromGroupId?: string; -}> = ({ scenes, filter, selectedIds, onSelectChange, fromGroupId }) => { - const queue = useMemo(() => SceneQueue.fromListFilterModel(filter), [filter]); +}> = PatchComponent( + "SceneList", + ({ scenes, filter, selectedIds, onSelectChange, fromGroupId }) => { + const queue = useMemo( + () => SceneQueue.fromListFilterModel(filter), + [filter] + ); + + if (scenes.length === 0 && filter.displayMode !== DisplayMode.Tagger) { + return null; + } + + if (filter.displayMode === DisplayMode.Grid) { + return ( + + ); + } + if (filter.displayMode === DisplayMode.List) { + return ( + + ); + } + if (filter.displayMode === DisplayMode.Wall) { + return ( + + ); + } + if (filter.displayMode === DisplayMode.Tagger) { + return ( + + ); + } - if (scenes.length === 0) { return null; } - - if (filter.displayMode === DisplayMode.Grid) { - return ( - - ); - } - if (filter.displayMode === DisplayMode.List) { - return ( - - ); - } - if (filter.displayMode === DisplayMode.Wall) { - return ( - - ); - } - if (filter.displayMode === DisplayMode.Tagger) { - return ; - } - - return null; -}; +); const ScenesFilterSidebarSections = PatchContainerComponent( "FilteredSceneList.SidebarSections" @@ -247,6 +256,7 @@ const SidebarContent: React.FC<{ onClose?: () => void; showEditFilter: (editingCriterion?: string) => void; count?: number; + focus?: ReturnType; }> = ({ filter, setFilter, @@ -256,6 +266,7 @@ const SidebarContent: React.FC<{ sidebarOpen, onClose, count, + focus, }) => { const showResultsId = count !== undefined ? "actions.show_count_results" : "actions.show_results"; @@ -270,41 +281,36 @@ const SidebarContent: React.FC<{ filter={filter} setFilter={setFilter} view={view} + focus={focus} /> {!hideStudios && ( } - data-type={StudiosCriterionOption.type} - option={StudiosCriterionOption} filter={filter} setFilter={setFilter} filterHook={filterHook} /> )} } - data-type={PerformersCriterionOption.type} - option={PerformersCriterionOption} filter={filter} setFilter={setFilter} filterHook={filterHook} /> } - data-type={TagsCriterionOption.type} - option={TagsCriterionOption} filter={filter} setFilter={setFilter} filterHook={filterHook} /> - } - data-type={RatingCriterionOption.type} - option={RatingCriterionOption} + + + } + data-type={HasMarkersCriterionOption.type} + option={HasMarkersCriterionOption} filter={filter} setFilter={setFilter} + sectionID="hasMarkers" /> } @@ -312,6 +318,14 @@ const SidebarContent: React.FC<{ option={OrganizedCriterionOption} filter={filter} setFilter={setFilter} + sectionID="organized" + /> + } + option={PerformerAgeCriterionOption} + filter={filter} + setFilter={setFilter} + sectionID="performer_age" /> @@ -324,196 +338,6 @@ const SidebarContent: React.FC<{ ); }; -interface IOperations { - text: string; - onClick: () => void; - isDisplayed?: () => boolean; - className?: string; -} - -const ListToolbarContent: React.FC<{ - criteria: Criterion[]; - items: GQL.SlimSceneDataFragment[]; - selectedIds: Set; - operations: IOperations[]; - onToggleSidebar: () => void; - onEditCriterion: (c: Criterion) => void; - onRemoveCriterion: (criterion: Criterion, valueIndex?: number) => void; - onRemoveAllCriterion: () => void; - onSelectAll: () => void; - onSelectNone: () => void; - onEdit: () => void; - onDelete: () => void; - onPlay: () => void; - onCreateNew: () => void; -}> = ({ - criteria, - items, - selectedIds, - operations, - onToggleSidebar, - onEditCriterion, - onRemoveCriterion, - onRemoveAllCriterion, - onSelectAll, - onSelectNone, - onEdit, - onDelete, - onPlay, - onCreateNew, -}) => { - const intl = useIntl(); - - const hasSelection = selectedIds.size > 0; - - return ( - <> - {!hasSelection && ( -
        - onToggleSidebar()} - count={criteria.length} - title={intl.formatMessage({ id: "actions.sidebar.toggle" })} - /> - -
        - )} - {hasSelection && ( -
        - - {selectedIds.size} selected - -
        - )} -
        - - {!!items.length && ( - - )} - {!hasSelection && ( - - )} - - {hasSelection && ( - <> - - - - )} - - - {operations.map((o) => { - if (o.isDisplayed && !o.isDisplayed()) { - return null; - } - - return ( - - ); - })} - - -
        - - ); -}; - -const ListResultsHeader: React.FC<{ - loading: boolean; - filter: ListFilterModel; - totalCount: number; - metadataByline?: React.ReactNode; - onChangeFilter: (filter: ListFilterModel) => void; -}> = ({ loading, filter, totalCount, metadataByline, onChangeFilter }) => { - return ( - -
        - -
        -
        - - onChangeFilter(filter.setSortBy(s ?? undefined)) - } - onChangeSortDirection={() => - onChangeFilter(filter.toggleSortDirection()) - } - onReshuffleRandomSort={() => - onChangeFilter(filter.reshuffleRandomSort()) - } - /> - onChangeFilter(filter.setPageSize(s))} - /> - - onChangeFilter(filter.setDisplayMode(mode)) - } - onSetZoom={(zoom) => onChangeFilter(filter.setZoom(zoom))} - /> -
        -
        - ); -}; - interface IFilteredScenes { filterHook?: (filter: ListFilterModel) => ListFilterModel; defaultSort?: string; @@ -522,325 +346,381 @@ interface IFilteredScenes { fromGroupId?: string; } -export const FilteredSceneList = (props: IFilteredScenes) => { - const intl = useIntl(); - const history = useHistory(); +export const FilteredSceneList = PatchComponent( + "FilteredSceneList", + (props: IFilteredScenes) => { + const intl = useIntl(); + const history = useHistory(); + const location = useLocation(); - const { filterHook, defaultSort, view, alterQuery, fromGroupId } = props; + const searchFocus = useFocus(); - // States - const { - showSidebar, - setShowSidebar, - loading: sidebarStateLoading, - } = useSidebarState(view); + const { filterHook, defaultSort, view, alterQuery, fromGroupId } = props; - const { filterState, queryResult, modalState, listSelect, showEditFilter } = - useFilteredItemList({ - filterStateProps: { - filterMode: GQL.FilterMode.Scenes, - defaultSort, - view, - useURL: alterQuery, - }, - queryResultProps: { - useResult: useFindScenes, - getCount: (r) => r.data?.findScenes.count ?? 0, - getItems: (r) => r.data?.findScenes.scenes ?? [], - filterHook, - }, + // States + const { + showSidebar, + setShowSidebar, + loading: sidebarStateLoading, + sectionOpen, + setSectionOpen, + } = useSidebarState(view); + + const { filterState, queryResult, modalState, listSelect, showEditFilter } = + useFilteredItemList({ + filterStateProps: { + filterMode: GQL.FilterMode.Scenes, + defaultSort, + view, + useURL: alterQuery, + }, + queryResultProps: { + useResult: useFindScenes, + getCount: (r) => r.data?.findScenes.count ?? 0, + getItems: (r) => r.data?.findScenes.scenes ?? [], + filterHook, + }, + }); + + const { filter, setFilter } = filterState; + + const { effectiveFilter, result, cachedResult, items, totalCount } = + queryResult; + + const { + selectedIds, + selectedItems, + onSelectChange, + onSelectAll, + onSelectNone, + onInvertSelection, + hasSelection, + } = listSelect; + + const { modal, showModal, closeModal } = modalState; + + // Utility hooks + const { setPage, removeCriterion, clearAllCriteria } = useFilterOperations({ + filter, + setFilter, }); - const { filter, setFilter, loading: filterLoading } = filterState; + useAddKeybinds(filter, totalCount); + useFilteredSidebarKeybinds({ + showSidebar, + setShowSidebar, + }); - const { effectiveFilter, result, cachedResult, items, totalCount } = - queryResult; + const onCloseEditDelete = useCloseEditDelete({ + closeModal, + onSelectNone, + result, + }); - const { - selectedIds, - selectedItems, - onSelectChange, - onSelectAll, - onSelectNone, - hasSelection, - } = listSelect; + const onEdit = useCallback(() => { + showModal( + + ); + }, [showModal, selectedItems, onCloseEditDelete]); - const { modal, showModal, closeModal } = modalState; + const onDelete = useCallback(() => { + showModal( + + ); + }, [showModal, selectedItems, onCloseEditDelete]); - // Utility hooks - const { setPage, removeCriterion, clearAllCriteria } = useFilterOperations({ - filter, - setFilter, - }); + useEffect(() => { + Mousetrap.bind("e", () => { + if (hasSelection) { + onEdit?.(); + } + }); - useAddKeybinds(filter, totalCount); - useFilteredSidebarKeybinds({ - showSidebar, - setShowSidebar, - }); + Mousetrap.bind("d d", () => { + if (hasSelection) { + onDelete?.(); + } + }); - useEffect(() => { - Mousetrap.bind("e", () => { - if (hasSelection) { - onEdit?.(); + return () => { + Mousetrap.unbind("e"); + Mousetrap.unbind("d d"); + }; + }, [onSelectAll, onSelectNone, hasSelection, onEdit, onDelete]); + useZoomKeybinds({ + zoomIndex: filter.zoomIndex, + onChangeZoom: (zoom) => setFilter(filter.setZoom(zoom)), + }); + + const metadataByline = useMemo(() => { + if (cachedResult.loading) return null; + + return renderMetadataByline(cachedResult) ?? null; + }, [cachedResult]); + + const queue = useMemo( + () => SceneQueue.fromListFilterModel(filter), + [filter] + ); + + const playRandom = usePlayRandom(effectiveFilter, totalCount); + const playSelected = usePlaySelected(selectedIds); + const playFirst = usePlayFirst(); + + function onCreateNew() { + let queryParam = new URLSearchParams(location.search).get("q"); + let newPath = "/scenes/new"; + if (queryParam) { + newPath += "?q=" + encodeURIComponent(queryParam); } - }); - - Mousetrap.bind("d d", () => { - if (hasSelection) { - onDelete?.(); - } - }); - - return () => { - Mousetrap.unbind("e"); - Mousetrap.unbind("d d"); - }; - }); - - const onCloseEditDelete = useCloseEditDelete({ - closeModal, - onSelectNone, - result, - }); - - const metadataByline = useMemo(() => { - if (cachedResult.loading) return null; - - return renderMetadataByline(cachedResult) ?? null; - }, [cachedResult]); - - const queue = useMemo(() => SceneQueue.fromListFilterModel(filter), [filter]); - - const playRandom = usePlayRandom(filter, totalCount); - const playSelected = usePlaySelected(selectedIds); - const playFirst = usePlayFirst(); - - function onCreateNew() { - history.push("/scenes/new"); - } - - function onPlay() { - if (items.length === 0) { - return; + history.push(newPath); } - // if there are selected items, play those - if (hasSelection) { - playSelected(); - return; + function onPlay() { + if (items.length === 0) { + return; + } + + // if there are selected items, play those + if (hasSelection) { + playSelected(); + return; + } + + // otherwise, play the first item in the list + const sceneID = items[0].id; + playFirst(queue, sceneID, 0); } - // otherwise, play the first item in the list - const sceneID = items[0].id; - playFirst(queue, sceneID, 0); - } + function onExport(all: boolean) { + showModal( + closeModal()} + /> + ); + } - function onExport(all: boolean) { - showModal( - closeModal()} - /> - ); - } + function onMerge() { + const selected = + selectedItems.map((s) => { + return { + id: s.id, + title: objectTitle(s), + }; + }) ?? []; + showModal( + { + closeModal(); + if (mergedID) { + history.push(`/scenes/${mergedID}`); + } + }} + show + /> + ); + } - function onMerge() { - const selected = - selectedItems.map((s) => { - return { - id: s.id, - title: objectTitle(s), - }; - }) ?? []; - showModal( - { - closeModal(); - if (mergedID) { - history.push(`/scenes/${mergedID}`); - } - }} - show - /> - ); - } - - function onEdit() { - showModal( - - ); - } - - function onDelete() { - showModal( - - ); - } - - const otherOperations = [ - { - text: intl.formatMessage({ id: "actions.play" }), - onClick: () => onPlay(), - isDisplayed: () => items.length > 0, - className: "play-item", - }, - { - text: intl.formatMessage( - { id: "actions.create_entity" }, - { entityType: intl.formatMessage({ id: "scene" }) } - ), - onClick: () => onCreateNew(), - isDisplayed: () => !hasSelection, - className: "create-new-item", - }, - { - text: intl.formatMessage({ id: "actions.select_all" }), - onClick: () => onSelectAll(), - isDisplayed: () => totalCount > 0, - }, - { - text: intl.formatMessage({ id: "actions.select_none" }), - onClick: () => onSelectNone(), - isDisplayed: () => hasSelection, - }, - { - text: intl.formatMessage({ id: "actions.play_random" }), - onClick: playRandom, - isDisplayed: () => totalCount > 1, - }, - { - text: `${intl.formatMessage({ id: "actions.generate" })}…`, - onClick: () => - showModal( - closeModal()} - /> + const otherOperations = [ + { + text: intl.formatMessage({ id: "actions.play" }), + onClick: () => onPlay(), + isDisplayed: () => items.length > 0, + className: "play-item", + }, + { + text: intl.formatMessage( + { id: "actions.create_entity" }, + { entityType: intl.formatMessage({ id: "scene" }) } ), - isDisplayed: () => hasSelection, - }, - { - text: `${intl.formatMessage({ id: "actions.identify" })}…`, - onClick: () => - showModal( - closeModal()} - /> - ), - isDisplayed: () => hasSelection, - }, - { - text: `${intl.formatMessage({ id: "actions.merge" })}…`, - onClick: () => onMerge(), - isDisplayed: () => hasSelection, - }, - { - text: intl.formatMessage({ id: "actions.export" }), - onClick: () => onExport(false), - isDisplayed: () => hasSelection, - }, - { - text: intl.formatMessage({ id: "actions.export_all" }), - onClick: () => onExport(true), - }, - ]; - - // render - if (filterLoading || sidebarStateLoading) return null; - - return ( - -
        - {modal} - - - setShowSidebar(false)}> - setShowSidebar(false)} - count={cachedResult.loading ? undefined : totalCount} + onClick: () => onCreateNew(), + isDisplayed: () => !hasSelection, + className: "create-new-item", + }, + { + text: intl.formatMessage({ id: "actions.select_all" }), + onClick: () => onSelectAll(), + isDisplayed: () => totalCount > 0, + }, + { + text: intl.formatMessage({ id: "actions.select_none" }), + onClick: () => onSelectNone(), + isDisplayed: () => hasSelection, + }, + { + text: intl.formatMessage({ id: "actions.invert_selection" }), + onClick: () => onInvertSelection(), + isDisplayed: () => totalCount > 0, + }, + { + text: intl.formatMessage({ id: "actions.play_random" }), + onClick: playRandom, + isDisplayed: () => totalCount > 1, + }, + { + text: `${intl.formatMessage({ id: "actions.generate" })}…`, + onClick: () => + showModal( + closeModal()} /> - -
        - - setShowSidebar(!showSidebar)} - onEditCriterion={(c) => showEditFilter(c.criterionOption.type)} - onRemoveCriterion={removeCriterion} - onRemoveAllCriterion={() => clearAllCriteria()} - onSelectAll={() => onSelectAll()} - onSelectNone={() => onSelectNone()} - onEdit={onEdit} - onDelete={onDelete} - onCreateNew={onCreateNew} - onPlay={onPlay} - /> - - - setFilter(newFilter)} + ), + isDisplayed: () => hasSelection, + }, + { + text: `${intl.formatMessage({ id: "actions.identify" })}…`, + onClick: () => + showModal( + closeModal()} /> + ), + isDisplayed: () => hasSelection, + }, + { + text: `${intl.formatMessage({ id: "actions.merge" })}…`, + onClick: () => onMerge(), + isDisplayed: () => hasSelection, + }, + { + text: intl.formatMessage({ id: "actions.export" }), + onClick: () => onExport(false), + isDisplayed: () => hasSelection, + }, + { + text: intl.formatMessage({ id: "actions.export_all" }), + onClick: () => onExport(true), + }, + ]; - - - + // render + if (sidebarStateLoading) return null; - {totalCount > filter.itemsPerPage && ( -
        - + ); + + return ( + +
        + {modal} + + + + setShowSidebar(false)}> + setShowSidebar(false)} + count={cachedResult.loading ? undefined : totalCount} + focus={searchFocus} /> -
        - )} -
        - -
        - - ); -}; + + setShowSidebar(!showSidebar)} + > + + + + showEditFilter(c.criterionOption.type) + } + onRemoveCriterion={removeCriterion} + onRemoveAll={clearAllCriteria} + /> + +
        + setFilter(filter.changePage(page))} + /> + +
        + + + + + + {totalCount > filter.itemsPerPage && ( +
        +
        + +
        +
        + )} +
        +
        + +
        +
        + ); + } +); export default FilteredSceneList; diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx index 644decf42..96961d68b 100644 --- a/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import { useMemo } from "react"; import { Button, ButtonGroup } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; import { Icon } from "../Shared/Icon"; @@ -6,12 +6,13 @@ import { TagLink } from "../Shared/TagLink"; import { HoverPopover } from "../Shared/HoverPopover"; import NavUtils from "src/utils/navigation"; import TextUtils from "src/utils/text"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { GridCard } from "../Shared/GridCard/GridCard"; import { faTag } from "@fortawesome/free-solid-svg-icons"; import { markerTitle } from "src/core/markers"; import { Link } from "react-router-dom"; import { objectTitle } from "src/core/files"; +import { PatchComponent } from "src/patch"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; import { ScenePreview } from "./SceneCard"; import { TruncatedText } from "../Shared/TruncatedText"; @@ -28,154 +29,166 @@ interface ISceneMarkerCardProps { onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void; } -const SceneMarkerCardPopovers = (props: ISceneMarkerCardProps) => { - function maybeRenderPerformerPopoverButton() { - if (props.marker.scene.performers.length <= 0) return; +const SceneMarkerCardPopovers = PatchComponent( + "SceneMarkerCard.Popovers", + (props: ISceneMarkerCardProps) => { + function maybeRenderPerformerPopoverButton() { + if (props.marker.scene.performers.length <= 0) return; - return ( - - ); - } - - function renderTagPopoverButton() { - const popoverContent = [ - , - ]; - - props.marker.tags.map((tag) => - popoverContent.push( - - ) - ); - - return ( - - - - ); - } - - function renderPopoverButtonGroup() { - if (!props.compact) { return ( - <> -
        - - {maybeRenderPerformerPopoverButton()} - {renderTagPopoverButton()} - - + ); } + + function renderTagPopoverButton() { + const popoverContent = [ + , + ]; + + props.marker.tags.map((tag) => + popoverContent.push( + + ) + ); + + return ( + + + + ); + } + + function renderPopoverButtonGroup() { + if (!props.compact) { + return ( + <> +
        + + {maybeRenderPerformerPopoverButton()} + {renderTagPopoverButton()} + + + ); + } + } + + return <>{renderPopoverButtonGroup()}; } +); - return <>{renderPopoverButtonGroup()}; -}; - -const SceneMarkerCardDetails = (props: ISceneMarkerCardProps) => { - return ( -
        - - {TextUtils.formatTimestampRange( - props.marker.seconds, - props.marker.end_seconds ?? undefined - )} - - - {objectTitle(props.marker.scene)} - - } - /> -
        - ); -}; - -const SceneMarkerCardImage = (props: ISceneMarkerCardProps) => { - const { configuration } = React.useContext(ConfigurationContext); - - const file = useMemo( - () => - props.marker.scene.files.length > 0 - ? props.marker.scene.files[0] - : undefined, - [props.marker.scene] - ); - - function isPortrait() { - const width = file?.width ? file.width : 0; - const height = file?.height ? file.height : 0; - return height > width; - } - - function maybeRenderSceneSpecsOverlay() { +const SceneMarkerCardDetails = PatchComponent( + "SceneMarkerCard.Details", + (props: ISceneMarkerCardProps) => { return ( -
        - {props.marker.end_seconds && ( - - {TextUtils.secondsToTimestamp( - props.marker.end_seconds - props.marker.seconds - )} - - )} +
        + + {TextUtils.formatTimestampRange( + props.marker.seconds, + props.marker.end_seconds ?? undefined + )} + + + {objectTitle(props.marker.scene)} + + } + />
        ); } +); - return ( - <> - - {maybeRenderSceneSpecsOverlay()} - - ); -}; +const SceneMarkerCardImage = PatchComponent( + "SceneMarkerCard.Image", + (props: ISceneMarkerCardProps) => { + const { configuration } = useConfigurationContext(); -export const SceneMarkerCard = (props: ISceneMarkerCardProps) => { - function zoomIndex() { - if (!props.compact && props.zoomIndex !== undefined) { - return `zoom-${props.zoomIndex}`; + const file = useMemo( + () => + props.marker.scene.files.length > 0 + ? props.marker.scene.files[0] + : undefined, + [props.marker.scene] + ); + + function isPortrait() { + const width = file?.width ? file.width : 0; + const height = file?.height ? file.height : 0; + return height > width; } - return ""; - } + function maybeRenderSceneSpecsOverlay() { + return ( +
        + {props.marker.end_seconds && ( + + {TextUtils.secondsToTimestamp( + props.marker.end_seconds - props.marker.seconds + )} + + )} +
        + ); + } - return ( - } - details={} - popovers={} - selected={props.selected} - selecting={props.selecting} - onSelectedChanged={props.onSelectedChanged} - /> - ); -}; + return ( + <> + + {maybeRenderSceneSpecsOverlay()} + + ); + } +); + +export const SceneMarkerCard = PatchComponent( + "SceneMarkerCard", + (props: ISceneMarkerCardProps) => { + function zoomIndex() { + if (!props.compact && props.zoomIndex !== undefined) { + return `zoom-${props.zoomIndex}`; + } + + return ""; + } + + return ( + } + details={} + popovers={} + selected={props.selected} + selecting={props.selecting} + onSelectedChanged={props.onSelectedChanged} + /> + ); + } +); diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerCardGrid.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerCardGrid.tsx new file mode 100644 index 000000000..ad869918b --- /dev/null +++ b/ui/v2.5/src/components/Scenes/SceneMarkerCardGrid.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import * as GQL from "src/core/generated-graphql"; +import { SceneMarkerCard } from "./SceneMarkerCard"; +import { + useCardWidth, + useContainerDimensions, +} from "../Shared/GridCard/GridCard"; +import { PatchComponent } from "src/patch"; + +interface ISceneMarkerCardGrid { + markers: GQL.SceneMarkerDataFragment[]; + selectedIds: Set; + zoomIndex: number; + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; +} + +const zoomWidths = [240, 340, 480, 640]; + +export const SceneMarkerCardGrid: React.FC = + PatchComponent( + "SceneMarkerCardGrid", + ({ markers, selectedIds, zoomIndex, onSelectChange }) => { + const [componentRef, { width: containerWidth }] = + useContainerDimensions(); + const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); + + return ( +
        + {markers.map((marker, index) => ( + 0} + selected={selectedIds.has(marker.id)} + onSelectedChanged={(selected: boolean, shiftKey: boolean) => + onSelectChange(marker.id, selected, shiftKey) + } + /> + ))} +
        + ); + } + ); diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerCardsGrid.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerCardsGrid.tsx deleted file mode 100644 index 9f01fe6da..000000000 --- a/ui/v2.5/src/components/Scenes/SceneMarkerCardsGrid.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from "react"; -import * as GQL from "src/core/generated-graphql"; -import { SceneMarkerCard } from "./SceneMarkerCard"; -import { - useCardWidth, - useContainerDimensions, -} from "../Shared/GridCard/GridCard"; - -interface ISceneMarkerCardsGrid { - markers: GQL.SceneMarkerDataFragment[]; - selectedIds: Set; - zoomIndex: number; - onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; -} - -const zoomWidths = [240, 340, 480, 640]; - -export const SceneMarkerCardsGrid: React.FC = ({ - markers, - selectedIds, - zoomIndex, - onSelectChange, -}) => { - const [componentRef, { width: containerWidth }] = useContainerDimensions(); - const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); - - return ( -
        - {markers.map((marker, index) => ( - 0} - selected={selectedIds.has(marker.id)} - onSelectedChanged={(selected: boolean, shiftKey: boolean) => - onSelectChange(marker.id, selected, shiftKey) - } - /> - ))} -
        - ); -}; diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx index 94eb6e133..b5975ca5a 100644 --- a/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx @@ -14,8 +14,11 @@ import { ListFilterModel } from "src/models/list-filter/filter"; import { DisplayMode } from "src/models/list-filter/types"; import { MarkerWallPanel } from "./SceneMarkerWallPanel"; import { View } from "../List/views"; -import { SceneMarkerCardsGrid } from "./SceneMarkerCardsGrid"; +import { SceneMarkerCardGrid } from "./SceneMarkerCardGrid"; import { DeleteSceneMarkersDialog } from "./DeleteSceneMarkersDialog"; +import { EditSceneMarkersDialog } from "./EditSceneMarkersDialog"; +import { PatchComponent } from "src/patch"; +import { IItemListOperation } from "../List/FilteredListToolbar"; function getItems(result: GQL.FindSceneMarkersQueryResult) { return result?.data?.findSceneMarkers?.scene_markers ?? []; @@ -30,123 +33,135 @@ interface ISceneMarkerList { view?: View; alterQuery?: boolean; defaultSort?: string; + extraOperations?: IItemListOperation[]; } -export const SceneMarkerList: React.FC = ({ - filterHook, - view, - alterQuery, -}) => { - const intl = useIntl(); - const history = useHistory(); +export const SceneMarkerList: React.FC = PatchComponent( + "SceneMarkerList", + ({ filterHook, view, alterQuery, extraOperations = [] }) => { + const intl = useIntl(); + const history = useHistory(); - const filterMode = GQL.FilterMode.SceneMarkers; + const filterMode = GQL.FilterMode.SceneMarkers; - const otherOperations = [ - { - text: intl.formatMessage({ id: "actions.play_random" }), - onClick: playRandom, - }, - ]; + const otherOperations = [ + ...extraOperations, + { + text: intl.formatMessage({ id: "actions.play_random" }), + onClick: playRandom, + }, + ]; - function addKeybinds( - result: GQL.FindSceneMarkersQueryResult, - filter: ListFilterModel - ) { - Mousetrap.bind("p r", () => { - playRandom(result, filter); - }); + function addKeybinds( + result: GQL.FindSceneMarkersQueryResult, + filter: ListFilterModel + ) { + Mousetrap.bind("p r", () => { + playRandom(result, filter); + }); - return () => { - Mousetrap.unbind("p r"); - }; - } + return () => { + Mousetrap.unbind("p r"); + }; + } - async function playRandom( - result: GQL.FindSceneMarkersQueryResult, - filter: ListFilterModel - ) { - // query for a random scene - if (result.data?.findSceneMarkers) { - const { count } = result.data.findSceneMarkers; + async function playRandom( + result: GQL.FindSceneMarkersQueryResult, + filter: ListFilterModel + ) { + // query for a random scene + if (result.data?.findSceneMarkers) { + const { count } = result.data.findSceneMarkers; - const index = Math.floor(Math.random() * count); - const filterCopy = cloneDeep(filter); - filterCopy.itemsPerPage = 1; - filterCopy.currentPage = index + 1; - const singleResult = await queryFindSceneMarkers(filterCopy); - if (singleResult.data.findSceneMarkers.scene_markers.length === 1) { - // navigate to the scene player page - const url = NavUtils.makeSceneMarkerUrl( - singleResult.data.findSceneMarkers.scene_markers[0] - ); - history.push(url); + const index = Math.floor(Math.random() * count); + const filterCopy = cloneDeep(filter); + filterCopy.itemsPerPage = 1; + filterCopy.currentPage = index + 1; + const singleResult = await queryFindSceneMarkers(filterCopy); + if (singleResult.data.findSceneMarkers.scene_markers.length === 1) { + // navigate to the scene player page + const url = NavUtils.makeSceneMarkerUrl( + singleResult.data.findSceneMarkers.scene_markers[0] + ); + history.push(url); + } } } - } - function renderContent( - result: GQL.FindSceneMarkersQueryResult, - filter: ListFilterModel, - selectedIds: Set, - onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void - ) { - if (!result.data?.findSceneMarkers) return; + function renderContent( + result: GQL.FindSceneMarkersQueryResult, + filter: ListFilterModel, + selectedIds: Set, + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void + ) { + if (!result.data?.findSceneMarkers) return; - if (filter.displayMode === DisplayMode.Wall) { + if (filter.displayMode === DisplayMode.Wall) { + return ( + + ); + } + + if (filter.displayMode === DisplayMode.Grid) { + return ( + + ); + } + } + + function renderEditDialog( + selectedMarkers: GQL.SceneMarkerDataFragment[], + onClose: (applied: boolean) => void + ) { return ( - + ); + } + + function renderDeleteDialog( + selectedSceneMarkers: GQL.SceneMarkerDataFragment[], + onClose: (confirmed: boolean) => void + ) { + return ( + ); } - if (filter.displayMode === DisplayMode.Grid) { - return ( - - ); - } - } - - function renderDeleteDialog( - selectedSceneMarkers: GQL.SceneMarkerDataFragment[], - onClose: (confirmed: boolean) => void - ) { return ( - + + + ); } - - return ( - - - - ); -}; +); export default SceneMarkerList; diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerRecommendationRow.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerRecommendationRow.tsx new file mode 100644 index 000000000..5c9769206 --- /dev/null +++ b/ui/v2.5/src/components/Scenes/SceneMarkerRecommendationRow.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { useFindSceneMarkers } from "src/core/StashService"; +import Slider from "@ant-design/react-slick"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { getSlickSliderSettings } from "src/core/recommendations"; +import { RecommendationRow } from "../FrontPage/RecommendationRow"; +import { FormattedMessage } from "react-intl"; +import { SceneMarkerCard } from "./SceneMarkerCard"; +import { PatchComponent } from "src/patch"; + +interface IProps { + isTouch: boolean; + filter: ListFilterModel; + header: string; +} + +export const SceneMarkerRecommendationRow: React.FC = PatchComponent( + "SceneMarkerRecommendationRow", + (props) => { + const result = useFindSceneMarkers(props.filter); + const cardCount = result.data?.findSceneMarkers.count; + + if (!result.loading && !cardCount) { + return null; + } + + return ( + + + + } + > + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
        + )) + : result.data?.findSceneMarkers.scene_markers.map( + (marker, index) => ( + + ) + )} +
        +
        + ); + } +); diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerWallPanel.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerWallPanel.tsx index 5202b94d1..863078c4e 100644 --- a/ui/v2.5/src/components/Scenes/SceneMarkerWallPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMarkerWallPanel.tsx @@ -1,21 +1,17 @@ -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Form } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; import Gallery, { GalleryI, PhotoProps, RenderImageProps, } from "react-photo-gallery"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { objectTitle } from "src/core/files"; import { Link, useHistory } from "react-router-dom"; import { TruncatedText } from "../Shared/TruncatedText"; import TextUtils from "src/utils/text"; +import { useDragMoveSelect } from "../Shared/GridCard/dragMoveSelect"; import cx from "classnames"; import NavUtils from "src/utils/navigation"; import { markerTitle } from "src/core/markers"; @@ -39,15 +35,32 @@ interface IMarkerPhoto { onError?: (photo: PhotoProps) => void; } -export const MarkerWallItem: React.FC> = ( - props: RenderImageProps -) => { - const { configuration } = useContext(ConfigurationContext); +interface IExtraProps { + maxHeight: number; + selected?: boolean; + onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void; + selecting?: boolean; +} + +export const MarkerWallItem: React.FC< + RenderImageProps & IExtraProps +> = (props: RenderImageProps & IExtraProps) => { + const { dragProps } = useDragMoveSelect({ + selecting: props.selecting || false, + selected: props.selected || false, + onSelectedChanged: props.onSelectedChanged, + }); + + const { configuration } = useConfigurationContext(); const playSound = configuration?.interface.soundOnPreview ?? false; const showTitle = configuration?.interface.wallShowTitle ?? false; const [active, setActive] = useState(false); + const height = Math.min(props.maxHeight, props.photo.height); + const zoomFactor = height / props.photo.height; + const width = props.photo.width * zoomFactor; + type style = Record; var divStyle: style = { margin: props.margin, @@ -61,6 +74,12 @@ export const MarkerWallItem: React.FC> = ( } var handleClick = function handleClick(event: React.MouseEvent) { + if (props.selecting && props.onSelectedChanged) { + props.onSelectedChanged(!props.selected, event.shiftKey); + event.preventDefault(); + event.stopPropagation(); + return; + } if (props.onClick) { props.onClick(event, { index: props.index }); } @@ -73,25 +92,42 @@ export const MarkerWallItem: React.FC> = ( const title = wallItemTitle(marker); const tagNames = marker.tags.map((p) => p.name); + let shiftKey = false; + return (
        + {props.onSelectedChanged && ( + props.onSelectedChanged!(!props.selected, shiftKey)} + onClick={(event: React.MouseEvent) => { + shiftKey = event.shiftKey; + event.stopPropagation(); + }} + /> + )} setActive(true)} onMouseLeave={() => setActive(false)} @@ -121,6 +157,9 @@ export const MarkerWallItem: React.FC> = ( interface IMarkerWallProps { markers: GQL.SceneMarkerDataFragment[]; zoomIndex: number; + selectedIds?: Set; + onSelectChange?: (id: string, selected: boolean, shiftKey: boolean) => void; + selecting?: boolean; } // HACK: typescript doesn't allow Gallery to accept a parameter for some reason @@ -160,9 +199,17 @@ const breakpointZoomHeights = [ { minWidth: 1400, heights: [160, 240, 300, 480] }, ]; -const MarkerWall: React.FC = ({ markers, zoomIndex }) => { +const MarkerWall: React.FC = ({ + markers, + zoomIndex, + selectedIds, + onSelectChange, + selecting, +}) => { const history = useHistory(); + const containerRef = React.useRef(null); + const margin = 3; const direction = "row"; @@ -208,22 +255,50 @@ const MarkerWall: React.FC = ({ markers, zoomIndex }) => { return Math.round(columnCount); } - function targetRowHeight(containerWidth: number) { - let zoomHeight = 280; - breakpointZoomHeights.forEach((e) => { - if (containerWidth >= e.minWidth) { - zoomHeight = e.heights[zoomIndex]; - } - }); - return zoomHeight; - } + const targetRowHeight = useCallback( + (containerWidth: number) => { + let zoomHeight = 280; + breakpointZoomHeights.forEach((e) => { + if (containerWidth >= e.minWidth) { + zoomHeight = e.heights[zoomIndex]; + } + }); + return zoomHeight; + }, + [zoomIndex] + ); - const renderImage = useCallback((props: RenderImageProps) => { - return ; - }, []); + // set the max height as a factor of the targetRowHeight + // this allows some images to be taller than the target row height + // but prevents images from becoming too tall when there is a small number of items + const maxHeightFactor = 1.3; + + const renderImage = useCallback( + (props: RenderImageProps) => { + const markerId = props.photo.marker.id; + return ( + + onSelectChange(markerId, selected, shiftKey) + : undefined + } + selecting={selecting} + /> + ); + }, + [targetRowHeight, selectedIds, onSelectChange, selecting] + ); return ( -
        +
        {photos.length ? ( = ({ markers, zoomIndex }) => { interface IMarkerWallPanelProps { markers: GQL.SceneMarkerDataFragment[]; zoomIndex: number; + selectedIds?: Set; + onSelectChange?: (id: string, selected: boolean, shiftKey: boolean) => void; } export const MarkerWallPanel: React.FC = ({ markers, zoomIndex, + selectedIds, + onSelectChange, }) => { - return ; + const selecting = !!selectedIds && selectedIds.size > 0; + return ( + + ); }; diff --git a/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx b/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx index 52b3ea67c..9455af186 100644 --- a/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx @@ -12,13 +12,13 @@ import { FormattedMessage, useIntl } from "react-intl"; import { useToast } from "src/hooks/Toast"; import { faExchangeAlt, faSignInAlt } from "@fortawesome/free-solid-svg-icons"; import { - ScrapeDialog, ScrapeDialogRow, ScrapedImageRow, ScrapedInputGroupRow, ScrapedStringListRow, ScrapedTextAreaRow, -} from "../Shared/ScrapeDialog/ScrapeDialog"; +} from "../Shared/ScrapeDialog/ScrapeDialogRow"; +import { ScrapeDialog } from "../Shared/ScrapeDialog/ScrapeDialog"; import { clone, uniq } from "lodash-es"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { ModalComponent } from "../Shared/Modal"; @@ -206,13 +206,7 @@ const SceneMergeDetails: React.FC = ({ setCode( new ScrapeResult(dest.code, sources.find((s) => s.code)?.code, !dest.code) ); - setURL( - new ScrapeResult( - dest.urls, - sources.find((s) => s.urls)?.urls, - !dest.urls?.length - ) - ); + setURL(new ScrapeResult(dest.urls, uniq(all.map((s) => s.urls).flat()))); setDate( new ScrapeResult(dest.date, sources.find((s) => s.date)?.date, !dest.date) ); @@ -311,8 +305,7 @@ const SceneMergeDetails: React.FC = ({ .filter((s, index, a) => { // remove entries with duplicate endpoints return index === a.findIndex((ss) => ss.endpoint === s.endpoint); - }), - !dest.stash_ids.length + }) ) ); @@ -379,83 +372,87 @@ const SceneMergeDetails: React.FC = ({ return ( <> setTitle(value)} /> setCode(value)} /> setURL(value)} /> setDate(value)} /> ( - - )} - renderNewField={() => ( - - )} + originalField={} + newField={} onChange={(value) => setRating(value)} /> ( + originalField={ {}} className="bg-secondary text-white border-secondary" /> - )} - renderNewField={() => ( + } + newField={ {}} className="bg-secondary text-white border-secondary" /> - )} + } onChange={(value) => setOCounter(value)} /> ( + originalField={ {}} className="bg-secondary text-white border-secondary" /> - )} - renderNewField={() => ( + } + newField={ {}} className="bg-secondary text-white border-secondary" /> - )} + } onChange={(value) => setPlayCount(value)} /> ( + originalField={ = ({ onChange={() => {}} className="bg-secondary text-white border-secondary" /> - )} - renderNewField={() => ( + } + newField={ {}} className="bg-secondary text-white border-secondary" /> - )} + } onChange={(value) => setPlayDuration(value)} /> ( + originalField={ = ({ isMulti isDisabled /> - )} - renderNewField={() => ( + } + newField={ = ({ isMulti isDisabled /> - )} + } onChange={(value) => setGalleries(value)} /> setStudio(value)} /> setPerformers(value)} ageFromDate={date.useNewValue ? date.newValue : date.originalValue} /> setGroups(value)} /> setTags(value)} /> setDetails(value)} /> ( + originalField={ {}} className="bg-secondary text-white border-secondary" /> - )} - renderNewField={() => ( + } + newField={ {}} className="bg-secondary text-white border-secondary" /> - )} + } onChange={(value) => setOrganized(value)} /> ( + originalField={ - )} - renderNewField={() => ( - - )} + } + newField={} onChange={(value) => setStashIDs(value)} /> = ({ title={dialogTitle} existingLabel={destinationLabel} scrapedLabel={sourceLabel} - renderScrapeRows={renderScrapeRows} onClose={(apply) => { if (!apply) { onClose(); @@ -632,7 +635,9 @@ const SceneMergeDetails: React.FC = ({ onClose(createValues()); } }} - /> + > + {renderScrapeRows()} + ); }; @@ -700,8 +705,6 @@ export const SceneMergeModal: React.FC = ({ ); if (result.data?.sceneMerge) { Toast.success(intl.formatMessage({ id: "toast.merged_scenes" })); - // refetch the scene - await queryFindScenesByID([parseInt(destScene[0].id)]); onClose(destScene[0].id); } onClose(); @@ -730,6 +733,7 @@ export const SceneMergeModal: React.FC = ({ sources={loadedSources} dest={loadedDest!} onClose={(values) => { + setSecondStep(false); if (values) { onMerge(values); } else { diff --git a/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx b/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx index d33762761..f90b63ec6 100644 --- a/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx +++ b/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx @@ -8,6 +8,7 @@ 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 { PatchComponent } from "src/patch"; interface IProps { isTouch: boolean; @@ -15,48 +16,54 @@ interface IProps { header: string; } -export const SceneRecommendationRow: React.FC = (props) => { - const result = useFindScenes(props.filter); - const cardCount = result.data?.findScenes.count; +export const SceneRecommendationRow: React.FC = PatchComponent( + "SceneRecommendationRow", + (props) => { + const result = useFindScenes(props.filter); + const cardCount = result.data?.findScenes.count; - const queue = useMemo(() => { - return SceneQueue.fromListFilterModel(props.filter); - }, [props.filter]); + const queue = useMemo(() => { + return SceneQueue.fromListFilterModel(props.filter); + }, [props.filter]); - if (!result.loading && !cardCount) { - return null; - } + if (!result.loading && !cardCount) { + return null; + } - return ( - - - - } - > - + + + } > - {result.loading - ? [...Array(props.filter.itemsPerPage)].map((i) => ( -
        - )) - : 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/SceneSelect.tsx b/ui/v2.5/src/components/Scenes/SceneSelect.tsx index 7871bc43e..8ab32b753 100644 --- a/ui/v2.5/src/components/Scenes/SceneSelect.tsx +++ b/ui/v2.5/src/components/Scenes/SceneSelect.tsx @@ -12,7 +12,7 @@ import { queryFindScenesForSelect, queryFindScenesByIDForSelect, } from "src/core/StashService"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { useIntl } from "react-intl"; import { defaultMaxOptionsShown } from "src/core/config"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -66,7 +66,7 @@ const sceneSelectSort = PatchFunction( const _SceneSelect: React.FC< IFilterProps & IFilterValueProps & ExtraSceneProps > = (props) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const intl = useIntl(); const maxOptionsShown = configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; diff --git a/ui/v2.5/src/components/Scenes/SceneWallPanel.tsx b/ui/v2.5/src/components/Scenes/SceneWallPanel.tsx index dcc0bf734..bf4a97b49 100644 --- a/ui/v2.5/src/components/Scenes/SceneWallPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneWallPanel.tsx @@ -1,10 +1,5 @@ -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Form } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; import { SceneQueue } from "src/models/sceneQueue"; import Gallery, { @@ -12,12 +7,13 @@ import Gallery, { PhotoProps, RenderImageProps, } from "react-photo-gallery"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { objectTitle } from "src/core/files"; import { Link, useHistory } from "react-router-dom"; import { TruncatedText } from "../Shared/TruncatedText"; import TextUtils from "src/utils/text"; import { useIntl } from "react-intl"; +import { useDragMoveSelect } from "../Shared/GridCard/dragMoveSelect"; import cx from "classnames"; interface IScenePhoto { @@ -26,15 +22,32 @@ interface IScenePhoto { onError?: (photo: PhotoProps) => void; } -export const SceneWallItem: React.FC> = ( - props: RenderImageProps -) => { +interface IExtraProps { + maxHeight: number; + selected?: boolean; + onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void; + selecting?: boolean; +} + +export const SceneWallItem: React.FC< + RenderImageProps & IExtraProps +> = (props: RenderImageProps & IExtraProps) => { const intl = useIntl(); - const { configuration } = useContext(ConfigurationContext); + const { dragProps } = useDragMoveSelect({ + selecting: props.selecting || false, + selected: props.selected || false, + onSelectedChanged: props.onSelectedChanged, + }); + + const { configuration } = useConfigurationContext(); const playSound = configuration?.interface.soundOnPreview ?? false; const showTitle = configuration?.interface.wallShowTitle ?? false; + const height = Math.min(props.maxHeight, props.photo.height); + const zoomFactor = height / props.photo.height; + const width = props.photo.width * zoomFactor; + const [active, setActive] = useState(false); type style = Record; @@ -50,6 +63,12 @@ export const SceneWallItem: React.FC> = ( } var handleClick = function handleClick(event: React.MouseEvent) { + if (props.selecting && props.onSelectedChanged) { + props.onSelectedChanged(!props.selected, event.shiftKey); + event.preventDefault(); + event.stopPropagation(); + return; + } if (props.onClick) { props.onClick(event, { index: props.index }); } @@ -66,25 +85,42 @@ export const SceneWallItem: React.FC> = ( ? [...performerNames.slice(0, -2), performerNames.slice(-2).join(" & ")] : performerNames; + let shiftKey = false; + return (
        + {props.onSelectedChanged && ( + props.onSelectedChanged!(!props.selected, shiftKey)} + onClick={(event: React.MouseEvent) => { + shiftKey = event.shiftKey; + event.stopPropagation(); + }} + /> + )} setActive(true)} onMouseLeave={() => setActive(false)} @@ -104,7 +140,9 @@ export const SceneWallItem: React.FC> = ( /> )} -
        {scene.date && TextUtils.formatDate(intl, scene.date)}
        +
        + {scene.date && TextUtils.formatFuzzyDate(intl, scene.date)} +
        @@ -127,6 +165,9 @@ interface ISceneWallProps { scenes: GQL.SlimSceneDataFragment[]; sceneQueue?: SceneQueue; zoomIndex: number; + selectedIds?: Set; + onSelectChange?: (id: string, selected: boolean, shiftKey: boolean) => void; + selecting?: boolean; } // HACK: typescript doesn't allow Gallery to accept a parameter for some reason @@ -143,9 +184,14 @@ const SceneWall: React.FC = ({ scenes, sceneQueue, zoomIndex, + selectedIds, + onSelectChange, + selecting, }) => { const history = useHistory(); + const containerRef = React.useRef(null); + const margin = 3; const direction = "row"; @@ -196,22 +242,50 @@ const SceneWall: React.FC = ({ return Math.round(columnCount); } - function targetRowHeight(containerWidth: number) { - let zoomHeight = 280; - breakpointZoomHeights.forEach((e) => { - if (containerWidth >= e.minWidth) { - zoomHeight = e.heights[zoomIndex]; - } - }); - return zoomHeight; - } + const targetRowHeight = useCallback( + (containerWidth: number) => { + let zoomHeight = 280; + breakpointZoomHeights.forEach((e) => { + if (containerWidth >= e.minWidth) { + zoomHeight = e.heights[zoomIndex]; + } + }); + return zoomHeight; + }, + [zoomIndex] + ); - const renderImage = useCallback((props: RenderImageProps) => { - return ; - }, []); + // set the max height as a factor of the targetRowHeight + // this allows some images to be taller than the target row height + // but prevents images from becoming too tall when there is a small number of items + const maxHeightFactor = 1.3; + + const renderImage = useCallback( + (props: RenderImageProps) => { + const sceneId = props.photo.scene.id; + return ( + + onSelectChange(sceneId, selected, shiftKey) + : undefined + } + selecting={selecting} + /> + ); + }, + [targetRowHeight, selectedIds, onSelectChange, selecting] + ); return ( -
        +
        {photos.length ? ( ; + onSelectChange?: (id: string, selected: boolean, shiftKey: boolean) => void; } export const SceneWallPanel: React.FC = ({ scenes, sceneQueue, zoomIndex, + selectedIds, + onSelectChange, }) => { + const selecting = !!selectedIds && selectedIds.size > 0; return ( - + ); }; diff --git a/ui/v2.5/src/components/Scenes/styles.scss b/ui/v2.5/src/components/Scenes/styles.scss index 279552575..78644b4c9 100644 --- a/ui/v2.5/src/components/Scenes/styles.scss +++ b/ui/v2.5/src/components/Scenes/styles.scss @@ -558,6 +558,10 @@ input[type="range"].blue-slider { top: 3rem; } } + + .form-group[data-field="urls"] .string-list-input input.form-control { + font-size: 0.85em; + } } .scene-markers-panel { @@ -902,51 +906,6 @@ input[type="range"].blue-slider { } } -.scene-list .filtered-list-toolbar { - display: flex; - flex-wrap: wrap; - row-gap: 1rem; - - & > div { - display: flex; - flex: 1; - flex-wrap: nowrap; - - &:first-child { - justify-content: flex-start; - } - - &:nth-child(2) { - justify-content: center; - } - - &:last-child { - justify-content: flex-end; - } - - @include media-breakpoint-down(xs) { - &:nth-child(2) { - justify-content: flex-end; - } - - &:last-child { - display: none; - } - } - } -} - -.scene-list.hide-sidebar .sidebar-toggle-button { - transition-delay: 0.1s; - transition-duration: 0; - transition-property: opacity; -} - -.scene-list:not(.hide-sidebar) .sidebar-toggle-button { - opacity: 0; - pointer-events: none; -} - .scene-wall, .marker-wall { .wall-item { @@ -1004,56 +963,13 @@ input[type="range"].blue-slider { } } -.scene-list-toolbar, -.scene-list-header { - align-items: center; - background-color: $body-bg; - display: flex; - justify-content: space-between; - - > div { - align-items: center; - display: flex; - gap: 0.5rem; - justify-content: flex-start; - - &:last-child { - flex-shrink: 0; - justify-content: flex-end; - } - } +.table-list.scene-table { + // Set max height to viewport height minus estimated header/footer height + // TODO - this will need to be rolled out to other tables + max-height: calc(100dvh - 210px); } -.scene-list-toolbar { - flex-wrap: nowrap; - gap: 1rem; - // offset the main padding - margin-top: -0.5rem; - padding-bottom: 0.5rem; - padding-top: 0.5rem; - position: sticky; - top: $navbar-height; - z-index: 10; - - @include media-breakpoint-down(xs) { - top: 0; - } - - .selected-items-info .btn { - margin-right: 0.5rem; - } - - // hide drop down menu items for play and create new - // when the buttons are visible - @include media-breakpoint-up(sm) { - .scene-list-operations { - .play-item, - .create-new-item { - display: none; - } - } - } - +.scene-list .filtered-list-toolbar { // hide play and create new buttons on xs screens // show these in the drop down menu instead @include media-breakpoint-down(xs) { @@ -1062,68 +978,15 @@ input[type="range"].blue-slider { display: none; } } +} - > div:first-child { - border: 1px solid $secondary; - border-radius: 0.25rem; - flex-grow: 1; - overflow-x: hidden; - - .filter-button { - border-bottom-right-radius: 0; - border-top-right-radius: 0; - } - } - - .filter-tags { - flex-grow: 1; - flex-wrap: nowrap; - justify-content: flex-start; - margin-bottom: 0; - width: calc(100% - 35px - 0.5rem); - - @include media-breakpoint-down(xs) { - overflow-x: auto; - scrollbar-width: thin; - } - - .tag-item { - white-space: nowrap; +// hide drop down menu items for play and create new +// when the buttons are visible +@include media-breakpoint-up(sm) { + .scene-list-operations-dropdown { + .dropdown-item.play-item, + .dropdown-item.create-new-item { + display: none; } } } - -.scene-list-header { - flex-wrap: wrap-reverse; - gap: 0.5rem; - margin-bottom: 0.5rem; - - .paginationIndex { - margin: 0; - } - - // center the header on smaller screens - @include media-breakpoint-down(sm) { - & > div, - & > div:last-child { - flex-basis: 100%; - justify-content: center; - margin-left: auto; - margin-right: auto; - } - } -} - -.detail-body .scene-list-toolbar { - top: calc($sticky-detail-header-height + $navbar-height); - - @include media-breakpoint-down(xs) { - top: 0; - } -} - -#more-criteria-popover { - box-shadow: 0 8px 10px 2px rgb(0 0 0 / 30%); - max-width: 400px; - padding: 0.25rem; -} diff --git a/ui/v2.5/src/components/Settings/PluginPackageManager.tsx b/ui/v2.5/src/components/Settings/PluginPackageManager.tsx index ed3160139..d71bea5ee 100644 --- a/ui/v2.5/src/components/Settings/PluginPackageManager.tsx +++ b/ui/v2.5/src/components/Settings/PluginPackageManager.tsx @@ -100,6 +100,12 @@ export const AvailablePluginPackages: React.FC = () => { const [jobID, setJobID] = useState(); const { job } = useMonitorJob(jobID, () => onPackageChanges()); + // Get installed packages to filter them out from available list + const { data: installedData } = useInstalledPluginPackages(false); + const installedPackageIds = new Set( + installedData?.installedPackages?.map((p) => p.package_id) ?? [] + ); + async function onInstallPackages(packages: GQL.PackageSpecInput[]) { const r = await mutateInstallPluginPackages(packages); @@ -114,7 +120,10 @@ export const AvailablePluginPackages: React.FC = () => { async function loadSource(source: string): Promise { const { data } = await queryAvailablePluginPackages(source); - return data.availablePackages; + // Filter out already installed packages + return data.availablePackages.filter( + (pkg) => !installedPackageIds.has(pkg.package_id) + ); } function addSource(source: GQL.PackageSource) { diff --git a/ui/v2.5/src/components/Settings/ScraperPackageManager.tsx b/ui/v2.5/src/components/Settings/ScraperPackageManager.tsx index cb6858610..5c93bc2a3 100644 --- a/ui/v2.5/src/components/Settings/ScraperPackageManager.tsx +++ b/ui/v2.5/src/components/Settings/ScraperPackageManager.tsx @@ -100,6 +100,12 @@ export const AvailableScraperPackages: React.FC = () => { const [jobID, setJobID] = useState(); const { job } = useMonitorJob(jobID, () => onPackageChanges()); + // Get installed packages to filter them out from available list + const { data: installedData } = useInstalledScraperPackages(false); + const installedPackageIds = new Set( + installedData?.installedPackages?.map((p) => p.package_id) ?? [] + ); + async function onInstallPackages(packages: GQL.PackageSpecInput[]) { const r = await mutateInstallScraperPackages(packages); @@ -114,7 +120,10 @@ export const AvailableScraperPackages: React.FC = () => { async function loadSource(source: string): Promise { const { data } = await queryAvailableScraperPackages(source); - return data.availablePackages; + // Filter out already installed packages + return data.availablePackages.filter( + (pkg) => !installedPackageIds.has(pkg.package_id) + ); } function addSource(source: GQL.PackageSource) { diff --git a/ui/v2.5/src/components/Settings/Settings.tsx b/ui/v2.5/src/components/Settings/Settings.tsx index 4c2b02455..86a781445 100644 --- a/ui/v2.5/src/components/Settings/Settings.tsx +++ b/ui/v2.5/src/components/Settings/Settings.tsx @@ -18,6 +18,8 @@ import { SettingsContext, useSettings } from "./context"; import { SettingsLibraryPanel } from "./SettingsLibraryPanel"; import { SettingsSecurityPanel } from "./SettingsSecurityPanel"; import Changelog from "../Changelog/Changelog"; +import { TroubleshootingModeButton } from "../TroubleshootingMode/TroubleshootingModeButton"; +import { useTroubleshootingMode } from "../TroubleshootingMode/useTroubleshootingMode"; const validTabs = [ "tasks", @@ -43,6 +45,7 @@ function isTabKey(tab: string | null): tab is TabKey { const SettingTabs: React.FC<{ tab: TabKey }> = ({ tab }) => { const { advancedMode, setAdvancedMode } = useSettings(); + const { isActive: troubleshootingModeActive } = useTroubleshootingMode(); const titleProps = useTitleProps({ id: "settings" }); @@ -148,6 +151,7 @@ const SettingTabs: React.FC<{ tab: TabKey }> = ({ tab }) => { />
        + {!troubleshootingModeActive && }
        diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index 1802fefe7..0ebe3f736 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -200,6 +200,7 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( onChange={(v) => saveInterface({ language: v })} > + @@ -219,7 +220,8 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( - + + @@ -232,11 +234,28 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( + + saveInterface({ sfwContentMode: v })} + /> + + saveUI({ title: v })} + /> +
        @@ -467,6 +486,7 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( onChange={(v) => saveUI({ showChildTagContent: v })} /> + + + saveUI({ showLinksOnPerformerCard: v })} + /> + + + + saveLightboxSettings({ disableAnimation: v })} + /> @@ -707,6 +743,19 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( }) } /> + + saveInterface({ + disableDropdownCreate: { + ...iface.disableDropdownCreate, + gallery: v, + }, + }) + } + />
        { value={general.backupDirectoryPath ?? undefined} onChange={(v) => saveGeneral({ backupDirectoryPath: v })} /> + + saveGeneral({ deleteTrashPath: v })} + /> @@ -465,6 +473,14 @@ export const SettingsConfigurationPanel: React.FC = () => { checked={general.logAccess ?? false} onChange={(v) => saveGeneral({ logAccess: v })} /> + + saveGeneral({ logFileMaxSize: v })} + /> ); diff --git a/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx b/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx index e093dc60a..c36e076f4 100644 --- a/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx @@ -22,7 +22,7 @@ import { SettingSection } from "../SettingSection"; import { BooleanSetting, Setting } from "../Inputs"; import { ManualLink } from "src/components/Help/context"; import { Icon } from "src/components/Shared/Icon"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect"; import { faMinus, @@ -44,7 +44,7 @@ const CleanDialog: React.FC = ({ onClose, }) => { const intl = useIntl(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const libraryPaths = configuration?.general.stashes.map((s) => s.path); diff --git a/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx b/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx index 9fdaf09f4..87a58f292 100644 --- a/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx @@ -9,7 +9,7 @@ import { useIntl } from "react-intl"; import { Icon } from "src/components/Shared/Icon"; import { ModalComponent } from "src/components/Shared/Modal"; import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; interface IDirectorySelectionDialogProps { animation?: boolean; @@ -22,7 +22,7 @@ export const DirectorySelectionDialog: React.FC< IDirectorySelectionDialogProps > = ({ animation, allowEmpty = false, initialPaths = [], onClose }) => { const intl = useIntl(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const libraryPaths = configuration?.general.stashes.map((s) => s.path); diff --git a/ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx b/ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx index 00d129be7..c68b6d5eb 100644 --- a/ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx @@ -7,7 +7,7 @@ import { } from "../GeneratePreviewOptions"; interface IGenerateOptions { - type?: "scene" | "image"; + type?: "scene" | "image" | "gallery"; selection?: boolean; options: GQL.GenerateMetadataInput; setOptions: (s: GQL.GenerateMetadataInput) => void; @@ -27,7 +27,7 @@ export const GenerateOptions: React.FC = ({ } const showSceneOptions = !type || type === "scene"; - const showImageOptions = !type || type === "image"; + const showImageOptions = !type || type === "image" || type === "gallery"; return ( <> @@ -100,7 +100,6 @@ export const GenerateOptions: React.FC = ({ id="marker-image-preview-task" className="sub-setting" checked={options.markerImagePreviews ?? false} - disabled={!options.markers} headingID="dialogs.scene_gen.marker_image_previews" tooltipID="dialogs.scene_gen.marker_image_previews_tooltip" onChange={(v) => @@ -112,7 +111,6 @@ export const GenerateOptions: React.FC = ({ setOptions({ markerScreenshots: v })} @@ -169,6 +167,13 @@ export const GenerateOptions: React.FC = ({ headingID="dialogs.scene_gen.image_thumbnails" onChange={(v) => setOptions({ imageThumbnails: v })} /> + setOptions({ imagePhashes: v })} + /> )} { type DialogOpenState = typeof dialogOpen; - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const [configRead, setConfigRead] = useState(false); useEffect(() => { @@ -160,30 +160,6 @@ export const LibraryTasks: React.FC = () => { setGenerateOptions(withoutTypename(generate)); } - if (configuration?.general) { - const { general } = configuration; - setGenerateOptions((existing) => ({ - ...existing, - previewOptions: { - ...existing.previewOptions, - previewSegments: - general.previewSegments ?? - existing.previewOptions?.previewSegments, - previewSegmentDuration: - general.previewSegmentDuration ?? - existing.previewOptions?.previewSegmentDuration, - previewExcludeStart: - general.previewExcludeStart ?? - existing.previewOptions?.previewExcludeStart, - previewExcludeEnd: - general.previewExcludeEnd ?? - existing.previewOptions?.previewExcludeEnd, - previewPreset: - general.previewPreset ?? existing.previewOptions?.previewPreset, - }, - })); - } - setConfigRead(true); } }, [configuration, configRead, taskDefaults, loading]); @@ -291,7 +267,30 @@ export const LibraryTasks: React.FC = () => { async function onGenerateClicked() { try { - await mutateMetadataGenerate(generateOptions); + // insert preview options here instead of loading them + const general = configuration?.general; + + await mutateMetadataGenerate({ + ...generateOptions, + previewOptions: { + ...generateOptions.previewOptions, + previewSegments: + general?.previewSegments ?? + generateOptions.previewOptions?.previewSegments, + previewSegmentDuration: + general?.previewSegmentDuration ?? + generateOptions.previewOptions?.previewSegmentDuration, + previewExcludeStart: + general?.previewExcludeStart ?? + generateOptions.previewOptions?.previewExcludeStart, + previewExcludeEnd: + general?.previewExcludeEnd ?? + generateOptions.previewOptions?.previewExcludeEnd, + previewPreset: + general?.previewPreset ?? + generateOptions.previewOptions?.previewPreset, + }, + }); Toast.success( intl.formatMessage( { id: "config.tasks.added_job_to_queue" }, diff --git a/ui/v2.5/src/components/Settings/Tasks/ScanOptions.tsx b/ui/v2.5/src/components/Settings/Tasks/ScanOptions.tsx index 5b5c7809e..bb9eb4480 100644 --- a/ui/v2.5/src/components/Settings/Tasks/ScanOptions.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/ScanOptions.tsx @@ -18,6 +18,7 @@ export const ScanOptions: React.FC = ({ scanGenerateSprites, scanGeneratePhashes, scanGenerateThumbnails, + scanGenerateImagePhashes, scanGenerateClipPreviews, rescan, } = options; @@ -72,6 +73,13 @@ export const ScanOptions: React.FC = ({ headingID="config.tasks.generate_thumbnails_during_scan" onChange={(v) => setOptions({ scanGenerateThumbnails: v })} /> + setOptions({ scanGenerateImagePhashes: v })} + /> = ({ goBack, next }) => { - const { configuration } = useSetupContext(); + const { configuration, setupState } = useSetupContext(); const [showStashAlert, setShowStashAlert] = useState(false); - const [stashes, setStashes] = useState([]); - const [databaseFile, setDatabaseFile] = useState(""); - const [generatedLocation, setGeneratedLocation] = useState(""); - const [cacheLocation, setCacheLocation] = useState(""); - const [storeBlobsInDatabase, setStoreBlobsInDatabase] = useState(false); - const [blobsLocation, setBlobsLocation] = useState(""); + const [stashes, setStashes] = useState( + setupState.stashes ?? [] + ); + const [sfwContentMode, setSfwContentMode] = useState( + setupState.sfwContentMode ?? false + ); + + const [databaseFile, setDatabaseFile] = useState( + setupState.databaseFile ?? "" + ); + const [generatedLocation, setGeneratedLocation] = useState( + setupState.generatedLocation ?? "" + ); + const [cacheLocation, setCacheLocation] = useState( + setupState.cacheLocation ?? "" + ); + const [storeBlobsInDatabase, setStoreBlobsInDatabase] = useState( + setupState.storeBlobsInDatabase ?? false + ); + const [blobsLocation, setBlobsLocation] = useState( + setupState.blobsLocation ?? "" + ); const overrideDatabase = configuration?.general.databasePath; const overrideGenerated = configuration?.general.generatedPath; @@ -543,6 +559,7 @@ const SetPathsStep: React.FC = ({ goBack, next }) => { cacheLocation, blobsLocation: storeBlobsInDatabase ? "" : blobsLocation, storeBlobsInDatabase, + sfwContentMode, }; next(input); } @@ -582,6 +599,22 @@ const SetPathsStep: React.FC = ({ goBack, next }) => { /> + +

        + +

        +

        + +

        + + } + onChange={() => setSfwContentMode(!sfwContentMode)} + /> + +
        {overrideDatabase ? null : ( = ({ stash }) => { }; const ConfirmStep: React.FC = ({ goBack, next }) => { - const { configuration, pathDir, pathJoin, pwd, setupState } = - useSetupContext(); + const { + configuration, + pathDir, + pathJoin, + setupState, + homeDirPath, + workingDir, + } = useSetupContext(); + // if unset, means use homeDirPath const cfgFile = setupState.configLocation - ? setupState.configLocation - : pathJoin(pwd, "config.yml"); + ? pathJoin(workingDir, setupState.configLocation) + : pathJoin(homeDirPath, "config.yml"); const cfgDir = pathDir(cfgFile); const stashes = setupState.stashes ?? []; const { @@ -933,8 +973,7 @@ const FinishStep: React.FC = ({ goBack }) => { export const Setup: React.FC = () => { const intl = useIntl(); - const { configuration, loading: configLoading } = - useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const [saveUI] = useConfigureUI(); @@ -1005,7 +1044,7 @@ export const Setup: React.FC = () => { } } - if (configLoading || statusLoading) { + if (statusLoading) { return ; } diff --git a/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx b/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx index 542ab5b3b..cf78798e1 100644 --- a/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx +++ b/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx @@ -7,6 +7,7 @@ import { Icon } from "./Icon"; interface IBulkUpdateTextInputProps extends FormControlProps { valueChanged: (value: string | undefined) => void; unsetDisabled?: boolean; + as?: React.ElementType; } export const BulkUpdateTextInput: React.FC = ({ @@ -24,6 +25,7 @@ export const BulkUpdateTextInput: React.FC = ({ {...props} className="input-control" type="text" + as={props.as} value={props.value ?? ""} placeholder={ props.value === undefined diff --git a/ui/v2.5/src/components/Shared/CollapseButton.tsx b/ui/v2.5/src/components/Shared/CollapseButton.tsx index d0b192162..0d05f6e64 100644 --- a/ui/v2.5/src/components/Shared/CollapseButton.tsx +++ b/ui/v2.5/src/components/Shared/CollapseButton.tsx @@ -3,7 +3,7 @@ import { faChevronRight, faChevronUp, } from "@fortawesome/free-solid-svg-icons"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Button, Collapse, CollapseProps } from "react-bootstrap"; import { Icon } from "./Icon"; @@ -12,22 +12,27 @@ interface IProps { text: React.ReactNode; collapseProps?: Partial; outsideCollapse?: React.ReactNode; - onOpen?: () => void; + onOpenChanged?: (o: boolean) => void; + open?: boolean; } export const CollapseButton: React.FC> = ( props: React.PropsWithChildren ) => { - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(props.open ?? false); function toggleOpen() { const nv = !open; setOpen(nv); - if (props.onOpen && nv) { - props.onOpen(); - } + props.onOpenChanged?.(nv); } + useEffect(() => { + if (props.open !== undefined) { + setOpen(props.open); + } + }, [props.open]); + return (
        diff --git a/ui/v2.5/src/components/Shared/CountButton.tsx b/ui/v2.5/src/components/Shared/CountButton.tsx index 1519c104b..ad099c2f5 100644 --- a/ui/v2.5/src/components/Shared/CountButton.tsx +++ b/ui/v2.5/src/components/Shared/CountButton.tsx @@ -1,10 +1,11 @@ -import { faEye } from "@fortawesome/free-solid-svg-icons"; +import { faEye, faThumbsUp } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button, ButtonGroup } from "react-bootstrap"; import { Icon } from "src/components/Shared/Icon"; import { SweatDrops } from "./SweatDrops"; import cx from "classnames"; import { useIntl } from "react-intl"; +import { useConfigurationContext } from "src/hooks/Config"; interface ICountButtonProps { value: number; @@ -63,11 +64,17 @@ export const ViewCountButton: React.FC = (props) => { export const OCounterButton: React.FC = (props) => { const intl = useIntl(); + const { configuration } = useConfigurationContext(); + const { sfwContentMode } = configuration.interface; + + const icon = !sfwContentMode ? : ; + const messageID = !sfwContentMode ? "o_count" : "o_count_sfw"; + return ( } - title={intl.formatMessage({ id: "o_count" })} + icon={icon} + title={intl.formatMessage({ id: messageID })} countTitle={intl.formatMessage({ id: "actions.view_history" })} /> ); diff --git a/ui/v2.5/src/components/Shared/CustomFields.tsx b/ui/v2.5/src/components/Shared/CustomFields.tsx index a522961a8..c8d389a17 100644 --- a/ui/v2.5/src/components/Shared/CustomFields.tsx +++ b/ui/v2.5/src/components/Shared/CustomFields.tsx @@ -8,6 +8,7 @@ import { Icon } from "./Icon"; import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons"; import cx from "classnames"; import { PatchComponent } from "src/patch"; +import { TruncatedText } from "./TruncatedText"; const maxFieldNameLength = 64; @@ -47,7 +48,7 @@ const CustomField: React.FC<{ field: string; value: unknown }> = ({ id={id} label={field} labelTitle={field} - value={valueStr} + value={{valueStr}} />} fullWidth={true} showEmpty /> @@ -188,136 +189,134 @@ interface ICustomFieldsInput { setError: (error?: string) => void; } -export const CustomFieldsInput: React.FC = ({ - values, - error, - onChange, - setError, -}) => { - const intl = useIntl(); +export const CustomFieldsInput: React.FC = PatchComponent( + "CustomFieldsInput", + ({ values, error, onChange, setError }) => { + const intl = useIntl(); - const [newCustomField, setNewCustomField] = useState({ - field: "", - value: "", - }); + const [newCustomField, setNewCustomField] = useState({ + field: "", + value: "", + }); - const fields = useMemo(() => { - const valueCopy = cloneDeep(values); - if (newCustomField.field !== "" && error === undefined) { - delete valueCopy[newCustomField.field]; + const fields = useMemo(() => { + const valueCopy = cloneDeep(values); + if (newCustomField.field !== "" && error === undefined) { + delete valueCopy[newCustomField.field]; + } + + const ret = Object.keys(valueCopy); + ret.sort(); + return ret; + }, [values, newCustomField, error]); + + function onSetNewField(v: ICustomField) { + // validate the field name + let newError = undefined; + if (v.field.length > maxFieldNameLength) { + newError = intl.formatMessage({ + id: "errors.custom_fields.field_name_length", + }); + } + if (v.field.trim() === "" && v.value !== "") { + newError = intl.formatMessage({ + id: "errors.custom_fields.field_name_required", + }); + } + if (v.field.trim() !== v.field) { + newError = intl.formatMessage({ + id: "errors.custom_fields.field_name_whitespace", + }); + } + if (fields.includes(v.field)) { + newError = intl.formatMessage({ + id: "errors.custom_fields.duplicate_field", + }); + } + + const oldField = newCustomField; + + setNewCustomField(v); + + const valuesCopy = cloneDeep(values); + if (oldField.field !== "" && error === undefined) { + delete valuesCopy[oldField.field]; + } + + // if valid, pass up + if (!newError && v.field !== "") { + valuesCopy[v.field] = v.value; + } + + onChange(valuesCopy); + setError(newError); } - const ret = Object.keys(valueCopy); - ret.sort(); - return ret; - }, [values, newCustomField, error]); - - function onSetNewField(v: ICustomField) { - // validate the field name - let newError = undefined; - if (v.field.length > maxFieldNameLength) { - newError = intl.formatMessage({ - id: "errors.custom_fields.field_name_length", - }); - } - if (v.field.trim() === "" && v.value !== "") { - newError = intl.formatMessage({ - id: "errors.custom_fields.field_name_required", - }); - } - if (v.field.trim() !== v.field) { - newError = intl.formatMessage({ - id: "errors.custom_fields.field_name_whitespace", - }); - } - if (fields.includes(v.field)) { - newError = intl.formatMessage({ - id: "errors.custom_fields.duplicate_field", - }); + function onAdd() { + const newValues = { + ...values, + [newCustomField.field]: newCustomField.value, + }; + setNewCustomField({ field: "", value: "" }); + onChange(newValues); } - const oldField = newCustomField; - - setNewCustomField(v); - - const valuesCopy = cloneDeep(values); - if (oldField.field !== "" && error === undefined) { - delete valuesCopy[oldField.field]; + function fieldChanged( + currentField: string, + newField: string, + value: unknown + ) { + let newValues = cloneDeep(values); + delete newValues[currentField]; + if (newField !== "") { + newValues[newField] = value; + } + onChange(newValues); } - // if valid, pass up - if (!newError && v.field !== "") { - valuesCopy[v.field] = v.value; - } - - onChange(valuesCopy); - setError(newError); - } - - function onAdd() { - const newValues = { - ...values, - [newCustomField.field]: newCustomField.value, - }; - setNewCustomField({ field: "", value: "" }); - onChange(newValues); - } - - function fieldChanged( - currentField: string, - newField: string, - value: unknown - ) { - let newValues = cloneDeep(values); - delete newValues[currentField]; - if (newField !== "") { - newValues[newField] = value; - } - onChange(newValues); - } - - return ( - - - - - - - - - - - - {fields.map((field) => ( - - fieldChanged(field, newField, newValue) - } - /> - ))} - onSetNewField({ field, value })} - isNew - /> - - - - - ); -}; + + + + + + + + + + + {fields.map((field) => ( + + fieldChanged(field, newField, newValue) + } + /> + ))} + onSetNewField({ field, value })} + isNew + /> + + + + + ); + } +); diff --git a/ui/v2.5/src/components/Shared/Date.tsx b/ui/v2.5/src/components/Shared/Date.tsx new file mode 100644 index 000000000..78dd23afa --- /dev/null +++ b/ui/v2.5/src/components/Shared/Date.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { FormattedDate as IntlDate } from "react-intl"; +import { PatchComponent } from "src/patch"; + +// wraps FormattedDate to handle year or year/month dates +export const FormattedDate: React.FC<{ + value: string | number | Date | undefined; +}> = PatchComponent("Date", ({ value }) => { + if (typeof value === "string") { + // try parsing as year or year/month + const yearMatch = value.match(/^(\d{4})$/); + if (yearMatch) { + const year = parseInt(yearMatch[1], 10); + return ( + + ); + } + + const yearMonthMatch = value.match(/^(\d{4})-(\d{2})$/); + if (yearMonthMatch) { + const year = parseInt(yearMonthMatch[1], 10); + const month = parseInt(yearMonthMatch[2], 10) - 1; + + return ( + + ); + } + } + + return ; +}); diff --git a/ui/v2.5/src/components/Shared/DeleteFilesDialog.tsx b/ui/v2.5/src/components/Shared/DeleteFilesDialog.tsx index e7d5af9ac..7d790539b 100644 --- a/ui/v2.5/src/components/Shared/DeleteFilesDialog.tsx +++ b/ui/v2.5/src/components/Shared/DeleteFilesDialog.tsx @@ -2,6 +2,7 @@ import React, { useState } from "react"; import { mutateDeleteFiles } from "src/core/StashService"; import { ModalComponent } from "./Modal"; import { useToast } from "src/hooks/Toast"; +import { ConfigurationContext } from "src/hooks/Config"; import { FormattedMessage, useIntl } from "react-intl"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; @@ -40,6 +41,9 @@ export const DeleteFilesDialog: React.FC = ( // Network state const [isDeleting, setIsDeleting] = useState(false); + const context = React.useContext(ConfigurationContext); + const config = context?.configuration; + async function onDelete() { setIsDeleting(true); try { @@ -56,6 +60,11 @@ export const DeleteFilesDialog: React.FC = ( function renderDeleteFileAlert() { const deletedFiles = props.selected.map((f) => f.path); + const deleteTrashPath = config?.general.deleteTrashPath; + const deleteAlertId = deleteTrashPath + ? "dialogs.delete_alert_to_trash" + : "dialogs.delete_alert"; + return (

        @@ -65,7 +74,7 @@ export const DeleteFilesDialog: React.FC = ( singularEntity: intl.formatMessage({ id: "file" }), pluralEntity: intl.formatMessage({ id: "files" }), }} - id="dialogs.delete_alert" + id={deleteAlertId} />

          diff --git a/ui/v2.5/src/components/Shared/DetailItem.tsx b/ui/v2.5/src/components/Shared/DetailItem.tsx index a92f75868..76b595127 100644 --- a/ui/v2.5/src/components/Shared/DetailItem.tsx +++ b/ui/v2.5/src/components/Shared/DetailItem.tsx @@ -3,6 +3,7 @@ import { FormattedMessage } from "react-intl"; interface IDetailItem { id?: string | null; + className?: string; label?: React.ReactNode; value?: React.ReactNode; labelTitle?: string; @@ -13,6 +14,7 @@ interface IDetailItem { export const DetailItem: React.FC = ({ id, + className = "", label, value, labelTitle, @@ -30,7 +32,7 @@ export const DetailItem: React.FC = ({ const sanitisedID = id.replace(/_/g, "-"); return ( -
          +
          {message} {fullWidth ? ":" : ""} diff --git a/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx b/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx index 2eafcbbc5..cea415db7 100644 --- a/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx +++ b/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx @@ -1,4 +1,4 @@ -import { Button, Modal } from "react-bootstrap"; +import { Button, Dropdown, Modal, SplitButton } from "react-bootstrap"; import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { ImageInput } from "./ImageInput"; @@ -10,6 +10,7 @@ interface IProps { isEditing: boolean; onToggleEdit: () => void; onSave: () => void; + onSaveAndNew?: () => void; saveDisabled?: boolean; onDelete: () => void; onAutoTag?: () => void; @@ -48,6 +49,23 @@ export const DetailsEditNavbar: React.FC = (props: IProps) => { function renderSaveButton() { if (!props.isEditing) return; + if (props.isNew && props.onSaveAndNew) { + return ( + props.onSave()} + > + props.onSaveAndNew!()}> + + + + ); + } + return ( + {onReset && ( + + )} ); } diff --git a/ui/v2.5/src/components/Shared/MultiSet.tsx b/ui/v2.5/src/components/Shared/MultiSet.tsx index b18c4087a..6be85b8b3 100644 --- a/ui/v2.5/src/components/Shared/MultiSet.tsx +++ b/ui/v2.5/src/components/Shared/MultiSet.tsx @@ -8,6 +8,10 @@ import { GalleryIDSelect, excludeFileBasedGalleries, } from "../Galleries/GallerySelect"; +import { PerformerIDSelect } from "../Performers/PerformerSelect"; +import { StudioIDSelect } from "../Studios/StudioSelect"; +import { TagIDSelect } from "../Tags/TagSelect"; +import { GroupIDSelect } from "../Groups/GroupSelect"; interface IMultiSetProps { type: "performers" | "studios" | "tags" | "groups" | "galleries"; @@ -27,32 +31,77 @@ const Select: React.FC = (props) => { props.onUpdate(items.map((i) => i.id)); } - if (type === "galleries") { - return ( - - ); + switch (type) { + case "performers": + return ( + + ); + case "studios": + return ( + + ); + case "tags": + return ( + + ); + case "groups": + return ( + + ); + case "galleries": + return ( + + ); + default: + return ( + + ); } - - return ( - - ); }; function getModeText(intl: IntlShape, mode: GQL.BulkUpdateIdMode) { diff --git a/ui/v2.5/src/components/Shared/PopoverCountButton.tsx b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx index 79a36bd9d..d652ff6ad 100644 --- a/ui/v2.5/src/components/Shared/PopoverCountButton.tsx +++ b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx @@ -11,14 +11,14 @@ import React from "react"; import { Button, OverlayTrigger, Tooltip } from "react-bootstrap"; import { FormattedNumber, useIntl } from "react-intl"; import { Link } from "react-router-dom"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import TextUtils from "src/utils/text"; import { Icon } from "./Icon"; export const Count: React.FC<{ count: number; }> = ({ count }) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const abbreviateCounter = configuration?.ui.abbreviateCounters ?? false; if (!abbreviateCounter) { diff --git a/ui/v2.5/src/components/Shared/Rating/RatingStars.tsx b/ui/v2.5/src/components/Shared/Rating/RatingStars.tsx index 5339044a1..f83c556fc 100644 --- a/ui/v2.5/src/components/Shared/Rating/RatingStars.tsx +++ b/ui/v2.5/src/components/Shared/Rating/RatingStars.tsx @@ -228,21 +228,16 @@ export const RatingStars = PatchComponent( ); }; - const maybeRenderStarRatingNumber = () => { + const maybeGetStarRatingNumber = () => { const ratingFraction = getCurrentSelectedRating(); if ( !ratingFraction || (ratingFraction.rating == 0 && ratingFraction.fraction == 0) ) { - return; + return ""; } - return ( - - {ratingFraction.rating + ratingFraction.fraction} - {suffix} - - ); + return ratingFraction.rating + ratingFraction.fraction + suffix; }; const precisionClassName = `rating-stars-precision-${props.precision}`; @@ -252,7 +247,7 @@ export const RatingStars = PatchComponent( {Array.from(Array(max)).map((value, index) => renderRatingButton(index + 1) )} - {maybeRenderStarRatingNumber()} + {maybeGetStarRatingNumber()}
          ); } diff --git a/ui/v2.5/src/components/Shared/Rating/RatingSystem.tsx b/ui/v2.5/src/components/Shared/Rating/RatingSystem.tsx index a0a11c363..11103acf8 100644 --- a/ui/v2.5/src/components/Shared/Rating/RatingSystem.tsx +++ b/ui/v2.5/src/components/Shared/Rating/RatingSystem.tsx @@ -1,5 +1,4 @@ -import React from "react"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { defaultRatingStarPrecision, defaultRatingSystemOptions, @@ -23,7 +22,7 @@ export interface IRatingSystemProps { export const RatingSystem = PatchComponent( "RatingSystem", (props: IRatingSystemProps) => { - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const ratingSystemOptions = config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions; diff --git a/ui/v2.5/src/components/Shared/RatingBanner.tsx b/ui/v2.5/src/components/Shared/RatingBanner.tsx index d152b8b52..d94b26433 100644 --- a/ui/v2.5/src/components/Shared/RatingBanner.tsx +++ b/ui/v2.5/src/components/Shared/RatingBanner.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React from "react"; import { FormattedMessage } from "react-intl"; import { convertToRatingFormat, @@ -6,14 +6,14 @@ import { RatingStarPrecision, RatingSystemType, } from "src/utils/rating"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; interface IProps { rating?: number | null; } export const RatingBanner: React.FC = ({ rating }) => { - const { configuration: config } = useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const ratingSystemOptions = config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions; const isLegacy = diff --git a/ui/v2.5/src/components/Shared/ScrapeDialog/CreateLinkTagDialog.tsx b/ui/v2.5/src/components/Shared/ScrapeDialog/CreateLinkTagDialog.tsx new file mode 100644 index 000000000..b5b19d913 --- /dev/null +++ b/ui/v2.5/src/components/Shared/ScrapeDialog/CreateLinkTagDialog.tsx @@ -0,0 +1,136 @@ +import React, { useEffect, useState } from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import * as GQL from "src/core/generated-graphql"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { faLink } from "@fortawesome/free-solid-svg-icons"; +import { Form } from "react-bootstrap"; +import { Tag, TagSelect } from "../../Tags/TagSelect"; + +export const CreateLinkTagDialog: React.FC<{ + tag: GQL.ScrapedTag; + onClose: (result: { + create?: GQL.TagCreateInput; + update?: GQL.TagUpdateInput; + }) => void; + endpoint?: string; +}> = ({ tag, onClose, endpoint }) => { + const intl = useIntl(); + + const [createNew, setCreateNew] = useState(false); + const [name, setName] = useState(tag.name); + const [existingTag, setExistingTag] = useState(null); + const [addAsAlias, setAddAsAlias] = useState(false); + + const canAddAlias = (createNew && name !== tag.name) || !createNew; + + useEffect(() => { + setAddAsAlias(canAddAlias); + }, [canAddAlias]); + + function handleTagSave() { + if (createNew) { + const createInput: GQL.TagCreateInput = { + name: name, + aliases: addAsAlias ? [tag.name] : [], + stash_ids: + endpoint && tag.remote_site_id + ? [{ endpoint: endpoint!, stash_id: tag.remote_site_id }] + : undefined, + }; + onClose({ create: createInput }); + } else if (existingTag) { + const updateInput: GQL.TagUpdateInput = { + id: existingTag.id, + aliases: addAsAlias + ? [...(existingTag.aliases || []), tag.name] + : undefined, + // add stash id if applicable + stash_ids: + endpoint && tag.remote_site_id + ? [ + ...(existingTag.stash_ids || []), + { endpoint: endpoint!, stash_id: tag.remote_site_id }, + ] + : undefined, + }; + onClose({ update: updateInput }); + } + } + + return ( + handleTagSave(), + }} + disabled={createNew ? name.trim() === "" : existingTag === null} + cancel={{ + text: intl.formatMessage({ id: "actions.cancel" }), + onClick: () => { + onClose({}); + }, + }} + dialogClassName="create-link-tag-modal" + icon={faLink} + header={intl.formatMessage({ id: "component_tagger.verb_match_tag" })} + > + + setCreateNew(true)} + /> + + + + + + setName(e.target.value)} + disabled={!createNew} + /> + + + setCreateNew(false)} + /> + + + setExistingTag(t.length > 0 ? t[0] : null)} + isDisabled={createNew} + menuPortalTarget={document.body} + /> + + + + setAddAsAlias(!addAsAlias)} + disabled={!canAddAlias} + /> + + + + ); +}; diff --git a/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialog.tsx b/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialog.tsx index 59d5f3985..ecf95541f 100644 --- a/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialog.tsx +++ b/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialog.tsx @@ -1,438 +1,56 @@ -import React, { useState } from "react"; -import { - Form, - Col, - Row, - InputGroup, - Button, - FormControl, - Badge, -} from "react-bootstrap"; -import { CollapseButton } from "../CollapseButton"; -import { Icon } from "../Icon"; +import React, { useMemo } from "react"; +import { Form, Col, Row } from "react-bootstrap"; import { ModalComponent } from "../Modal"; -import clone from "lodash-es/clone"; import { FormattedMessage, useIntl } from "react-intl"; -import { - faCheck, - faPencilAlt, - faPlus, - faTimes, -} from "@fortawesome/free-solid-svg-icons"; -import { getCountryByISO } from "src/utils/country"; -import { CountrySelect } from "../CountrySelect"; -import { StringListInput } from "../StringListInput"; -import { ImageSelector } from "../ImageSelector"; -import { ScrapeResult } from "./scrapeResult"; +import { faPencilAlt } from "@fortawesome/free-solid-svg-icons"; +import { useConfigurationContext } from "src/hooks/Config"; -interface IScrapedFieldProps { - result: ScrapeResult; +export interface IScrapeDialogContextState { + existingLabel?: React.ReactNode; + scrapedLabel?: React.ReactNode; } -interface IScrapedRowProps extends IScrapedFieldProps { - className?: string; - title: string; - renderOriginalField: (result: ScrapeResult) => JSX.Element | undefined; - renderNewField: (result: ScrapeResult) => JSX.Element | undefined; - onChange: (value: ScrapeResult) => void; - newValues?: V[]; - onCreateNew?: (index: number) => void; - getName?: (value: V) => string; -} - -function renderButtonIcon(selected: boolean) { - const className = selected ? "text-success" : "text-muted"; - - return ( - - ); -} - -export const ScrapeDialogRow = (props: IScrapedRowProps) => { - const { getName = () => "" } = props; - - function handleSelectClick(isNew: boolean) { - const ret = clone(props.result); - ret.useNewValue = isNew; - props.onChange(ret); - } - - function hasNewValues() { - return props.newValues && props.newValues.length > 0 && props.onCreateNew; - } - - if (!props.result.scraped && !hasNewValues()) { - return <>; - } - - function renderNewValues() { - if (!hasNewValues()) { - return; - } - - const ret = ( - <> - {props.newValues!.map((t, i) => ( - props.onCreateNew!(i)} - > - {getName(t)} - - - ))} - - ); - - const minCollapseLength = 10; - - if (props.newValues!.length >= minCollapseLength) { - return ( - - {ret} - - ); - } - - return ret; - } - - return ( - - - {props.title} - - - - - - - - - - {props.renderOriginalField(props.result)} - - - - - - - - {props.renderNewField(props.result)} - - {renderNewValues()} - - - - - ); -}; - -interface IScrapedInputGroupProps { - isNew?: boolean; - placeholder?: string; - locked?: boolean; - result: ScrapeResult; - onChange?: (value: string) => void; -} - -const ScrapedInputGroup: React.FC = (props) => { - return ( - { - if (props.isNew && props.onChange) { - props.onChange(e.target.value); - } - }} - className="bg-secondary text-white border-secondary" - /> - ); -}; - -function getNameString(value: string) { - return value; -} - -interface IScrapedInputGroupRowProps { - title: string; - placeholder?: string; - result: ScrapeResult; - locked?: boolean; - onChange: (value: ScrapeResult) => void; -} - -export const ScrapedInputGroupRow: React.FC = ( - props -) => { - return ( - ( - - )} - renderNewField={() => ( - - props.onChange(props.result.cloneWithValue(value)) - } - /> - )} - onChange={props.onChange} - getName={getNameString} - /> - ); -}; - -interface IScrapedStringListProps { - isNew?: boolean; - placeholder?: string; - locked?: boolean; - result: ScrapeResult; - onChange?: (value: string[]) => void; -} - -const ScrapedStringList: React.FC = (props) => { - const value = props.isNew - ? props.result.newValue - : props.result.originalValue; - - return ( - { - if (props.isNew && props.onChange) { - props.onChange(v); - } - }} - placeholder={props.placeholder} - readOnly={!props.isNew || props.locked} - /> - ); -}; - -interface IScrapedStringListRowProps { - title: string; - placeholder?: string; - result: ScrapeResult; - locked?: boolean; - onChange: (value: ScrapeResult) => void; -} - -export const ScrapedStringListRow: React.FC = ( - props -) => { - return ( - ( - - )} - renderNewField={() => ( - - props.onChange(props.result.cloneWithValue(value)) - } - /> - )} - onChange={props.onChange} - getName={getNameString} - /> - ); -}; - -const ScrapedTextArea: React.FC = (props) => { - return ( - { - if (props.isNew && props.onChange) { - props.onChange(e.target.value); - } - }} - className="bg-secondary text-white border-secondary scene-description" - /> - ); -}; - -export const ScrapedTextAreaRow: React.FC = ( - props -) => { - return ( - ( - - )} - renderNewField={() => ( - - props.onChange(props.result.cloneWithValue(value)) - } - /> - )} - onChange={props.onChange} - getName={getNameString} - /> - ); -}; - -interface IScrapedImageProps { - isNew?: boolean; - className?: string; - placeholder?: string; - result: ScrapeResult; -} - -const ScrapedImage: React.FC = (props) => { - const value = props.isNew - ? props.result.newValue - : props.result.originalValue; - - if (!value) { - return <>; - } - - return ( - {props.placeholder} - ); -}; - -interface IScrapedImageRowProps { - title: string; - className?: string; - result: ScrapeResult; - onChange: (value: ScrapeResult) => void; -} - -export const ScrapedImageRow: React.FC = (props) => { - return ( - ( - - )} - renderNewField={() => ( - - )} - onChange={props.onChange} - getName={getNameString} - /> - ); -}; - -interface IScrapedImagesRowProps { - title: string; - className?: string; - result: ScrapeResult; - images: string[]; - onChange: (value: ScrapeResult) => void; -} - -export const ScrapedImagesRow: React.FC = (props) => { - const [imageIndex, setImageIndex] = useState(0); - - function onSetImageIndex(newIdx: number) { - const ret = props.result.cloneWithValue(props.images[newIdx]); - props.onChange(ret); - setImageIndex(newIdx); - } - - return ( - ( - - )} - renderNewField={() => ( -
          - -
          - )} - onChange={props.onChange} - getName={getNameString} - /> - ); -}; +export const ScrapeDialogContext = + React.createContext({}); interface IScrapeDialogProps { + className?: string; title: string; - existingLabel?: string; - scrapedLabel?: string; - renderScrapeRows: () => JSX.Element; + existingLabel?: React.ReactNode; + scrapedLabel?: React.ReactNode; onClose: (apply?: boolean) => void; } -export const ScrapeDialog: React.FC = ( - props: IScrapeDialogProps -) => { +export const ScrapeDialog: React.FC< + React.PropsWithChildren +> = (props: React.PropsWithChildren) => { const intl = useIntl(); + const { configuration } = useConfigurationContext(); + const { sfwContentMode } = configuration.interface; + + const existingLabel = useMemo( + () => + props.existingLabel ?? ( + + ), + [props.existingLabel] + ); + const scrapedLabel = useMemo( + () => + props.scrapedLabel ?? ( + + ), + [props.scrapedLabel] + ); + + const contextState = useMemo( + () => ({ + existingLabel: existingLabel, + scrapedLabel: scrapedLabel, + }), + [existingLabel, scrapedLabel] + ); + return ( = ( text: intl.formatMessage({ id: "actions.cancel" }), variant: "secondary", }} - modalProps={{ size: "lg", dialogClassName: "scrape-dialog" }} + modalProps={{ + size: "lg", + dialogClassName: `${props.className ?? ""} scrape-dialog ${ + sfwContentMode ? "sfw-mode" : "" + }`, + }} >
          -
          - - - - - {props.existingLabel ?? ( - - )} - - - {props.scrapedLabel ?? ( - - )} - - - - + + + + + + + {existingLabel} + + + {scrapedLabel} + + + + - {props.renderScrapeRows()} - + {props.children} + +
          ); }; - -interface IScrapedCountryRowProps { - title: string; - result: ScrapeResult; - onChange: (value: ScrapeResult) => void; - locked?: boolean; - locale?: string; -} - -export const ScrapedCountryRow: React.FC = ({ - title, - result, - onChange, - locked, - locale, -}) => ( - ( - - )} - renderNewField={() => ( - { - if (onChange) { - onChange(result.cloneWithValue(value)); - } - }} - showFlag={false} - isClearable={false} - className="flex-grow-1" - /> - )} - onChange={onChange} - getName={getNameString} - /> -); diff --git a/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialogRow.tsx b/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialogRow.tsx new file mode 100644 index 000000000..88b79d87d --- /dev/null +++ b/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialogRow.tsx @@ -0,0 +1,433 @@ +import React, { useContext, useState } from "react"; +import { + Form, + Col, + Row, + InputGroup, + Button, + FormControl, +} from "react-bootstrap"; +import { Icon } from "../Icon"; +import clone from "lodash-es/clone"; +import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons"; +import { getCountryByISO } from "src/utils/country"; +import { CountrySelect } from "../CountrySelect"; +import { StringListInput } from "../StringListInput"; +import { ImageSelector } from "../ImageSelector"; +import { ScrapeResult } from "./scrapeResult"; +import { ScrapeDialogContext } from "./ScrapeDialog"; + +function renderButtonIcon(selected: boolean) { + const className = selected ? "text-success" : "text-muted"; + + return ( + + ); +} + +interface IScrapedFieldProps { + result: ScrapeResult; +} + +interface IScrapedRowProps extends IScrapedFieldProps { + className?: string; + field: string; + title: string; + originalField: React.ReactNode; + newField: React.ReactNode; + onChange: (value: ScrapeResult) => void; + newValues?: React.ReactNode; +} + +export const ScrapeDialogRow = (props: IScrapedRowProps) => { + const { existingLabel, scrapedLabel } = useContext(ScrapeDialogContext); + + function handleSelectClick(isNew: boolean) { + const ret = clone(props.result); + ret.useNewValue = isNew; + props.onChange(ret); + } + + if (!props.result.scraped && !props.newValues) { + return <>; + } + + return ( + + + {props.title} + + + + + + {existingLabel} + + + + + + + {props.originalField} + + + + + {scrapedLabel} + + + + + + + {props.newField} + + {props.newValues} + + + + + ); +}; + +interface IScrapedInputGroupProps { + isNew?: boolean; + placeholder?: string; + locked?: boolean; + result: ScrapeResult; + onChange?: (value: string) => void; +} + +const ScrapedInputGroup: React.FC = (props) => { + return ( + { + if (props.isNew && props.onChange) { + props.onChange(e.target.value); + } + }} + className="bg-secondary text-white border-secondary" + /> + ); +}; + +interface IScrapedInputGroupRowProps { + title: string; + field: string; + className?: string; + placeholder?: string; + result: ScrapeResult; + locked?: boolean; + onChange: (value: ScrapeResult) => void; +} + +export const ScrapedInputGroupRow: React.FC = ( + props +) => { + return ( + + } + newField={ + + props.onChange(props.result.cloneWithValue(value)) + } + /> + } + onChange={props.onChange} + /> + ); +}; + +interface IScrapedStringListProps { + isNew?: boolean; + placeholder?: string; + locked?: boolean; + result: ScrapeResult; + onChange?: (value: string[]) => void; +} + +const ScrapedStringList: React.FC = (props) => { + const value = props.isNew + ? props.result.newValue + : props.result.originalValue; + + return ( + { + if (props.isNew && props.onChange) { + props.onChange(v); + } + }} + placeholder={props.placeholder} + readOnly={!props.isNew || props.locked} + /> + ); +}; + +interface IScrapedStringListRowProps { + title: string; + field: string; + placeholder?: string; + result: ScrapeResult; + locked?: boolean; + onChange: (value: ScrapeResult) => void; +} + +export const ScrapedStringListRow: React.FC = ( + props +) => { + return ( + + } + newField={ + + props.onChange(props.result.cloneWithValue(value)) + } + /> + } + onChange={props.onChange} + /> + ); +}; + +const ScrapedTextArea: React.FC = (props) => { + return ( + { + if (props.isNew && props.onChange) { + props.onChange(e.target.value); + } + }} + className="bg-secondary text-white border-secondary scene-description" + /> + ); +}; + +export const ScrapedTextAreaRow: React.FC = ( + props +) => { + return ( + + } + newField={ + + props.onChange(props.result.cloneWithValue(value)) + } + /> + } + onChange={props.onChange} + /> + ); +}; + +interface IScrapedImageProps { + isNew?: boolean; + className?: string; + placeholder?: string; + result: ScrapeResult; +} + +const ScrapedImage: React.FC = (props) => { + const value = props.isNew + ? props.result.newValue + : props.result.originalValue; + + if (!value) { + return <>; + } + + return ( + {props.placeholder} + ); +}; + +interface IScrapedImageRowProps { + title: string; + field: string; + className?: string; + result: ScrapeResult; + onChange: (value: ScrapeResult) => void; +} + +export const ScrapedImageRow: React.FC = (props) => { + return ( + + } + newField={ + + } + onChange={props.onChange} + /> + ); +}; + +interface IScrapedImagesRowProps { + title: string; + field: string; + className?: string; + result: ScrapeResult; + images: string[]; + onChange: (value: ScrapeResult) => void; +} + +export const ScrapedImagesRow: React.FC = (props) => { + const [imageIndex, setImageIndex] = useState(0); + + function onSetImageIndex(newIdx: number) { + const ret = props.result.cloneWithValue(props.images[newIdx]); + props.onChange(ret); + setImageIndex(newIdx); + } + + return ( + + } + newField={ +
          + +
          + } + onChange={props.onChange} + /> + ); +}; + +interface IScrapedCountryRowProps { + title: string; + field: string; + result: ScrapeResult; + onChange: (value: ScrapeResult) => void; + locked?: boolean; + locale?: string; +} + +export const ScrapedCountryRow: React.FC = ({ + title, + field, + result, + onChange, + locked, + locale, +}) => ( + + } + newField={ + { + if (onChange) { + onChange(result.cloneWithValue(value)); + } + }} + showFlag={false} + isClearable={false} + className="flex-grow-1" + /> + } + onChange={onChange} + /> +); diff --git a/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapedObjectsRow.tsx b/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapedObjectsRow.tsx index 3d7bbe4ad..f383f245a 100644 --- a/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapedObjectsRow.tsx +++ b/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapedObjectsRow.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from "react"; import * as GQL from "src/core/generated-graphql"; -import { ScrapeDialogRow } from "src/components/Shared/ScrapeDialog/ScrapeDialog"; +import { ScrapeDialogRow } from "src/components/Shared/ScrapeDialog/ScrapeDialogRow"; import { PerformerSelect } from "src/components/Performers/PerformerSelect"; import { ObjectScrapeResult, @@ -10,13 +10,79 @@ import { TagIDSelect } from "src/components/Tags/TagSelect"; import { StudioSelect } from "src/components/Studios/StudioSelect"; import { GroupSelect } from "src/components/Groups/GroupSelect"; import { uniq } from "lodash-es"; +import { CollapseButton } from "../CollapseButton"; +import { Badge, Button } from "react-bootstrap"; +import { Icon } from "../Icon"; +import { faLink, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { useIntl } from "react-intl"; + +interface INewScrapedObjects { + newValues: T[]; + onCreateNew: (value: T) => void; + onLinkExisting?: (value: T) => void; + getName: (value: T) => string; +} + +export const NewScrapedObjects = (props: INewScrapedObjects) => { + const intl = useIntl(); + + if (props.newValues.length === 0) { + return null; + } + + const ret = ( + <> + {props.newValues.map((t) => ( + props.onCreateNew(t)} + > + {props.getName(t)} + + {props.onLinkExisting ? ( + + ) : null} + + ))} + + ); + + const minCollapseLength = 10; + + if (props.newValues!.length >= minCollapseLength) { + const missingText = intl.formatMessage({ + id: "dialogs.scrape_results_missing", + }); + return ( + + {ret} + + ); + } + + return ret; +}; interface IScrapedStudioRow { title: string; + field: string; result: ObjectScrapeResult; onChange: (value: ObjectScrapeResult) => void; newStudio?: GQL.ScrapedStudio; onCreateNew?: (value: GQL.ScrapedStudio) => void; + onLinkExisting?: (value: GQL.ScrapedStudio) => void; } function getObjectName(value: T) { @@ -25,10 +91,12 @@ function getObjectName(value: T) { export const ScrapedStudioRow: React.FC = ({ title, + field, result, onChange, newStudio, onCreateNew, + onLinkExisting, }) => { function renderScrapedStudio( scrapeResult: ObjectScrapeResult, @@ -41,7 +109,9 @@ export const ScrapedStudioRow: React.FC = ({ const value = resultValue ? [resultValue] : []; const selectValue = value.map((p) => { - const aliases: string[] = []; + const aliases: string[] = p.aliases + ? p.aliases.split(",").map((a) => a.trim()) + : []; return { id: p.stored_id ?? "", name: p.name ?? "", @@ -55,10 +125,11 @@ export const ScrapedStudioRow: React.FC = ({ isDisabled={!isNew} onSelect={(items) => { if (onChangeFn) { - const { id, ...data } = items[0]; + const { id, aliases, ...data } = items[0]; onChangeFn({ ...data, stored_id: id, + aliases: aliases?.join(", "), }); } }} @@ -70,29 +141,35 @@ export const ScrapedStudioRow: React.FC = ({ return ( renderScrapedStudio(result)} - renderNewField={() => - renderScrapedStudio(result, true, (value) => - onChange(result.cloneWithValue(value)) - ) - } + originalField={renderScrapedStudio(result)} + newField={renderScrapedStudio(result, true, (value) => + onChange(result.cloneWithValue(value)) + )} onChange={onChange} - newValues={newStudio ? [newStudio] : undefined} - onCreateNew={() => { - if (onCreateNew && newStudio) onCreateNew(newStudio); - }} - getName={getObjectName} + newValues={ + newStudio && onCreateNew ? ( + + ) : undefined + } /> ); }; interface IScrapedObjectsRow { title: string; + field: string; result: ScrapeResult; onChange: (value: ScrapeResult) => void; newObjects?: T[]; onCreateNew?: (value: T) => void; + onLinkExisting?: (value: T) => void; renderObjects: ( result: ScrapeResult, isNew?: boolean, @@ -104,10 +181,12 @@ interface IScrapedObjectsRow { export const ScrapedObjectsRow = (props: IScrapedObjectsRow) => { const { title, + field, result, onChange, - newObjects, + newObjects = [], onCreateNew, + onLinkExisting, renderObjects, getName, } = props; @@ -115,19 +194,23 @@ export const ScrapedObjectsRow = (props: IScrapedObjectsRow) => { return ( renderObjects(result)} - renderNewField={() => - renderObjects(result, true, (value) => - onChange(result.cloneWithValue(value)) - ) - } + originalField={renderObjects(result)} + newField={renderObjects(result, true, (value) => + onChange(result.cloneWithValue(value)) + )} onChange={onChange} - newValues={newObjects} - onCreateNew={(i) => { - if (onCreateNew) onCreateNew(newObjects![i]); - }} - getName={getName} + newValues={ + onCreateNew && newObjects.length > 0 ? ( + + ) : undefined + } /> ); }; @@ -139,7 +222,16 @@ type IScrapedObjectRowImpl = Omit< export const ScrapedPerformersRow: React.FC< IScrapedObjectRowImpl & { ageFromDate?: string | null } -> = ({ title, result, onChange, newObjects, onCreateNew, ageFromDate }) => { +> = ({ + title, + field, + result, + onChange, + newObjects, + onCreateNew, + ageFromDate, + onLinkExisting, +}) => { const performersCopy = useMemo(() => { return ( newObjects?.map((p) => { @@ -188,19 +280,29 @@ export const ScrapedPerformersRow: React.FC< return ( title={title} + field={field} result={result} renderObjects={renderScrapedPerformers} onChange={onChange} newObjects={performersCopy} onCreateNew={onCreateNew} getName={(value) => value.name ?? ""} + onLinkExisting={onLinkExisting} /> ); }; export const ScrapedGroupsRow: React.FC< IScrapedObjectRowImpl -> = ({ title, result, onChange, newObjects, onCreateNew }) => { +> = ({ + title, + field, + result, + onChange, + newObjects, + onCreateNew, + onLinkExisting, +}) => { const groupsCopy = useMemo(() => { return ( newObjects?.map((p) => { @@ -248,19 +350,29 @@ export const ScrapedGroupsRow: React.FC< return ( title={title} + field={field} result={result} renderObjects={renderScrapedGroups} onChange={onChange} newObjects={groupsCopy} onCreateNew={onCreateNew} getName={(value) => value.name ?? ""} + onLinkExisting={onLinkExisting} /> ); }; export const ScrapedTagsRow: React.FC< IScrapedObjectRowImpl -> = ({ title, result, onChange, newObjects, onCreateNew }) => { +> = ({ + title, + field, + result, + onChange, + newObjects, + onCreateNew, + onLinkExisting, +}) => { function renderScrapedTags( scrapeResult: ScrapeResult, isNew?: boolean, @@ -294,11 +406,13 @@ export const ScrapedTagsRow: React.FC< return ( title={title} + field={field} result={result} renderObjects={renderScrapedTags} onChange={onChange} newObjects={newObjects} onCreateNew={onCreateNew} + onLinkExisting={onLinkExisting} getName={getObjectName} /> ); diff --git a/ui/v2.5/src/components/Shared/ScrapeDialog/createObjects.ts b/ui/v2.5/src/components/Shared/ScrapeDialog/createObjects.ts index c4ba3a3e7..f16ecf2f2 100644 --- a/ui/v2.5/src/components/Shared/ScrapeDialog/createObjects.ts +++ b/ui/v2.5/src/components/Shared/ScrapeDialog/createObjects.ts @@ -46,6 +46,7 @@ interface IUseCreateNewStudioProps { scrapeResult: ObjectScrapeResult ) => void; setNewObject: (newObject: GQL.ScrapedStudio | undefined) => void; + endpoint?: string; } export function useCreateScrapedStudio(props: IUseCreateNewStudioProps) { @@ -54,12 +55,33 @@ export function useCreateScrapedStudio(props: IUseCreateNewStudioProps) { const { scrapeResult, setScrapeResult, setNewObject } = props; async function createNewStudio(toCreate: GQL.ScrapedStudio) { + const input: GQL.StudioCreateInput = { + name: toCreate.name, + urls: toCreate.urls, + aliases: + toCreate.aliases + ?.split(",") + .map((a) => a.trim()) + .filter((a) => a) || [], + details: toCreate.details, + image: toCreate.image, + tag_ids: (toCreate.tags ?? []) + .filter((t) => t.stored_id) + .map((t) => t.stored_id!), + }; + + if (props.endpoint && toCreate.remote_site_id) { + input.stash_ids = [ + { + endpoint: props.endpoint, + stash_id: toCreate.remote_site_id, + }, + ]; + } + const result = await createStudio({ variables: { - input: { - name: toCreate.name, - url: toCreate.url, - }, + input, }, }); @@ -81,6 +103,7 @@ interface IUseCreateNewObjectProps { setScrapeResult: (scrapeResult: ScrapeResult) => void; newObjects: T[]; setNewObjects: (newObject: T[]) => void; + endpoint?: string; } export function useCreateScrapedPerformer( @@ -91,7 +114,7 @@ export function useCreateScrapedPerformer( const { scrapeResult, setScrapeResult, newObjects, setNewObjects } = props; async function createNewPerformer(toCreate: GQL.ScrapedPerformer) { - const input = scrapedPerformerToCreateInput(toCreate); + const input = scrapedPerformerToCreateInput(toCreate, props.endpoint); const result = await createPerformer({ variables: { input }, @@ -160,26 +183,17 @@ export function useCreateScrapedGroup( return useCreateObject("group", createNewGroup); } -export function useCreateScrapedTag( +export function useLinkScrapedTag( props: IUseCreateNewObjectProps ) { - const [createTag] = useTagCreate(); - const { scrapeResult, setScrapeResult, newObjects, setNewObjects } = props; - async function createNewTag(toCreate: GQL.ScrapedTag) { - const input: GQL.TagCreateInput = { name: toCreate.name ?? "" }; - - const result = await createTag({ - variables: { input }, - }); - + function linkTag(id: string, matchedName: string, scrapedName: string) { const newValue = [...(scrapeResult.newValue ?? [])]; - if (result.data?.tagCreate) - newValue.push({ - stored_id: result.data.tagCreate.id, - name: result.data.tagCreate.name, - }); + newValue.push({ + stored_id: id, + name: matchedName, + }); // add the new tag to the new tags value const tagClone = scrapeResult.cloneWithValue(newValue); @@ -187,7 +201,7 @@ export function useCreateScrapedTag( // remove the tag from the list const newTagsClone = newObjects.concat(); - const pIndex = newTagsClone.findIndex((p) => p.name === toCreate.name); + const pIndex = newTagsClone.findIndex((p) => p.name === scrapedName); if (pIndex === -1) throw new Error("Could not find tag to remove"); newTagsClone.splice(pIndex, 1); @@ -195,5 +209,40 @@ export function useCreateScrapedTag( setNewObjects(newTagsClone); } + return linkTag; +} + +export function useCreateScrapedTag( + props: IUseCreateNewObjectProps +) { + const [createTag] = useTagCreate(); + const linkTag = useLinkScrapedTag(props); + + async function createNewTag(toCreate: GQL.ScrapedTag) { + const input: GQL.TagCreateInput = { + name: toCreate.name ?? "", + }; + + if (props.endpoint && toCreate.remote_site_id) { + input.stash_ids = [ + { + endpoint: props.endpoint, + stash_id: toCreate.remote_site_id, + }, + ]; + } + + const result = await createTag({ + variables: { input }, + }); + + if (result.data?.tagCreate) + linkTag( + result.data.tagCreate.id, + result.data.tagCreate.name, + toCreate.name ?? "" + ); + } + return useCreateObject("tag", createNewTag); } diff --git a/ui/v2.5/src/components/Shared/ScrapeDialog/scrapedTags.tsx b/ui/v2.5/src/components/Shared/ScrapeDialog/scrapedTags.tsx index 5527138d1..8ab88878d 100644 --- a/ui/v2.5/src/components/Shared/ScrapeDialog/scrapedTags.tsx +++ b/ui/v2.5/src/components/Shared/ScrapeDialog/scrapedTags.tsx @@ -4,14 +4,20 @@ import * as GQL from "src/core/generated-graphql"; import { ObjectListScrapeResult } from "./scrapeResult"; import { sortStoredIdObjects } from "src/utils/data"; import { Tag } from "src/components/Tags/TagSelect"; -import { useCreateScrapedTag } from "./createObjects"; +import { useCreateScrapedTag, useLinkScrapedTag } from "./createObjects"; import { ScrapedTagsRow } from "./ScrapedObjectsRow"; +import { CreateLinkTagDialog } from "src/components/Shared/ScrapeDialog/CreateLinkTagDialog"; +import { useTagCreate, useTagUpdate } from "src/core/StashService"; +import { toastOperation, useToast } from "src/hooks/Toast"; export function useScrapedTags( existingTags: Tag[], - scrapedTags?: GQL.Maybe + scrapedTags?: GQL.Maybe, + endpoint?: string ) { const intl = useIntl(); + const Toast = useToast(); + const [tags, setTags] = useState>( new ObjectListScrapeResult( sortStoredIdObjects( @@ -27,27 +33,105 @@ export function useScrapedTags( const [newTags, setNewTags] = useState( scrapedTags?.filter((t) => !t.stored_id) ?? [] ); + const [linkedTag, setLinkedTag] = useState(null); const createNewTag = useCreateScrapedTag({ scrapeResult: tags, setScrapeResult: setTags, newObjects: newTags, setNewObjects: setNewTags, + endpoint, }); + const [createTag] = useTagCreate(); + const [updateTag] = useTagUpdate(); + + const linkScrapedTag = useLinkScrapedTag({ + scrapeResult: tags, + setScrapeResult: setTags, + newObjects: newTags, + setNewObjects: setNewTags, + }); + + async function handleLinkTagResult(tag: { + create?: GQL.TagCreateInput; + update?: GQL.TagUpdateInput; + }) { + if (tag.create) { + await toastOperation( + Toast, + async () => { + // create the new tag + const result = await createTag({ variables: { input: tag.create! } }); + + // adjust scrape result + if (result.data?.tagCreate) { + linkScrapedTag( + result.data.tagCreate.id, + result.data.tagCreate.name, + linkedTag?.name ?? "" + ); + } + }, + intl.formatMessage( + { id: "toast.created_entity" }, + { + entity: intl.formatMessage({ id: "tag" }).toLocaleLowerCase(), + } + ) + )(); + } else if (tag.update) { + // link existing tag + await toastOperation( + Toast, + async () => { + const result = await updateTag({ variables: { input: tag.update! } }); + + // adjust scrape result + if (result.data?.tagUpdate) { + linkScrapedTag( + result.data.tagUpdate.id, + result.data.tagUpdate.name, + linkedTag?.name ?? "" + ); + } + }, + intl.formatMessage( + { id: "toast.updated_entity" }, + { + entity: intl.formatMessage({ id: "tag" }).toLocaleLowerCase(), + } + ) + )(); + } + + setLinkedTag(null); + } + + const linkDialog = linkedTag ? ( + + ) : null; + const scrapedTagsRow = ( setTags(value)} newObjects={newTags} onCreateNew={createNewTag} + onLinkExisting={(l) => setLinkedTag(l)} /> ); return { tags, newTags, + linkDialog, scrapedTagsRow, }; } diff --git a/ui/v2.5/src/components/Shared/Select.tsx b/ui/v2.5/src/components/Shared/Select.tsx index 4ae547cfe..4ad9a51c9 100644 --- a/ui/v2.5/src/components/Shared/Select.tsx +++ b/ui/v2.5/src/components/Shared/Select.tsx @@ -15,7 +15,7 @@ import CreatableSelect from "react-select/creatable"; import * as GQL from "src/core/generated-graphql"; import { useMarkerStrings } from "src/core/StashService"; import { SelectComponents } from "react-select/dist/declarations/src/components"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { objectTitle } from "src/core/files"; import { defaultMaxOptionsShown } from "src/core/config"; import { useDebounce } from "src/hooks/debounce"; @@ -108,7 +108,7 @@ const getSelectedItems = (selectedItems: OnChangeValue) => { const LimitedSelectMenu = ( props: MenuListProps> ) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const maxOptionsShown = configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; @@ -385,7 +385,7 @@ export const FilterSelect: React.FC = (props) => { case "groups": return ; case "galleries": - return ; + return ; default: return ; } @@ -496,6 +496,7 @@ export const ListSelect = (props: IListSelect) => { type DisableOption = Option & { isDisabled?: boolean; + className?: string; }; interface ICheckBoxSelectProps { @@ -510,7 +511,20 @@ export const CheckBoxSelect: React.FC = ({ onChange, }) => { const Option = (props: OptionProps) => ( - + , + HTMLDivElement + > + } + > ; + +// this needs to correspond to the CSS media query that overlaps the sidebar over content +const fixedSidebarMediaQuery = "only screen and (max-width: 767px)"; export const Sidebar: React.FC< PropsWithChildren<{ @@ -54,14 +60,79 @@ export const SidebarPane: React.FC< ); }; +export const SidebarToggleButton: React.FC<{ + onClick: () => void; +}> = ({ onClick }) => { + const intl = useIntl(); + return ( +
          + +
          + ); +}; + +export const SidebarPaneContent: React.FC<{ onSidebarToggle: () => void }> = ({ + onSidebarToggle, + children, +}) => { + return ( +
          + + {children} +
          + ); +}; + +interface IContext { + sectionOpen: SidebarSectionStates; + setSectionOpen: (section: string, open: boolean) => void; +} + +export const SidebarStateContext = React.createContext(null); + export const SidebarSection: React.FC< PropsWithChildren<{ text: React.ReactNode; className?: string; outsideCollapse?: React.ReactNode; onOpen?: () => void; + // used to store open/closed state in SidebarStateContext + sectionID?: string; }> -> = ({ className = "", text, outsideCollapse, onOpen, children }) => { +> = ({ + className = "", + text, + outsideCollapse, + onOpen, + sectionID = "", + children, +}) => { + // this is optional + const contextState = React.useContext(SidebarStateContext); + const openState = + !contextState || !sectionID + ? undefined + : contextState.sectionOpen[sectionID] ?? undefined; + + function onOpenInternal(open: boolean) { + if (contextState && sectionID) { + contextState.setSectionOpen(sectionID, open); + } + } + + useEffect(() => { + if (openState && onOpen) { + onOpen(); + } + }, [openState, onOpen]); + const collapseProps: Partial = { mountOnEnter: true, unmountOnExit: true, @@ -72,72 +143,14 @@ export const SidebarSection: React.FC< collapseProps={collapseProps} text={text} outsideCollapse={outsideCollapse} - onOpen={onOpen} + onOpenChanged={onOpenInternal} + open={openState} > {children} ); }; -export const SidebarIcon: React.FC = () => ( - <> - {/* From: https://iconduck.com/icons/19707/sidebar -MIT License -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */} - - - - - -); - -export const SidebarToolbar: React.FC<{ - onClose?: () => void; -}> = ({ onClose, children }) => { - const intl = useIntl(); - - return ( - - {onClose ? ( - - ) : null} - {children} - - ); -}; - // show sidebar by default if not on mobile export function defaultShowSidebar() { return !ScreenUtils.matchesMediaQuery(fixedSidebarMediaQuery); @@ -146,6 +159,7 @@ export function defaultShowSidebar() { export function useSidebarState(view?: View) { const [interfaceLocalForage, setInterfaceLocalForage] = useInterfaceLocalForage(); + const history = useHistory(); const { data: interfaceLocalForageData, loading } = interfaceLocalForage; @@ -154,6 +168,7 @@ export function useSidebarState(view?: View) { }, [view, interfaceLocalForageData]); const [showSidebar, setShowSidebar] = useState(); + const [sectionOpen, setSectionOpen] = useState(); // set initial state once loading is done useEffect(() => { @@ -168,7 +183,17 @@ export function useSidebarState(view?: View) { // only show sidebar by default on large screens setShowSidebar(!!viewConfig.showSidebar && defaultShowSidebar()); - }, [view, loading, showSidebar, viewConfig.showSidebar]); + setSectionOpen( + (history.location.state as { sectionOpen?: SidebarSectionStates }) + ?.sectionOpen || {} + ); + }, [ + view, + loading, + showSidebar, + viewConfig.showSidebar, + history.location.state, + ]); const onSetShowSidebar = useCallback( (show: boolean | ((prevState: boolean | undefined) => boolean)) => { @@ -190,9 +215,28 @@ export function useSidebarState(view?: View) { [showSidebar, setInterfaceLocalForage, view, viewConfig] ); + const onSetSectionOpen = useCallback( + (section: string, open: boolean) => { + const newSectionOpen = { ...sectionOpen, [section]: open }; + setSectionOpen(newSectionOpen); + if (view === undefined) return; + + history.replace({ + ...history.location, + state: { + ...(history.location.state as {}), + sectionOpen: newSectionOpen, + }, + }); + }, + [sectionOpen, view, history] + ); + return { showSidebar: showSidebar ?? defaultShowSidebar(), + sectionOpen: sectionOpen || {}, setShowSidebar: onSetShowSidebar, + setSectionOpen: onSetSectionOpen, loading: showSidebar === undefined, }; } diff --git a/ui/v2.5/src/components/Shared/StashBoxIDSearchModal.tsx b/ui/v2.5/src/components/Shared/StashBoxIDSearchModal.tsx new file mode 100644 index 000000000..47683dc3c --- /dev/null +++ b/ui/v2.5/src/components/Shared/StashBoxIDSearchModal.tsx @@ -0,0 +1,525 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { Form, Button, Row, Col, Badge, InputGroup } from "react-bootstrap"; +import { FormattedMessage, useIntl } from "react-intl"; +import { faSearch } from "@fortawesome/free-solid-svg-icons"; +import * as GQL from "src/core/generated-graphql"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { stashboxDisplayName } from "src/utils/stashbox"; +import { TruncatedText } from "src/components/Shared/TruncatedText"; +import TextUtils from "src/utils/text"; +import GenderIcon from "src/components/Performers/GenderIcon"; +import { CountryFlag } from "src/components/Shared/CountryFlag"; +import { Icon } from "src/components/Shared/Icon"; +import { + stashBoxPerformerQuery, + stashBoxSceneQuery, + stashBoxStudioQuery, + stashBoxTagQuery, +} from "src/core/StashService"; +import { useToast } from "src/hooks/Toast"; +import { stringToGender } from "src/utils/gender"; + +type SearchResultItem = + | GQL.ScrapedPerformerDataFragment + | GQL.ScrapedSceneDataFragment + | GQL.ScrapedStudioDataFragment + | GQL.ScrapedSceneTagDataFragment; + +export type StashBoxEntityType = "performer" | "scene" | "studio" | "tag"; + +interface IProps { + entityType: StashBoxEntityType; + stashBoxes: GQL.StashBox[]; + excludedStashBoxEndpoints?: string[]; + onSelectItem: (item?: GQL.StashIdInput) => void; + initialQuery?: string; +} + +const CLASSNAME = "StashBoxIDSearchModal"; +const CLASSNAME_LIST = `${CLASSNAME}-list`; +const CLASSNAME_LIST_CONTAINER = `${CLASSNAME_LIST}-container`; + +interface IHasRemoteSiteID { + remote_site_id?: string | null; +} + +// Shared component for rendering images +const SearchResultImage: React.FC<{ imageUrl?: string | null }> = ({ + imageUrl, +}) => { + if (!imageUrl) return null; + + return ( +
          + +
          + ); +}; + +// Shared component for rendering tags +const SearchResultTags: React.FC<{ + tags?: GQL.ScrapedTag[] | null; +}> = ({ tags }) => { + if (!tags || tags.length === 0) return null; + + return ( + + + {tags.map((tag) => ( + + {tag.name} + + ))} + + + ); +}; + +// Performer Result Component +interface IPerformerResultProps { + performer: GQL.ScrapedPerformerDataFragment; +} + +const PerformerSearchResultDetails: React.FC = ({ + performer, +}) => { + const age = performer?.birthdate + ? TextUtils.age(performer.birthdate, performer.death_date) + : undefined; + + return ( +
          + + +
          +

          + {performer.name} + {performer.disambiguation && ( + + {` (${performer.disambiguation})`} + + )} +

          +
          + {performer.gender && ( + + + + )} + {age && ( + + {`${age} `} + + + )} +
          + {performer.country && ( + + + + )} +
          +
          + + + + + + +
          + ); +}; + +export const PerformerSearchResult: React.FC = ({ + performer, +}) => { + return ( +
          + +
          + ); +}; + +// Scene Result Component +interface ISceneResultProps { + scene: GQL.ScrapedSceneDataFragment; +} + +const SceneSearchResultDetails: React.FC = ({ scene }) => { + return ( +
          + + +
          +

          + {scene.title} + {scene.code && ( + {` (${scene.code})`} + )} +

          +
          + {scene.studio?.name && {scene.studio.name}} + {scene.date && ( + {` • ${scene.date}`} + )} +
          + {scene.performers && scene.performers.length > 0 && ( +
          + {scene.performers.map((p) => p.name).join(", ")} +
          + )} +
          +
          + + + + + + +
          + ); +}; + +export const SceneSearchResult: React.FC = ({ scene }) => { + return ( +
          + +
          + ); +}; + +// Studio Result Component +interface IStudioResultProps { + studio: GQL.ScrapedStudioDataFragment; +} + +const StudioSearchResultDetails: React.FC = ({ + studio, +}) => { + return ( +
          + + +
          +

          + {studio.name} +

          + {studio.parent?.name && ( +
          + {studio.parent.name} +
          + )} + {studio.urls && studio.urls.length > 0 && ( +
          {studio.urls[0]}
          + )} +
          +
          +
          + ); +}; + +export const StudioSearchResult: React.FC = ({ + studio, +}) => { + return ( +
          + +
          + ); +}; + +// Tag Result Component +interface ITagResultProps { + tag: GQL.ScrapedSceneTagDataFragment; +} + +export const TagSearchResult: React.FC = ({ tag }) => { + return ( +
          +
          + +
          +

          + {tag.name} +

          +
          +
          +
          +
          + ); +}; + +// Helper to get entity type message id for i18n +function getEntityTypeMessageId(entityType: StashBoxEntityType): string { + switch (entityType) { + case "performer": + return "performer"; + case "scene": + return "scene"; + case "studio": + return "studio"; + case "tag": + return "tag"; + } +} + +// Helper to get the "found" message id based on entity type +function getFoundMessageId(entityType: StashBoxEntityType): string { + switch (entityType) { + case "performer": + return "dialogs.performers_found"; + case "scene": + return "dialogs.scenes_found"; + case "studio": + return "dialogs.studios_found"; + case "tag": + return "dialogs.tags_found"; + } +} + +// Main Modal Component +export const StashBoxIDSearchModal: React.FC = ({ + entityType, + stashBoxes, + excludedStashBoxEndpoints = [], + onSelectItem, + initialQuery = "", +}) => { + const intl = useIntl(); + const Toast = useToast(); + const inputRef = useRef(null); + + const [selectedStashBox, setSelectedStashBox] = useState( + null + ); + const [query, setQuery] = useState(initialQuery); + const [results, setResults] = useState( + undefined + ); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (stashBoxes.length > 0) { + setSelectedStashBox(stashBoxes[0]); + } + }, [stashBoxes]); + + useEffect(() => inputRef.current?.focus(), []); + + const doSearch = useCallback(async () => { + if (!selectedStashBox || !query) { + return; + } + + setLoading(true); + setResults([]); + + try { + switch (entityType) { + case "performer": { + const queryData = await stashBoxPerformerQuery( + query, + selectedStashBox.endpoint + ); + setResults(queryData.data?.scrapeSinglePerformer ?? []); + break; + } + case "scene": { + const queryData = await stashBoxSceneQuery( + query, + selectedStashBox.endpoint + ); + setResults(queryData.data?.scrapeSingleScene ?? []); + break; + } + case "studio": { + const queryData = await stashBoxStudioQuery( + query, + selectedStashBox.endpoint + ); + setResults(queryData.data?.scrapeSingleStudio ?? []); + break; + } + case "tag": { + const queryData = await stashBoxTagQuery( + query, + selectedStashBox.endpoint + ); + setResults(queryData.data?.scrapeSingleTag ?? []); + break; + } + } + } catch (error) { + Toast.error(error); + } finally { + setLoading(false); + } + }, [query, selectedStashBox, Toast, entityType]); + + function handleItemClick(item: IHasRemoteSiteID) { + if (selectedStashBox && item.remote_site_id) { + onSelectItem({ + endpoint: selectedStashBox.endpoint, + stash_id: item.remote_site_id, + }); + } else { + onSelectItem(undefined); + } + } + + function handleClose() { + onSelectItem(undefined); + } + + function renderResultItem(item: SearchResultItem) { + switch (entityType) { + case "performer": + return ( + + ); + case "scene": + return ( + + ); + case "studio": + return ( + + ); + case "tag": + return ( + + ); + } + } + + function renderResults() { + if (!results || results.length === 0) { + return null; + } + + return ( +
          +
          + +
          +
            + {results.map((item, i) => ( +
          • handleItemClick(item)}> + {renderResultItem(item)} +
          • + ))} +
          +
          + ); + } + + const entityTypeDisplayName = intl.formatMessage({ + id: getEntityTypeMessageId(entityType), + }); + + return ( + +
          + + + + + { + const box = stashBoxes.find( + (b) => b.endpoint === e.currentTarget.value + ); + if (box) { + setSelectedStashBox(box); + } + }} + > + {stashBoxes.map((box, index) => ( + + ))} + + + + {selectedStashBox && + excludedStashBoxEndpoints.includes(selectedStashBox.endpoint) && ( + + + + )} + + + setQuery(e.currentTarget.value)} + value={query} + placeholder={intl.formatMessage( + { id: "stashbox_search.placeholder_name_or_id" }, + { entityType: entityTypeDisplayName } + )} + className="text-input" + ref={inputRef} + onKeyPress={(e: React.KeyboardEvent) => + e.key === "Enter" && doSearch() + } + /> + + + + + + {loading ? ( +
          + +
          + ) : results && results.length > 0 ? ( + renderResults() + ) : ( + results !== undefined && + results.length === 0 && ( +
          + +
          + ) + )} +
          +
          + ); +}; + +export default StashBoxIDSearchModal; diff --git a/ui/v2.5/src/components/Shared/StashID.tsx b/ui/v2.5/src/components/Shared/StashID.tsx index 00bddf58e..847dd7ab2 100644 --- a/ui/v2.5/src/components/Shared/StashID.tsx +++ b/ui/v2.5/src/components/Shared/StashID.tsx @@ -1,16 +1,16 @@ import React, { useMemo } from "react"; import { StashId } from "src/core/generated-graphql"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { getStashboxBase } from "src/utils/stashbox"; import { ExternalLink } from "./ExternalLink"; -export type LinkType = "performers" | "scenes" | "studios"; +export type LinkType = "performers" | "scenes" | "studios" | "tags"; export const StashIDPill: React.FC<{ stashID: Pick; linkType: LinkType; }> = ({ stashID, linkType }) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const { endpoint, stash_id } = stashID; diff --git a/ui/v2.5/src/components/Shared/StringListInput.tsx b/ui/v2.5/src/components/Shared/StringListInput.tsx index 768f282a0..89401884c 100644 --- a/ui/v2.5/src/components/Shared/StringListInput.tsx +++ b/ui/v2.5/src/components/Shared/StringListInput.tsx @@ -1,5 +1,5 @@ -import { faMinus } from "@fortawesome/free-solid-svg-icons"; -import React, { ComponentType } from "react"; +import { faGripVertical, faMinus } from "@fortawesome/free-solid-svg-icons"; +import React, { ComponentType, useState } from "react"; import { Button, Form, InputGroup } from "react-bootstrap"; import { Icon } from "./Icon"; @@ -25,6 +25,8 @@ export interface IStringListInputProps { errors?: string; errorIdx?: number[]; readOnly?: boolean; + // defaults to true if not set + orderable?: boolean; } export const StringInput: React.FC = ({ @@ -51,6 +53,9 @@ export const StringListInput: React.FC = (props) => { const Input = props.inputComponent ?? StringInput; const AppendComponent = props.appendComponent; const values = props.value.concat(""); + const [draggedIdx, setDraggedIdx] = useState(null); + + const { orderable = true } = props; function valueChanged(idx: number, value: string) { const newValues = props.value.slice(); @@ -70,12 +75,46 @@ export const StringListInput: React.FC = (props) => { props.setValue(newValues); } + function handleDragStart(event: React.DragEvent, idx: number) { + event.dataTransfer.dropEffect = "move"; + setDraggedIdx(idx); + } + + function handleDragOver(e: React.DragEvent, idx: number) { + e.dataTransfer.dropEffect = "move"; + e.preventDefault(); + + if ( + draggedIdx === null || + draggedIdx === idx || + idx === values.length - 1 + ) { + return; + } + + const newValues = [...props.value]; + const draggedValue = newValues[draggedIdx]; + newValues.splice(draggedIdx, 1); + newValues.splice(idx, 0, draggedValue); + + props.setValue(newValues); + setDraggedIdx(idx); + } + + function handleDragEnd() { + setDraggedIdx(null); + } + return ( <>
          {values.map((v, i) => ( - + handleDragOver(e, i)} + > valueChanged(i, value)} @@ -85,11 +124,24 @@ export const StringListInput: React.FC = (props) => { /> {AppendComponent && } + {!props.readOnly && values.length > 2 && orderable && ( + + )} {!props.readOnly && ( diff --git a/ui/v2.5/src/components/Shared/TagLink.tsx b/ui/v2.5/src/components/Shared/TagLink.tsx index a59ac83cb..e572a76f8 100644 --- a/ui/v2.5/src/components/Shared/TagLink.tsx +++ b/ui/v2.5/src/components/Shared/TagLink.tsx @@ -36,7 +36,7 @@ const SortNameLinkComponent: React.FC = ({ {children} @@ -55,7 +55,7 @@ const CommonLinkComponent: React.FC = ({ children, }) => { return ( - + {children} ); diff --git a/ui/v2.5/src/components/Shared/styles.scss b/ui/v2.5/src/components/Shared/styles.scss index 10e381bd8..f72bbbeea 100644 --- a/ui/v2.5/src/components/Shared/styles.scss +++ b/ui/v2.5/src/components/Shared/styles.scss @@ -62,10 +62,23 @@ padding: 0; row-gap: 0.5rem; - .btn { + > .btn { margin-right: 0.5rem; white-space: nowrap; } + + > .btn-group { + margin-right: 0.5rem; + + .btn { + margin-right: 0; + } + + // Show caret on split button dropdown toggle + .dropdown-toggle-split::after { + content: ""; + } + } } .col-md-8 .details-edit div:nth-last-child(2), @@ -155,6 +168,15 @@ } .scrape-dialog { + .column-label { + color: $muted-gray; + font-size: 0.85em; + } + + .string-list-input { + width: 100%; + } + .modal-content .dialog-container { max-height: calc(100vh - 14rem); overflow-y: auto; @@ -275,6 +297,10 @@ button.collapse-button { opacity: 0; width: 1.2rem; + &:checked { + opacity: 0.75; + } + @media (hover: none), (pointer: coarse) { // always show card controls when hovering not supported opacity: 0.25; @@ -288,10 +314,6 @@ button.collapse-button { .card-check { padding-left: 15px; - &:checked { - opacity: 0.75; - } - @media (hover: none), (pointer: coarse) { // and make it bigger when hovering not supported width: 1.5rem; @@ -305,6 +327,34 @@ button.collapse-button { } } +.search-item-check, +.wall-item-check { + height: 1.2rem; + width: 1.2rem; +} + +// Wall item checkbox styles +.wall-item-check { + left: 0.5rem; + opacity: 0; + position: absolute; + top: 0.5rem; + z-index: 10; + + &:checked { + opacity: 0.75; + } + + @media (hover: none) { + opacity: 0.25; + } +} + +.wall-item:hover .wall-item-check { + opacity: 0.75; + transition: opacity 0.5s; +} + .TruncatedText { -webkit-box-orient: vertical; display: -webkit-box; @@ -391,8 +441,37 @@ button.collapse-button { opacity: 0.5; } -.string-list-input .input-group { - margin-bottom: 0.35rem; +.string-list-input { + .form-group { + margin-bottom: 0; + } + + .input-group { + margin-bottom: 0.35rem; + + &:last-child { + margin-bottom: 0; + } + } + + .btn.drag-handle { + display: inline-block; + margin: -0.25em 0.25em -0.25em -0.25em; + padding: 0.25em 0.5em 0.25em; + + &:not(:disabled):not(.disabled) { + cursor: move; + } + + &:hover, + &:active, + &:focus, + &:focus:active { + background-color: initial; + border-color: initial; + box-shadow: initial; + } + } } .bulk-update-text-input { @@ -712,6 +791,10 @@ button.btn.favorite-button { .custom-fields { width: 100%; + + .detail-item { + max-width: 100%; + } } .custom-fields .detail-item .detail-item-title { @@ -721,6 +804,14 @@ button.btn.favorite-button { white-space: nowrap; } +.custom-fields .detail-item .detail-item-value { + word-break: break-word; + + .TruncatedText { + white-space: pre-line; + } +} + .custom-fields-input > .collapse-button { font-weight: 700; } @@ -755,8 +846,6 @@ button.btn.favorite-button { } } -$sidebar-width: 250px; - .sidebar-pane { display: flex; @@ -807,7 +896,7 @@ $sidebar-width: 250px; } } - @include media-breakpoint-up(xl) { + @include media-breakpoint-up(md) { transition: margin-left 0.1s; &:not(.hide-sidebar) { @@ -818,27 +907,41 @@ $sidebar-width: 250px; } @include media-breakpoint-down(xs) { .sidebar { - width: 100%; - } - - &.hide-sidebar .sidebar { - margin-left: -100%; - } - - &.hide-sidebar > :nth-child(2) { - width: 100%; - } - } - @include media-breakpoint-down(xs) { - display: block; - - .sidebar { - margin-bottom: $navbar-height; margin-top: 0; } } } +.sidebar-toggle-button-container { + height: 100%; + position: absolute; + + .sidebar-toggle-button { + border-bottom: 1px solid $secondary; + border-bottom-left-radius: 0; + border-bottom-right-radius: 10px; + border-right: 1px solid $secondary; + border-top: 1px solid $secondary; + border-top-left-radius: 0; + border-top-right-radius: 10px; + margin-left: -15px; + opacity: 0.5; + position: sticky; + top: calc($navbar-height + 0.5rem); + z-index: 10; + + @include media-breakpoint-down(sm) { + top: 0.5rem; + } + } +} + +.sidebar-pane:not(.hide-sidebar) .sidebar-toggle-button-container { + .sidebar-toggle-button { + margin-left: -0.5rem; + } +} + .sidebar-toolbar { // TODO - use different colours for sidebar and toolbar background-color: $body-bg; @@ -886,13 +989,25 @@ $sidebar-width: 250px; $sticky-header-height: calc(50px + 3.3rem); // special case for sidebar in details view +.detail-body .sidebar-toggle-button-container .sidebar-toggle-button { + top: calc($sticky-header-height + 0.5rem); + + @include media-breakpoint-down(sm) { + top: 0.5rem; + } +} + .detail-body { + .sidebar-pane { + position: sticky; + top: calc($sticky-detail-header-height + $navbar-height); + } + .sidebar { // required for sticky to work align-self: flex-start; // take a further 15px padding to match the detail body - height: calc(100vh - $sticky-header-height - 15px); margin-top: -15px; max-height: calc(100vh - $sticky-header-height - 15px); overflow-y: auto; @@ -906,48 +1021,186 @@ $sticky-header-height: calc(50px + 3.3rem); } } + .sidebar-pane:not(.hide-sidebar) .sidebar { + height: calc(100vh - $sticky-header-height - 15px); + } + .sidebar-pane.hide-sidebar .sidebar { left: -$sidebar-width; margin-left: calc(-15px - $sidebar-width); } // on smaller viewports we want the sidebar to overlap content - @include media-breakpoint-down(lg) { + @include media-breakpoint-down(sm) { .sidebar-pane:not(.hide-sidebar) .sidebar { margin-right: -$sidebar-width; } - .sidebar-pane > :nth-child(2) { + .sidebar-pane > .sidebar-pane-content { transition: none; } } @include media-breakpoint-down(xs) { - .sidebar { - flex: 100% 0 0; - height: calc(100vh - 4rem); - max-height: calc(100vh - 4rem); + .sidebar-pane { top: 0; } - .sidebar-pane:not(.hide-sidebar) .sidebar { - margin-right: -100%; - } - - .sidebar-pane.hide-sidebar .sidebar { - display: none; + .sidebar { + // flex: 100% 0 0; + height: calc(100vh - $navbar-height); + max-height: calc(100vh - $navbar-height); + top: 0; } } - @include media-breakpoint-up(xl) { + @include media-breakpoint-up(md) { .sidebar-pane:not(.hide-sidebar) { > :nth-child(2) { margin-left: 0; } } + } - .sidebar-pane.hide-sidebar { - > :nth-child(2) { - padding-left: 15px; - } + .sidebar-pane.hide-sidebar { + > :nth-child(2) { + padding-left: 15px; } } } + +// Duration slider styles +.duration-slider-container { + padding: 0.5rem 0 1rem; + width: 100%; +} + +.double-range-input-labels { + color: $text-color; + display: flex; + font-size: 0.875rem; + font-weight: 500; + justify-content: space-between; + margin-bottom: 0.5rem; + padding: 0 0.25rem; + + input[type="text"] { + &:first-child { + text-align: left; + } + + &:last-child { + text-align: right; + } + } +} + +.double-range-sliders { + height: 22px; + position: relative; +} + +.double-range-slider { + pointer-events: none; + position: absolute; + width: 100%; + + &::-webkit-slider-thumb { + appearance: none; + background-color: $primary; + border: 2px solid $primary; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + cursor: pointer; + height: 18px; + pointer-events: all; + position: relative; + width: 18px; + } + + &::-moz-range-thumb { + appearance: none; + background-color: $primary; + border: 2px solid $primary; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + cursor: pointer; + height: 18px; + pointer-events: all; + position: relative; + width: 18px; + } + + &::-ms-thumb { + appearance: none; + background-color: $primary; + border: 2px solid $primary; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + cursor: pointer; + height: 18px; + pointer-events: all; + position: relative; + width: 18px; + } +} + +.double-range-slider-min { + z-index: 1; +} + +input[type="range"].double-range-slider-max { + z-index: 2; + + // combining these into one rule doesn't work for some reason + &::-webkit-slider-runnable-track { + background: transparent; + } + + &::-moz-range-track { + background: transparent; + } + + &::-ms-track { + background: transparent; + } +} + +// Label offset for buttons that need to align with form fields +.ml-label { + @include media-breakpoint-up(sm) { + // sm: label is 3 of 12 columns = 25%, plus partial gutter + margin-left: calc(25% + 7.5px); + } + @include media-breakpoint-up(xl) { + // xl: label is 2 of 12 columns = 16.667%, plus partial gutter + margin-left: calc(16.667% + 7.5px); + } +} + +// StashBox Search Modal +.StashBoxSearchModal { + &-list { + list-style: none; + padding: 0; + + li { + border-radius: 0.25rem; + cursor: pointer; + margin-bottom: 0.5rem; + padding: 0.5rem; + transition: background-color 0.2s; + + &:hover { + background-color: rgba(138, 155, 168, 0.1); + } + + &.selected { + background-color: #e7f3ff; + } + } + } + + &-list-container { + max-height: 60vh; + overflow-y: auto; + } +} diff --git a/ui/v2.5/src/components/Stats.tsx b/ui/v2.5/src/components/Stats.tsx index b035fe66b..6b5a49d83 100644 --- a/ui/v2.5/src/components/Stats.tsx +++ b/ui/v2.5/src/components/Stats.tsx @@ -4,8 +4,16 @@ import { FormattedMessage, FormattedNumber } from "react-intl"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import TextUtils from "src/utils/text"; import { FileSize } from "./Shared/FileSize"; +import { useConfigurationContext } from "src/hooks/Config"; export const Stats: React.FC = () => { + const { configuration } = useConfigurationContext(); + const { sfwContentMode } = configuration.interface; + + const oCountID = sfwContentMode + ? "stats.total_o_count_sfw" + : "stats.total_o_count"; + const { data, error, loading } = useStats(); if (error) return {error.message}; @@ -111,7 +119,7 @@ export const Stats: React.FC = () => {

          - +

          diff --git a/ui/v2.5/src/components/Studios/EditStudiosDialog.tsx b/ui/v2.5/src/components/Studios/EditStudiosDialog.tsx new file mode 100644 index 000000000..293a8dfb3 --- /dev/null +++ b/ui/v2.5/src/components/Studios/EditStudiosDialog.tsx @@ -0,0 +1,245 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { Col, Form, Row } from "react-bootstrap"; +import { FormattedMessage, useIntl } from "react-intl"; +import { useBulkStudioUpdate } from "src/core/StashService"; +import * as GQL from "src/core/generated-graphql"; +import { ModalComponent } from "../Shared/Modal"; +import { useToast } from "src/hooks/Toast"; +import { MultiSet } from "../Shared/MultiSet"; +import { RatingSystem } from "../Shared/Rating/RatingSystem"; +import { + getAggregateInputValue, + getAggregateState, + getAggregateStateObject, +} from "src/utils/bulkUpdate"; +import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox"; +import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput"; +import { faPencilAlt } from "@fortawesome/free-solid-svg-icons"; +import * as FormUtils from "src/utils/form"; +import { StudioSelect } from "../Shared/Select"; + +interface IListOperationProps { + selected: GQL.SlimStudioDataFragment[]; + onClose: (applied: boolean) => void; +} + +const studioFields = ["favorite", "rating100", "details", "ignore_auto_tag"]; + +export const EditStudiosDialog: React.FC = ( + props: IListOperationProps +) => { + const intl = useIntl(); + const Toast = useToast(); + + const [updateInput, setUpdateInput] = useState({ + ids: props.selected.map((studio) => { + return studio.id; + }), + }); + + const [tagIds, setTagIds] = useState({ + mode: GQL.BulkUpdateIdMode.Add, + }); + + const [updateStudios] = useBulkStudioUpdate(); + + // Network state + const [isUpdating, setIsUpdating] = useState(false); + + const aggregateState = useMemo(() => { + const updateState: Partial = {}; + const state = props.selected; + let updateTagIds: string[] = []; + let first = true; + + state.forEach((studio: GQL.SlimStudioDataFragment) => { + getAggregateStateObject(updateState, studio, studioFields, first); + + // studio data fragment doesn't have parent_id, so handle separately + updateState.parent_id = getAggregateState( + updateState.parent_id, + studio.parent_studio?.id, + first + ); + + const studioTagIDs = (studio.tags ?? []).map((p) => p.id).sort(); + + updateTagIds = getAggregateState(updateTagIds, studioTagIDs, first) ?? []; + + first = false; + }); + + return { state: updateState, tagIds: updateTagIds }; + }, [props.selected]); + + // update initial state from aggregate + useEffect(() => { + setUpdateInput((current) => ({ ...current, ...aggregateState.state })); + }, [aggregateState]); + + function setUpdateField(input: Partial) { + setUpdateInput((current) => ({ ...current, ...input })); + } + + function getStudioInput(): GQL.BulkStudioUpdateInput { + const studioInput: GQL.BulkStudioUpdateInput = { + ...updateInput, + tag_ids: tagIds, + }; + + // we don't have unset functionality for the rating star control + // so need to determine if we are setting a rating or not + studioInput.rating100 = getAggregateInputValue( + updateInput.rating100, + aggregateState.state.rating100 + ); + + return studioInput; + } + + async function onSave() { + setIsUpdating(true); + try { + await updateStudios({ + variables: { + input: getStudioInput(), + }, + }); + Toast.success( + intl.formatMessage( + { id: "toast.updated_entity" }, + { + entity: intl.formatMessage({ id: "studios" }).toLocaleLowerCase(), + } + ) + ); + props.onClose(true); + } catch (e) { + Toast.error(e); + } + setIsUpdating(false); + } + + function renderTextField( + name: string, + value: string | undefined | null, + setter: (newValue: string | undefined) => void, + area: boolean = false + ) { + return ( + + + + + setter(newValue)} + unsetDisabled={props.selected.length < 2} + as={area ? "textarea" : undefined} + /> + + ); + } + + function render() { + return ( + props.onClose(false), + text: intl.formatMessage({ id: "actions.cancel" }), + variant: "secondary", + }} + isRunning={isUpdating} + > + + {FormUtils.renderLabel({ + title: intl.formatMessage({ id: "parent_studio" }), + })} + + + setUpdateField({ + parent_id: items.length > 0 ? items[0]?.id : undefined, + }) + } + ids={updateInput.parent_id ? [updateInput.parent_id] : []} + isDisabled={isUpdating} + menuPortalTarget={document.body} + /> + + + + {FormUtils.renderLabel({ + title: intl.formatMessage({ id: "rating" }), + })} + + + setUpdateField({ rating100: value ?? undefined }) + } + disabled={isUpdating} + /> + + +
          + + setUpdateField({ favorite: checked })} + checked={updateInput.favorite ?? undefined} + label={intl.formatMessage({ id: "favourite" })} + /> + + + + + + + setTagIds((v) => ({ ...v, ids: itemIDs }))} + onSetMode={(newMode) => + setTagIds((v) => ({ ...v, mode: newMode })) + } + existingIds={aggregateState.tagIds ?? []} + ids={tagIds.ids ?? []} + mode={tagIds.mode} + menuPortalTarget={document.body} + /> + + + {renderTextField( + "details", + updateInput.details, + (newValue) => setUpdateField({ details: newValue }), + true + )} + + + + setUpdateField({ ignore_auto_tag: checked }) + } + checked={updateInput.ignore_auto_tag ?? undefined} + /> + +
          +
          + ); + } + + return render(); +}; diff --git a/ui/v2.5/src/components/Studios/StudioCard.tsx b/ui/v2.5/src/components/Studios/StudioCard.tsx index 5cd1cc209..87c9b9528 100644 --- a/ui/v2.5/src/components/Studios/StudioCard.tsx +++ b/ui/v2.5/src/components/Studios/StudioCard.tsx @@ -3,6 +3,7 @@ import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; import NavUtils from "src/utils/navigation"; import { GridCard } from "src/components/Shared/GridCard/GridCard"; +import { PatchComponent } from "src/patch"; import { HoverPopover } from "../Shared/HoverPopover"; import { Icon } from "../Shared/Icon"; import { TagLink } from "../Shared/TagLink"; @@ -13,6 +14,7 @@ import { RatingBanner } from "../Shared/RatingBanner"; import { FavoriteIcon } from "../Shared/FavoriteIcon"; import { useStudioUpdate } from "src/core/StashService"; import { faTag } from "@fortawesome/free-solid-svg-icons"; +import { OCounterButton } from "../Shared/CountButton"; interface IProps { studio: GQL.StudioDataFragment; @@ -69,171 +71,182 @@ function maybeRenderChildren(studio: GQL.StudioDataFragment) { } } -export const StudioCard: React.FC = ({ - studio, - cardWidth, - hideParent, - selecting, - selected, - zoomIndex, - onSelectedChanged, -}) => { - const [updateStudio] = useStudioUpdate(); +export const StudioCard: React.FC = PatchComponent( + "StudioCard", + ({ + studio, + cardWidth, + hideParent, + selecting, + selected, + zoomIndex, + onSelectedChanged, + }) => { + const [updateStudio] = useStudioUpdate(); - function onToggleFavorite(v: boolean) { - if (studio.id) { - updateStudio({ - variables: { - input: { - id: studio.id, - favorite: v, + function onToggleFavorite(v: boolean) { + if (studio.id) { + updateStudio({ + variables: { + input: { + id: studio.id, + favorite: v, + }, }, - }, - }); + }); + } } - } - function maybeRenderScenesPopoverButton() { - if (!studio.scene_count) return; + function maybeRenderScenesPopoverButton() { + if (!studio.scene_count) return; - return ( - - ); - } - - function maybeRenderImagesPopoverButton() { - if (!studio.image_count) return; - - return ( - - ); - } - - function maybeRenderGalleriesPopoverButton() { - if (!studio.gallery_count) return; - - return ( - - ); - } - - function maybeRenderGroupsPopoverButton() { - if (!studio.group_count) return; - - return ( - - ); - } - - function maybeRenderPerformersPopoverButton() { - if (!studio.performer_count) return; - - return ( - - ); - } - - function maybeRenderTagPopoverButton() { - if (studio.tags.length <= 0) return; - - const popoverContent = studio.tags.map((tag) => ( - - )); - - return ( - - - - ); - } - - function maybeRenderPopoverButtonGroup() { - if ( - studio.scene_count || - studio.image_count || - studio.gallery_count || - studio.group_count || - studio.performer_count || - studio.tags.length > 0 - ) { return ( - <> -
          - - {maybeRenderScenesPopoverButton()} - {maybeRenderGroupsPopoverButton()} - {maybeRenderImagesPopoverButton()} - {maybeRenderGalleriesPopoverButton()} - {maybeRenderPerformersPopoverButton()} - {maybeRenderTagPopoverButton()} - - + ); } - } - return ( - - } - details={ -
          - {maybeRenderParent(studio, hideParent)} - {maybeRenderChildren(studio)} - -
          - } - overlays={ - onToggleFavorite(v)} - size="2x" - className="hide-not-favorite" + ); + } + + function maybeRenderGalleriesPopoverButton() { + if (!studio.gallery_count) return; + + return ( + + ); + } + + function maybeRenderGroupsPopoverButton() { + if (!studio.group_count) return; + + return ( + + ); + } + + function maybeRenderPerformersPopoverButton() { + if (!studio.performer_count) return; + + return ( + + ); + } + + function maybeRenderTagPopoverButton() { + if (studio.tags.length <= 0) return; + + const popoverContent = studio.tags.map((tag) => ( + + )); + + return ( + + + + ); + } + + function maybeRenderOCounter() { + if (!studio.o_counter) return; + + return ; + } + + function maybeRenderPopoverButtonGroup() { + if ( + studio.scene_count || + studio.image_count || + studio.gallery_count || + studio.group_count || + studio.performer_count || + studio.o_counter || + studio.tags.length > 0 + ) { + return ( + <> +
          + + {maybeRenderScenesPopoverButton()} + {maybeRenderGroupsPopoverButton()} + {maybeRenderImagesPopoverButton()} + {maybeRenderGalleriesPopoverButton()} + {maybeRenderPerformersPopoverButton()} + {maybeRenderTagPopoverButton()} + {maybeRenderOCounter()} + + + ); } - popovers={maybeRenderPopoverButtonGroup()} - selected={selected} - selecting={selecting} - onSelectedChanged={onSelectedChanged} - /> - ); -}; + } + + return ( + + } + details={ +
          + {maybeRenderParent(studio, hideParent)} + {maybeRenderChildren(studio)} + +
          + } + overlays={ + onToggleFavorite(v)} + size="2x" + className="hide-not-favorite" + /> + } + popovers={maybeRenderPopoverButtonGroup()} + selected={selected} + selecting={selecting} + onSelectedChanged={onSelectedChanged} + /> + ); + } +); diff --git a/ui/v2.5/src/components/Studios/StudioCardGrid.tsx b/ui/v2.5/src/components/Studios/StudioCardGrid.tsx index 311d4e9d6..8edba4da2 100644 --- a/ui/v2.5/src/components/Studios/StudioCardGrid.tsx +++ b/ui/v2.5/src/components/Studios/StudioCardGrid.tsx @@ -5,6 +5,7 @@ import { useContainerDimensions, } from "../Shared/GridCard/GridCard"; import { StudioCard } from "./StudioCard"; +import { PatchComponent } from "src/patch"; interface IStudioCardGrid { studios: GQL.StudioDataFragment[]; @@ -16,32 +17,29 @@ interface IStudioCardGrid { const zoomWidths = [280, 340, 420, 560]; -export const StudioCardGrid: React.FC = ({ - studios, - fromParent, - selectedIds, - zoomIndex, - onSelectChange, -}) => { - const [componentRef, { width: containerWidth }] = useContainerDimensions(); - const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); +export const StudioCardGrid: React.FC = PatchComponent( + "StudioCardGrid", + ({ studios, fromParent, selectedIds, zoomIndex, onSelectChange }) => { + const [componentRef, { width: containerWidth }] = useContainerDimensions(); + const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); - return ( -
          - {studios.map((studio) => ( - 0} - selected={selectedIds.has(studio.id)} - onSelectedChanged={(selected: boolean, shiftKey: boolean) => - onSelectChange(studio.id, selected, shiftKey) - } - /> - ))} -
          - ); -}; + return ( +
          + {studios.map((studio) => ( + 0} + selected={selectedIds.has(studio.id)} + onSelectedChanged={(selected: boolean, shiftKey: boolean) => + onSelectChange(studio.id, selected, shiftKey) + } + /> + ))} +
          + ); + } +); diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx index 2140af340..2edc53fe1 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx @@ -18,7 +18,7 @@ import { ModalComponent } from "src/components/Shared/Modal"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { ErrorMessage } from "src/components/Shared/ErrorMessage"; import { useToast } from "src/hooks/Toast"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { StudioScenesPanel } from "./StudioScenesPanel"; import { StudioGalleriesPanel } from "./StudioGalleriesPanel"; import { StudioImagesPanel } from "./StudioImagesPanel"; @@ -47,6 +47,8 @@ import { FavoriteIcon } from "src/components/Shared/FavoriteIcon"; import { ExternalLinkButtons } from "src/components/Shared/ExternalLinksButton"; import { AliasList } from "src/components/Shared/DetailsPage/AliasList"; import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage"; +import { goBackOrReplace } from "src/utils/history"; +import { OCounterButton } from "src/components/Shared/CountButton"; interface IProps { studio: GQL.StudioDataFragment; @@ -263,7 +265,7 @@ const StudioPage: React.FC = ({ studio, tabKey }) => { const intl = useIntl(); // Configuration settings - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const uiConfig = configuration?.ui; const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; const enableBackgroundImage = uiConfig?.enableStudioBackgroundImage ?? false; @@ -286,11 +288,6 @@ const StudioPage: React.FC = ({ studio, tabKey }) => { const showAllCounts = uiConfig?.showChildStudioContent; - // make array of url so that it doesn't re-render on every change - const urls = useMemo(() => { - return studio?.url ? [studio.url] : []; - }, [studio.url]); - const studioImage = useMemo(() => { const existingPath = studio.image_path; if (isEditing) { @@ -378,7 +375,7 @@ const StudioPage: React.FC = ({ studio, tabKey }) => { return; } - history.goBack(); + goBackOrReplace(history, "/studios"); } function renderDeleteAlert() { @@ -470,17 +467,22 @@ const StudioPage: React.FC = ({ studio, tabKey }) => { favorite={studio.favorite} onToggleFavorite={(v) => setFavorite(v)} /> - + - setRating(value)} - clickToRate - withoutContext - /> +
          + setRating(value)} + clickToRate + withoutContext + /> + {!!studio.o_counter && ( + + )} +
          {!isEditing && ( { const [createStudio] = useStudioCreate(); - async function onSave(input: GQL.StudioCreateInput) { + async function onSave(input: GQL.StudioCreateInput, andNew?: boolean) { const result = await createStudio({ variables: { input }, }); if (result.data?.studioCreate?.id) { - history.push(`/studios/${result.data.studioCreate.id}`); + if (!andNew) { + history.push(`/studios/${result.data.studioCreate.id}`); + } Toast.success( intl.formatMessage( { id: "toast.created_entity" }, diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx index 81e389765..5ad92100f 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx @@ -3,6 +3,7 @@ import { TagLink } from "src/components/Shared/TagLink"; import * as GQL from "src/core/generated-graphql"; import { DetailItem } from "src/components/Shared/DetailItem"; import { StashIDPill } from "src/components/Shared/StashID"; +import { PatchComponent } from "src/patch"; import { Link } from "react-router-dom"; interface IStudioDetailsPanel { @@ -11,66 +12,85 @@ interface IStudioDetailsPanel { fullWidth?: boolean; } -export const StudioDetailsPanel: React.FC = ({ - studio, - fullWidth, -}) => { - function renderTagsField() { - if (!studio.tags.length) { - return; - } - return ( -
            - {(studio.tags ?? []).map((tag) => ( - - ))} -
          - ); - } - - function renderStashIDs() { - if (!studio.stash_ids?.length) { - return; +export const StudioDetailsPanel: React.FC = PatchComponent( + "StudioDetailsPanel", + ({ studio, fullWidth }) => { + function renderTagsField() { + if (!studio.tags.length) { + return; + } + return ( +
            + {(studio.tags ?? []).map((tag) => ( + + ))} +
          + ); } - return ( -
            - {studio.stash_ids.map((stashID) => { - return ( -
          • - + function renderStashIDs() { + if (!studio.stash_ids?.length) { + return; + } + + return ( +
              + {studio.stash_ids.map((stashID) => { + return ( +
            • + +
            • + ); + })} +
            + ); + } + + function renderURLs() { + if (!studio.urls?.length) { + return; + } + + return ( +
              + {studio.urls.map((url) => ( +
            • + + {url} +
            • - ); - })} -
            + ))} +
          + ); + } + + return ( +
          + + + + {studio.parent_studio.name} + + ) : ( + "" + ) + } + fullWidth={fullWidth} + /> + + +
          ); } - - return ( -
          - - - {studio.parent_studio.name} - - ) : ( - "" - ) - } - fullWidth={fullWidth} - /> - - -
          - ); -}; +); export const CompressedStudioDetailsPanel: React.FC = ({ studio, diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx index 1089e5ffe..f887e5403 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx @@ -5,22 +5,26 @@ import * as yup from "yup"; import Mousetrap from "mousetrap"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; -import { Form } from "react-bootstrap"; +import { Button, Form } from "react-bootstrap"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; import ImageUtils from "src/utils/image"; -import { getStashIDs } from "src/utils/stashIds"; +import { addUpdateStashID, getStashIDs } from "src/utils/stashIds"; import { useFormik } from "formik"; import { Prompt } from "react-router-dom"; import isEqual from "lodash-es/isEqual"; import { useToast } from "src/hooks/Toast"; +import { useConfigurationContext } from "src/hooks/Config"; import { handleUnsavedChanges } from "src/utils/navigation"; import { formikUtils } from "src/utils/form"; -import { yupFormikValidate, yupUniqueAliases } from "src/utils/yup"; +import { yupFormikValidate, yupRequiredStringArray } from "src/utils/yup"; import { Studio, StudioSelect } from "../StudioSelect"; import { useTagsEdit } from "src/hooks/tagsEdit"; +import { Icon } from "src/components/Shared/Icon"; +import StashBoxIDSearchModal from "src/components/Shared/StashBoxIDSearchModal"; interface IStudioEditPanel { studio: Partial; - onSubmit: (studio: GQL.StudioCreateInput) => Promise; + onSubmit: (studio: GQL.StudioCreateInput, andNew?: boolean) => Promise; onCancel: () => void; onDelete: () => void; setImage: (image?: string | null) => void; @@ -37,9 +41,13 @@ export const StudioEditPanel: React.FC = ({ }) => { const intl = useIntl(); const Toast = useToast(); + const { configuration: stashConfig } = useConfigurationContext(); const isNew = studio.id === undefined; + // Editing state + const [isStashIDSearchOpen, setIsStashIDSearchOpen] = useState(false); + // Network state const [isLoading, setIsLoading] = useState(false); @@ -47,10 +55,10 @@ export const StudioEditPanel: React.FC = ({ const schema = yup.object({ name: yup.string().required(), - url: yup.string().ensure(), + urls: yup.array(yup.string().required()).defined(), details: yup.string().ensure(), parent_id: yup.string().required().nullable(), - aliases: yupUniqueAliases(intl, "name"), + aliases: yupRequiredStringArray(intl).defined(), tag_ids: yup.array(yup.string().required()).defined(), ignore_auto_tag: yup.boolean().defined(), stash_ids: yup.mixed().defined(), @@ -60,7 +68,7 @@ export const StudioEditPanel: React.FC = ({ const initialValues = { id: studio.id, name: studio.name ?? "", - url: studio.url ?? "", + urls: studio.urls ?? [], details: studio.details ?? "", parent_id: studio.parent_studio?.id ?? null, aliases: studio.aliases ?? [], @@ -124,10 +132,10 @@ export const StudioEditPanel: React.FC = ({ }; }); - async function onSave(input: InputValues) { + async function onSave(input: InputValues, andNew?: boolean) { setIsLoading(true); try { - await onSubmit(input); + await onSubmit(input, andNew); formik.resetForm(); } catch (e) { Toast.error(e); @@ -135,6 +143,11 @@ export const StudioEditPanel: React.FC = ({ setIsLoading(false); } + async function onSaveAndNewClick() { + const input = schema.cast(formik.values); + onSave(input, true); + } + function onImageLoad(imageData: string | null) { formik.setFieldValue("image", imageData); } @@ -143,6 +156,14 @@ export const StudioEditPanel: React.FC = ({ ImageUtils.onImageChange(event, onImageLoad); } + function onStashIDSelected(item?: GQL.StashIdInput) { + if (!item) return; + formik.setFieldValue( + "stash_ids", + addUpdateStashID(formik.values.stash_ids, item) + ); + } + const { renderField, renderInputField, @@ -173,6 +194,21 @@ export const StudioEditPanel: React.FC = ({ return ( <> + {isStashIDSearchOpen && ( + s.endpoint + )} + onSelectItem={(item) => { + onStashIDSelected(item); + setIsStashIDSearchOpen(false); + }} + initialQuery={studio.name ?? ""} + /> + )} + { @@ -187,11 +223,25 @@ export const StudioEditPanel: React.FC = ({
          {renderInputField("name")} {renderStringListField("aliases")} - {renderInputField("url")} + {renderStringListField("urls")} {renderInputField("details", "textarea")} {renderParentStudioField()} {renderTagsField()} - {renderStashIDsField("stash_ids", "studios")} + {renderStashIDsField( + "stash_ids", + "studios", + "stash_ids", + undefined, + + )}
          {renderInputField("ignore_auto_tag", "checkbox")}
          @@ -203,6 +253,7 @@ export const StudioEditPanel: React.FC = ({ isEditing onToggleEdit={onCancel} onSave={formik.handleSubmit} + onSaveAndNew={isNew ? onSaveAndNewClick : undefined} saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})} onImageChange={onImageChange} onImageChangeURL={onImageLoad} diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioGalleriesPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioGalleriesPanel.tsx index 340586b94..f5a1aba32 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioGalleriesPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioGalleriesPanel.tsx @@ -1,6 +1,6 @@ import React from "react"; import * as GQL from "src/core/generated-graphql"; -import { GalleryList } from "src/components/Galleries/GalleryList"; +import { FilteredGalleryList } from "src/components/Galleries/GalleryList"; import { useStudioFilterHook } from "src/core/studios"; import { View } from "src/components/List/views"; @@ -17,7 +17,7 @@ export const StudioGalleriesPanel: React.FC = ({ }) => { const filterHook = useStudioFilterHook(studio, showChildStudioContent); return ( - ListFilterModel; view?: View; alterQuery?: boolean; + extraOperations?: IItemListOperation[]; } -export const StudioList: React.FC = ({ - fromParent, - filterHook, - view, - alterQuery, -}) => { - const intl = useIntl(); - const history = useHistory(); - const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); - const [isExportAll, setIsExportAll] = useState(false); +export const StudioList: React.FC = PatchComponent( + "StudioList", + ({ fromParent, filterHook, view, alterQuery, extraOperations = [] }) => { + const intl = useIntl(); + const history = useHistory(); + const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); + const [isExportAll, setIsExportAll] = useState(false); - const filterMode = GQL.FilterMode.Studios; + const filterMode = GQL.FilterMode.Studios; - const otherOperations = [ - { - text: intl.formatMessage({ id: "actions.view_random" }), - onClick: viewRandom, - }, - { - text: intl.formatMessage({ id: "actions.export" }), - onClick: onExport, - isDisplayed: showWhenSelected, - }, - { - text: intl.formatMessage({ id: "actions.export_all" }), - onClick: onExportAll, - }, - ]; + const otherOperations = [ + ...extraOperations, + { + text: intl.formatMessage({ id: "actions.view_random" }), + onClick: viewRandom, + }, + { + text: intl.formatMessage({ id: "actions.export" }), + onClick: onExport, + isDisplayed: showWhenSelected, + }, + { + text: intl.formatMessage({ id: "actions.export_all" }), + onClick: onExportAll, + }, + ]; - function addKeybinds( - result: GQL.FindStudiosQueryResult, - filter: ListFilterModel - ) { - Mousetrap.bind("p r", () => { - viewRandom(result, filter); - }); + function addKeybinds( + result: GQL.FindStudiosQueryResult, + filter: ListFilterModel + ) { + Mousetrap.bind("p r", () => { + viewRandom(result, filter); + }); - return () => { - Mousetrap.unbind("p r"); - }; - } - - async function viewRandom( - result: GQL.FindStudiosQueryResult, - filter: ListFilterModel - ) { - // query for a random studio - if (result.data?.findStudios) { - const { count } = result.data.findStudios; - - const index = Math.floor(Math.random() * count); - const filterCopy = cloneDeep(filter); - filterCopy.itemsPerPage = 1; - filterCopy.currentPage = index + 1; - const singleResult = await queryFindStudios(filterCopy); - if (singleResult.data.findStudios.studios.length === 1) { - const { id } = singleResult.data.findStudios.studios[0]; - // navigate to the studio page - history.push(`/studios/${id}`); - } + return () => { + Mousetrap.unbind("p r"); + }; } - } - async function onExport() { - setIsExportAll(false); - setIsExportDialogOpen(true); - } + async function viewRandom( + result: GQL.FindStudiosQueryResult, + filter: ListFilterModel + ) { + // query for a random studio + if (result.data?.findStudios) { + const { count } = result.data.findStudios; - async function onExportAll() { - setIsExportAll(true); - setIsExportDialogOpen(true); - } - - function renderContent( - result: GQL.FindStudiosQueryResult, - filter: ListFilterModel, - selectedIds: Set, - onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void - ) { - function maybeRenderExportDialog() { - if (isExportDialogOpen) { - return ( - setIsExportDialogOpen(false)} - /> - ); + const index = Math.floor(Math.random() * count); + const filterCopy = cloneDeep(filter); + filterCopy.itemsPerPage = 1; + filterCopy.currentPage = index + 1; + const singleResult = await queryFindStudios(filterCopy); + if (singleResult.data.findStudios.studios.length === 1) { + const { id } = singleResult.data.findStudios.studios[0]; + // navigate to the studio page + history.push(`/studios/${id}`); + } } } - function renderStudios() { - if (!result.data?.findStudios) return; + async function onExport() { + setIsExportAll(false); + setIsExportDialogOpen(true); + } - if (filter.displayMode === DisplayMode.Grid) { - return ( - - ); + async function onExportAll() { + setIsExportAll(true); + setIsExportDialogOpen(true); + } + + function renderContent( + result: GQL.FindStudiosQueryResult, + filter: ListFilterModel, + selectedIds: Set, + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void + ) { + function maybeRenderExportDialog() { + if (isExportDialogOpen) { + return ( + setIsExportDialogOpen(false)} + /> + ); + } } - if (filter.displayMode === DisplayMode.List) { - return

          TODO

          ; - } - if (filter.displayMode === DisplayMode.Wall) { - return

          TODO

          ; - } - if (filter.displayMode === DisplayMode.Tagger) { - return ; + + function renderStudios() { + if (!result.data?.findStudios) return; + + if (filter.displayMode === DisplayMode.Grid) { + return ( + + ); + } + if (filter.displayMode === DisplayMode.List) { + return

          TODO

          ; + } + if (filter.displayMode === DisplayMode.Wall) { + return

          TODO

          ; + } + if (filter.displayMode === DisplayMode.Tagger) { + return ; + } } + + return ( + <> + {maybeRenderExportDialog()} + {renderStudios()} + + ); + } + + function renderEditDialog( + selectedStudios: GQL.SlimStudioDataFragment[], + onClose: (applied: boolean) => void + ) { + return ; + } + + function renderDeleteDialog( + selectedStudios: GQL.SlimStudioDataFragment[], + onClose: (confirmed: boolean) => void + ) { + return ( + + ); } return ( - <> - {maybeRenderExportDialog()} - {renderStudios()} - - ); - } - - function renderDeleteDialog( - selectedStudios: GQL.SlimStudioDataFragment[], - onClose: (confirmed: boolean) => void - ) { - return ( - - ); - } - - return ( - - - - ); -}; + selectable + > + + + ); + } +); diff --git a/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx b/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx index 3df4f65c6..bede2da1d 100644 --- a/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx +++ b/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx @@ -7,6 +7,7 @@ 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 { PatchComponent } from "src/patch"; interface IProps { isTouch: boolean; @@ -14,41 +15,44 @@ interface IProps { header: string; } -export const StudioRecommendationRow: React.FC = (props) => { - const result = useFindStudios(props.filter); - const cardCount = result.data?.findStudios.count; +export const StudioRecommendationRow: React.FC = PatchComponent( + "StudioRecommendationRow", + (props) => { + const result = useFindStudios(props.filter); + const cardCount = result.data?.findStudios.count; - if (!result.loading && !cardCount) { - return null; - } + if (!result.loading && !cardCount) { + return null; + } - return ( - - - - } - > - + + + } > - {result.loading - ? [...Array(props.filter.itemsPerPage)].map((i) => ( -
          - )) - : result.data?.findStudios.studios.map((s) => ( - - ))} -
          -
          - ); -}; + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
          + )) + : result.data?.findStudios.studios.map((s) => ( + + ))} +
          + + ); + } +); diff --git a/ui/v2.5/src/components/Studios/StudioSelect.tsx b/ui/v2.5/src/components/Studios/StudioSelect.tsx index 7b22b7f22..7305aa60d 100644 --- a/ui/v2.5/src/components/Studios/StudioSelect.tsx +++ b/ui/v2.5/src/components/Studios/StudioSelect.tsx @@ -13,7 +13,7 @@ import { queryFindStudiosByIDForSelect, queryFindStudiosForSelect, } from "src/core/StashService"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { useIntl } from "react-intl"; import { defaultMaxOptionsShown, IUIConfig } from "src/core/config"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -65,12 +65,12 @@ const _StudioSelect: React.FC< > = (props) => { const [createStudio] = useStudioCreate(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const intl = useIntl(); const maxOptionsShown = (configuration?.ui as IUIConfig).maxOptionsShown ?? defaultMaxOptionsShown; const defaultCreatable = - !configuration?.interface.disableDropdownCreate.studio ?? true; + !configuration?.interface.disableDropdownCreate.studio; const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]); diff --git a/ui/v2.5/src/components/Studios/styles.scss b/ui/v2.5/src/components/Studios/styles.scss index eaab21d10..0526902b7 100644 --- a/ui/v2.5/src/components/Studios/styles.scss +++ b/ui/v2.5/src/components/Studios/styles.scss @@ -41,6 +41,27 @@ width: auto; } + .quality-group { + display: inline-flex; + margin-top: 0.25rem; + } + + // The following min-width declarations prevent + // the O-Count from moving around + // when hovering over rating stars + .rating-stars-precision-full .star-rating-number { + min-width: 0.75rem; + } + + .rating-stars-precision-half .star-rating-number, + .rating-stars-precision-tenth .star-rating-number { + min-width: 1.45rem; + } + + .rating-stars-precision-quarter .star-rating-number { + min-width: 2rem; + } + // the detail element ids are the same as field type name // which don't follow the correct convention /* stylelint-disable selector-class-pattern */ @@ -49,5 +70,9 @@ display: none; } } + + .detail-item.urls ul { + list-style-type: none; + } /* stylelint-enable selector-class-pattern */ } diff --git a/ui/v2.5/src/components/Tagger/LinkButton.tsx b/ui/v2.5/src/components/Tagger/LinkButton.tsx new file mode 100644 index 000000000..c2c544e71 --- /dev/null +++ b/ui/v2.5/src/components/Tagger/LinkButton.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { useIntl } from "react-intl"; +import { faLink } from "@fortawesome/free-solid-svg-icons"; + +import { OperationButton } from "../Shared/OperationButton"; +import { Icon } from "../Shared/Icon"; + +export const LinkButton: React.FC<{ + disabled: boolean; + onLink: () => Promise; +}> = ({ disabled, onLink }) => { + const intl = useIntl(); + + return ( + + + + ); +}; diff --git a/ui/v2.5/src/components/Tagger/config.ts b/ui/v2.5/src/components/Tagger/config.ts index 78515f550..c30db7da2 100644 --- a/ui/v2.5/src/components/Tagger/config.ts +++ b/ui/v2.5/src/components/Tagger/config.ts @@ -1,10 +1,10 @@ -import { useCallback, useContext } from "react"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useCallback } from "react"; +import { useConfigurationContext } from "src/hooks/Config"; import { initialConfig, ITaggerConfig } from "./constants"; import { useConfigureUISetting } from "src/core/StashService"; export function useTaggerConfig() { - const { configuration: stashConfig } = useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const [saveUISetting] = useConfigureUISetting(); const config = stashConfig?.ui.taggerConfig ?? initialConfig; diff --git a/ui/v2.5/src/components/Tagger/constants.ts b/ui/v2.5/src/components/Tagger/constants.ts index e519fb884..d499062aa 100644 --- a/ui/v2.5/src/components/Tagger/constants.ts +++ b/ui/v2.5/src/components/Tagger/constants.ts @@ -1,4 +1,4 @@ -import { ScraperSourceInput } from "src/core/generated-graphql"; +import { GenderEnum, ScraperSourceInput } from "src/core/generated-graphql"; export const STASH_BOX_PREFIX = "stashbox:"; export const SCRAPER_PREFIX = "scraper:"; @@ -27,7 +27,6 @@ export const DEFAULT_EXCLUDED_STUDIO_FIELDS = ["name"]; export const initialConfig: ITaggerConfig = { blacklist: DEFAULT_BLACKLIST, - showMales: true, mode: "auto", setCoverImage: true, setTags: true, @@ -43,7 +42,7 @@ export type ParseMode = "auto" | "filename" | "dir" | "path" | "metadata"; export type TagOperation = "merge" | "overwrite"; export interface ITaggerConfig { blacklist: string[]; - showMales: boolean; + performerGenders?: GenderEnum[]; mode: ParseMode; setCoverImage: boolean; setTags: boolean; diff --git a/ui/v2.5/src/components/Tagger/context.tsx b/ui/v2.5/src/components/Tagger/context.tsx index 434a47cce..fb73f21e3 100644 --- a/ui/v2.5/src/components/Tagger/context.tsx +++ b/ui/v2.5/src/components/Tagger/context.tsx @@ -15,9 +15,10 @@ import { useStudioCreate, useStudioUpdate, useTagCreate, + useTagUpdate, } from "src/core/StashService"; import { useToast } from "src/hooks/Toast"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ITaggerSource, SCRAPER_PREFIX, STASH_BOX_PREFIX } from "./constants"; import { errorToString } from "src/utils"; import { mergeStudioStashIDs } from "./utils"; @@ -55,6 +56,10 @@ export interface ITaggerContextState { ) => Promise; updateStudio: (studio: GQL.StudioUpdateInput) => Promise; linkStudio: (studio: GQL.ScrapedStudio, studioID: string) => Promise; + updateTag: ( + tag: GQL.ScrapedTag, + updateInput: GQL.TagUpdateInput + ) => Promise; resolveScene: ( sceneID: string, index: number, @@ -92,6 +97,7 @@ export const TaggerStateContext = React.createContext({ createNewStudio: dummyValFn, updateStudio: dummyFn, linkStudio: dummyFn, + updateTag: dummyFn, resolveScene: dummyFn, submitFingerprints: dummyFn, pendingFingerprints: [], @@ -117,7 +123,7 @@ export const TaggerContext: React.FC = ({ children }) => { const stopping = useRef(false); - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const { config, setConfig } = useTaggerConfig(); const Scrapers = useListSceneScrapers(); @@ -129,6 +135,7 @@ export const TaggerContext: React.FC = ({ children }) => { const [createStudio] = useStudioCreate(); const [updateStudio] = useStudioUpdate(); const [updateScene] = useSceneUpdate(); + const [updateTag] = useTagUpdate(); useEffect(() => { if (!stashConfig || !Scrapers.data) { @@ -173,16 +180,41 @@ export const TaggerContext: React.FC = ({ children }) => { setSources(stashboxSources.concat(scraperSources)); }, [Scrapers.data, stashConfig]); + // set the current source on load useEffect(() => { - if (sources.length && !currentSource) { - setCurrentSource(sources[0]); + if (!sources.length || currentSource) { + return; } - }, [sources, currentSource]); + // First, see if we have a saved endpoint. + if (config.selectedEndpoint) { + let source = sources.find( + (s) => s.sourceInput.stash_box_endpoint == config.selectedEndpoint + ); + if (source) { + setCurrentSource(source); + return; + } + } + // Otherwise, just use the first source. + setCurrentSource(sources[0]); + }, [sources, currentSource, config]); + // clear the search results when the source changes useEffect(() => { setSearchResults({}); }, [currentSource]); + // keep selected endpoint in config in sync with current source + useEffect(() => { + const selectedEndpoint = currentSource?.sourceInput.stash_box_endpoint; + if (selectedEndpoint && selectedEndpoint !== config.selectedEndpoint) { + setConfig({ + ...config, + selectedEndpoint, + }); + } + }, [currentSource, config, setConfig]); + function getPendingFingerprints() { const endpoint = currentSource?.sourceInput.stash_box_endpoint; if (!config || !endpoint) return []; @@ -568,7 +600,12 @@ export const TaggerContext: React.FC = ({ children }) => { return { ...r, performers: r.performers.map((p) => { - if (p.name === performer.name) { + // Match by remote_site_id if available, otherwise fall back to name + const matches = performer.remote_site_id + ? p.remote_site_id === performer.remote_site_id + : p.name === performer.name; + + if (matches) { return { ...p, stored_id: performerID, @@ -830,6 +867,50 @@ export const TaggerContext: React.FC = ({ children }) => { } } + async function updateExistingTag( + tag: GQL.ScrapedTag, + updateInput: GQL.TagUpdateInput + ) { + const hasRemoteID = !!tag.remote_site_id; + + try { + await updateTag({ + variables: { + input: updateInput, + }, + }); + + const newSearchResults = mapResults((r) => { + if (!r.tags) { + return r; + } + + return { + ...r, + tags: r.tags.map((t) => { + if ( + (hasRemoteID && t.remote_site_id === tag.remote_site_id) || + (!hasRemoteID && t.name === tag.name) + ) { + return { + ...t, + stored_id: updateInput.id, + }; + } + + return t; + }), + }; + }); + + setSearchResults(newSearchResults); + + Toast.success(Updated tag); + } catch (e) { + Toast.error(e); + } + } + return ( { createNewStudio, updateStudio: updateExistingStudio, linkStudio, + updateTag: updateExistingTag, resolveScene, saveScene, submitFingerprints, diff --git a/ui/v2.5/src/components/Tagger/performers/Config.tsx b/ui/v2.5/src/components/Tagger/performers/Config.tsx index a839e1ae6..0d5316735 100644 --- a/ui/v2.5/src/components/Tagger/performers/Config.tsx +++ b/ui/v2.5/src/components/Tagger/performers/Config.tsx @@ -1,7 +1,7 @@ import React, { Dispatch, useState } from "react"; import { Badge, Button, Card, Collapse, Form } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ITaggerConfig } from "../constants"; import PerformerFieldSelector from "../PerformerFieldSelector"; @@ -13,7 +13,7 @@ interface IConfigProps { } const Config: React.FC = ({ show, config, setConfig }) => { - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const [showExclusionModal, setShowExclusionModal] = useState(false); const excludedFields = config.excludedPerformerFields ?? []; diff --git a/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx b/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx index f0c87ff57..bb934a241 100755 --- a/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx +++ b/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx @@ -16,7 +16,7 @@ import { performerMutationImpactedQueries, } from "src/core/StashService"; import { Manual } from "src/components/Help/Manual"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import StashSearchResult from "./StashSearchResult"; import PerformerConfig from "./Config"; @@ -25,6 +25,7 @@ import PerformerModal from "../PerformerModal"; import { useUpdatePerformer } from "../queries"; import { faStar, faTags } from "@fortawesome/free-solid-svg-icons"; import { mergeStashIDs } from "src/utils/stashbox"; +import { separateNamesAndStashIds } from "src/utils/stashIds"; import { ExternalLink } from "src/components/Shared/ExternalLink"; import { useTaggerConfig } from "../config"; @@ -222,7 +223,7 @@ const PerformerBatchAddModal: React.FC = ({ as="textarea" ref={performerInput} placeholder={intl.formatMessage({ - id: "performer_tagger.performer_names_separated_by_comma", + id: "performer_tagger.performer_names_or_stashids_separated_by_comma", })} rows={6} /> @@ -620,7 +621,7 @@ interface ITaggerProps { export const PerformerTagger: React.FC = ({ performers }) => { const jobsSubscribe = useJobsSubscribe(); const intl = useIntl(); - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const { config, setConfig } = useTaggerConfig(); const [showConfig, setShowConfig] = useState(false); const [showManual, setShowManual] = useState(false); @@ -666,14 +667,17 @@ export const PerformerTagger: React.FC = ({ performers }) => { async function batchAdd(performerInput: string) { if (performerInput && selectedEndpoint) { - const names = performerInput + const inputs = performerInput .split(",") .map((n) => n.trim()) .filter((n) => n.length > 0); - if (names.length > 0) { + const { names, stashIds } = separateNamesAndStashIds(inputs); + + if (names.length > 0 || stashIds.length > 0) { const ret = await mutateStashBoxBatchPerformerTag({ - names: names, + names: names.length > 0 ? names : undefined, + stash_ids: stashIds.length > 0 ? stashIds : undefined, endpoint: selectedEndpointIndex, refresh: false, createParent: false, diff --git a/ui/v2.5/src/components/Tagger/scenes/Config.tsx b/ui/v2.5/src/components/Tagger/scenes/Config.tsx index f15fbd250..8b0b9ea1f 100644 --- a/ui/v2.5/src/components/Tagger/scenes/Config.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/Config.tsx @@ -13,6 +13,8 @@ import { FormattedMessage, useIntl } from "react-intl"; import { Icon } from "src/components/Shared/Icon"; import { ParseMode, TagOperation } from "../constants"; import { TaggerStateContext } from "../context"; +import { GenderEnum } from "src/core/generated-graphql"; +import { genderList } from "src/utils/gender"; const Blacklist: React.FC<{ list: string[]; @@ -118,6 +120,27 @@ const Config: React.FC = ({ show }) => { const { config, setConfig } = useContext(TaggerStateContext); const intl = useIntl(); + function renderGenderCheckbox(gender: GenderEnum) { + const performerGenders = config.performerGenders || genderList.slice(); + return ( + } + checked={performerGenders.includes(gender)} + onChange={(e) => { + const isChecked = e.currentTarget.checked; + setConfig({ + ...config, + performerGenders: isChecked + ? [...performerGenders, gender] + : performerGenders.filter((g) => g !== gender), + }); + }} + /> + ); + } + return ( @@ -127,18 +150,16 @@ const Config: React.FC = ({ show }) => {
          - - - } - checked={config.showMales} - onChange={(e) => - setConfig({ ...config, showMales: e.currentTarget.checked }) - } - /> + + + + + {genderList.map(renderGenderCheckbox)} - + diff --git a/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx index 55e7f7f25..53caba2ff 100755 --- a/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx @@ -3,10 +3,7 @@ import { Button, ButtonGroup } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; import * as GQL from "src/core/generated-graphql"; -import { Icon } from "src/components/Shared/Icon"; -import { OperationButton } from "src/components/Shared/OperationButton"; import { OptionalField } from "../IncludeButton"; -import { faSave } from "@fortawesome/free-solid-svg-icons"; import { Performer, PerformerSelect, @@ -14,6 +11,7 @@ import { import { getStashboxBase } from "src/utils/stashbox"; import { ExternalLink } from "src/components/Shared/ExternalLink"; import { Link } from "react-router-dom"; +import { LinkButton } from "../LinkButton"; const PerformerLink: React.FC<{ performer: GQL.ScrapedPerformer | Performer; @@ -148,21 +146,6 @@ const PerformerResult: React.FC = ({ ); } - function maybeRenderLinkButton() { - if (endpoint && onLink) { - return ( - - - - ); - } - } - const selectedSource = !selectedID ? "skip" : "existing"; const safeBuildPerformerScraperLink = (id: string | null | undefined) => { @@ -199,7 +182,9 @@ const PerformerResult: React.FC = ({ isClearable={false} ageFromDate={ageFromDate} /> - {maybeRenderLinkButton()} + {endpoint && onLink && ( + + )}
          ); diff --git a/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx b/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx index 922ecc473..76a67e306 100755 --- a/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx @@ -12,7 +12,7 @@ import Config from "./Config"; import { TaggerScene } from "./TaggerScene"; import { SceneTaggerModals } from "./sceneTaggerModals"; import { SceneSearchResults } from "./StashSearchResult"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { faCog } from "@fortawesome/free-solid-svg-icons"; import { useLightbox } from "src/hooks/Lightbox/hooks"; @@ -22,11 +22,21 @@ const Scene: React.FC<{ queue?: SceneQueue; index: number; showLightboxImage: (imagePath: string) => void; -}> = ({ scene, searchResult, queue, index, showLightboxImage }) => { + selected?: boolean; + onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void; +}> = ({ + scene, + searchResult, + queue, + index, + showLightboxImage, + selected, + onSelectedChanged, +}) => { const intl = useIntl(); const { currentSource, doSceneQuery, doSceneFragmentScrape, loading } = useContext(TaggerStateContext); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const cont = configuration?.interface.continuePlaylistDefault ?? false; @@ -71,6 +81,8 @@ const Scene: React.FC<{ showLightboxImage={showLightboxImage} queue={queue} index={index} + selected={selected} + onSelectedChanged={onSelectedChanged} > {searchResult && searchResult.results?.length ? ( @@ -82,9 +94,16 @@ const Scene: React.FC<{ interface ITaggerProps { scenes: GQL.SlimSceneDataFragment[]; queue?: SceneQueue; + selectedIds: Set; + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; } -export const Tagger: React.FC = ({ scenes, queue }) => { +export const Tagger: React.FC = ({ + scenes, + queue, + selectedIds, + onSelectChange, +}) => { const { sources, setCurrentSource, @@ -103,6 +122,8 @@ export const Tagger: React.FC = ({ scenes, queue }) => { const intl = useIntl(); + const hasSelection = selectedIds.size > 0; + function handleSourceSelect(e: React.ChangeEvent) { setCurrentSource(sources!.find((s) => s.id === e.currentTarget.value)); } @@ -211,6 +232,15 @@ export const Tagger: React.FC = ({ scenes, queue }) => { return; } + // Use selected scenes if any, otherwise all scenes + const scenesToScrape = hasSelection + ? scenes.filter((s) => selectedIds.has(s.id)) + : scenes; + + if (scenesToScrape.length === 0) { + return; + } + if (loadingMulti) { return (
          ); - async function onCreateTag(t: GQL.ScrapedTag) { - const toCreate: GQL.TagCreateInput = { name: t.name }; - const newTagID = await createNewTag(t, toCreate); - if (newTagID !== undefined) { - setTagIDs([...tagIDs, newTagID]); - } - } - function maybeRenderTagsField() { if (!config.setTags) return; @@ -754,9 +785,24 @@ 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 249e34e74..a77025d57 100644 --- a/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx @@ -84,6 +84,44 @@ const StudioDetails: React.FC = ({ ); } + function maybeRenderURLListField( + name: string, + text: string[] | null | undefined, + truncate: boolean = true + ) { + if (!text) return; + + return ( +
        +
        + {!isNew && ( + + )} + + : + +
        +
        +
          + {text.map((t, i) => ( +
        • + + {truncate ? : t} + +
        • + ))} +
        +
        +
        + ); + } + function maybeRenderStashBoxLink() { if (!link) return; @@ -103,7 +141,10 @@ const StudioDetails: React.FC = ({
        {maybeRenderField("name", studio.name, !isNew)} - {maybeRenderField("url", studio.url)} + {maybeRenderURLListField("urls", studio.urls)} + {maybeRenderField("details", studio.details)} + {maybeRenderField("aliases", studio.aliases)} + {maybeRenderField("tags", studio.tags?.map((t) => t.name).join(", "))} {maybeRenderField("parent_studio", studio.parent?.name, false)} {maybeRenderStashBoxLink()}
        @@ -191,9 +232,17 @@ const StudioModal: React.FC = ({ const studioData: GQL.StudioCreateInput = { name: studio.name, - url: studio.url, + urls: studio.urls, image: studio.image, parent_id: studio.parent?.stored_id, + details: studio.details, + aliases: studio.aliases + ?.split(",") + .map((a) => a.trim()) + .filter((a) => a), + tag_ids: studio.tags?.map((t) => t.stored_id).filter((id) => id) as + | string[] + | undefined, }; // stashid handling code @@ -221,8 +270,16 @@ const StudioModal: React.FC = ({ parentData = { name: studio.parent?.name, - url: studio.parent?.url, + urls: studio.parent?.urls, image: studio.parent?.image, + details: studio.parent?.details, + aliases: studio.parent?.aliases + ?.split(",") + .map((a) => a.trim()) + .filter((a) => a), + tag_ids: studio.parent?.tags + ?.map((t) => t.stored_id) + .filter((id) => id) as string[] | undefined, }; // stashid handling code diff --git a/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx b/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx index 410ce2d19..c37df2258 100755 --- a/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx @@ -3,16 +3,14 @@ import { Button, ButtonGroup } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; import cx from "classnames"; -import { Icon } from "src/components/Shared/Icon"; -import { OperationButton } from "src/components/Shared/OperationButton"; import { StudioSelect, SelectObject } from "src/components/Shared/Select"; import * as GQL from "src/core/generated-graphql"; import { OptionalField } from "../IncludeButton"; -import { faSave } from "@fortawesome/free-solid-svg-icons"; import { getStashboxBase } from "src/utils/stashbox"; import { ExternalLink } from "src/components/Shared/ExternalLink"; import { Link } from "react-router-dom"; +import { LinkButton } from "../LinkButton"; const StudioLink: React.FC<{ studio: GQL.ScrapedStudio | GQL.SlimStudioDataFragment; @@ -117,21 +115,6 @@ const StudioResult: React.FC = ({ ); } - function maybeRenderLinkButton() { - if (endpoint && onLink) { - return ( - - - - ); - } - } - const selectedSource = !selectedID ? "skip" : "existing"; const safeBuildStudioScraperLink = (id: string | null | undefined) => { @@ -169,7 +152,9 @@ const StudioResult: React.FC = ({ })} isClearable={false} /> - {maybeRenderLinkButton()} + {endpoint && onLink && ( + + )}
        ); diff --git a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx index db36bf404..5446257e5 100644 --- a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx @@ -19,7 +19,7 @@ import { faImage, } from "@fortawesome/free-solid-svg-icons"; import { objectPath, objectTitle } from "src/core/files"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { SceneQueue } from "src/models/sceneQueue"; interface ITaggerSceneDetails { @@ -116,6 +116,8 @@ interface ITaggerScene { showLightboxImage: (imagePath: string) => void; queue?: SceneQueue; index?: number; + selected?: boolean; + onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void; } export const TaggerScene: React.FC> = ({ @@ -129,6 +131,8 @@ export const TaggerScene: React.FC> = ({ showLightboxImage, queue, index, + selected, + onSelectedChanged, }) => { const { config } = useContext(TaggerStateContext); const [queryString, setQueryString] = useState(""); @@ -154,7 +158,7 @@ export const TaggerScene: React.FC> = ({ const history = useHistory(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const cont = configuration?.interface.continuePlaylistDefault ?? false; async function query() { @@ -235,10 +239,28 @@ export const TaggerScene: React.FC> = ({ history.push(link); } + let shiftKey = false; + return (
        -
        + {onSelectedChanged && ( +
        + onSelectedChanged(!selected, shiftKey)} + onClick={( + event: React.MouseEvent + ) => { + shiftKey = event.shiftKey; + event.stopPropagation(); + }} + /> +
        + )} +
        > = ({
        -
        +
        {renderQueryForm()} {scrapeSceneFragment ? ( diff --git a/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx b/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx index 816e4e294..29339a9fc 100644 --- a/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx @@ -6,12 +6,17 @@ import PerformerModal from "../PerformerModal"; import { TaggerStateContext } from "../context"; import { useIntl } from "react-intl"; import { faTags } from "@fortawesome/free-solid-svg-icons"; +import { CreateLinkTagDialog } from "src/components/Shared/ScrapeDialog/CreateLinkTagDialog"; type PerformerModalCallback = (toCreate?: GQL.PerformerCreateInput) => void; type StudioModalCallback = ( toCreate?: GQL.StudioCreateInput, parentInput?: GQL.StudioCreateInput ) => void; +type TagModalCallback = (result: { + create?: GQL.TagCreateInput; + update?: GQL.TagUpdateInput; +}) => void; export interface ISceneTaggerModalsContextState { createPerformerModal: ( @@ -22,12 +27,14 @@ export interface ISceneTaggerModalsContextState { studio: GQL.ScrapedSceneStudioDataFragment, callback: StudioModalCallback ) => void; + createTagModal: (tag: GQL.ScrapedTag, callback: TagModalCallback) => void; } export const SceneTaggerModalsState = React.createContext({ createPerformerModal: () => {}, createStudioModal: () => {}, + createTagModal: () => {}, }); export const SceneTaggerModals: React.FC = ({ children }) => { @@ -47,6 +54,15 @@ export const SceneTaggerModals: React.FC = ({ children }) => { StudioModalCallback | undefined >(); + const [tagToCreate, setTagToCreate] = useState(); + const [tagCallback, setTagCallback] = useState< + | ((result: { + create?: GQL.TagCreateInput; + update?: GQL.TagUpdateInput; + }) => void) + | undefined + >(); + const intl = useIntl(); function handlePerformerSave(toCreate: GQL.PerformerCreateInput) { @@ -106,11 +122,28 @@ export const SceneTaggerModals: React.FC = ({ children }) => { setStudioCallback(() => callback); } + function handleTagSave(result: { + create?: GQL.TagCreateInput; + update?: GQL.TagUpdateInput; + }) { + if (tagCallback) { + tagCallback(result); + } + + setTagToCreate(undefined); + setTagCallback(undefined); + } + + function createTagModal(tag: GQL.ScrapedTag, callback: TagModalCallback) { + setTagToCreate(tag); + setTagCallback(() => callback); + } + const endpoint = currentSource?.sourceInput.stash_box_endpoint ?? undefined; return ( {performerToCreate && ( { endpoint={endpoint} /> )} + {tagToCreate && ( + + )} {children} ); diff --git a/ui/v2.5/src/components/Tagger/studios/Config.tsx b/ui/v2.5/src/components/Tagger/studios/Config.tsx index 9dd9f6856..ddfd17b1e 100644 --- a/ui/v2.5/src/components/Tagger/studios/Config.tsx +++ b/ui/v2.5/src/components/Tagger/studios/Config.tsx @@ -1,7 +1,7 @@ import React, { Dispatch, useState } from "react"; import { Badge, Button, Card, Collapse, Form } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ITaggerConfig } from "../constants"; import StudioFieldSelector from "./StudioFieldSelector"; @@ -13,7 +13,7 @@ interface IConfigProps { } const Config: React.FC = ({ show, config, setConfig }) => { - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const [showExclusionModal, setShowExclusionModal] = useState(false); const excludedFields = config.excludedStudioFields ?? []; diff --git a/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx b/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx index b8fbefdb5..ed9570431 100644 --- a/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx +++ b/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx @@ -17,7 +17,7 @@ import { evictQueries, } from "src/core/StashService"; import { Manual } from "src/components/Help/Manual"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import StashSearchResult from "./StashSearchResult"; import StudioConfig from "./Config"; @@ -28,6 +28,7 @@ import { apolloError } from "src/utils"; import { faStar, faTags } from "@fortawesome/free-solid-svg-icons"; import { ExternalLink } from "src/components/Shared/ExternalLink"; import { mergeStudioStashIDs } from "../utils"; +import { separateNamesAndStashIds } from "src/utils/stashIds"; import { useTaggerConfig } from "../config"; type JobFragment = Pick< @@ -242,7 +243,7 @@ const StudioBatchAddModal: React.FC = ({ as="textarea" ref={studioInput} placeholder={intl.formatMessage({ - id: "studio_tagger.studio_names_separated_by_comma", + id: "studio_tagger.studio_names_or_stashids_separated_by_comma", })} rows={6} /> @@ -669,7 +670,7 @@ interface ITaggerProps { export const StudioTagger: React.FC = ({ studios }) => { const jobsSubscribe = useJobsSubscribe(); const intl = useIntl(); - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const { config, setConfig } = useTaggerConfig(); const [showConfig, setShowConfig] = useState(false); const [showManual, setShowManual] = useState(false); @@ -715,14 +716,17 @@ export const StudioTagger: React.FC = ({ studios }) => { async function batchAdd(studioInput: string, createParent: boolean) { if (studioInput && selectedEndpoint) { - const names = studioInput + const inputs = studioInput .split(",") .map((n) => n.trim()) .filter((n) => n.length > 0); - if (names.length > 0) { + const { names, stashIds } = separateNamesAndStashIds(inputs); + + if (names.length > 0 || stashIds.length > 0) { const ret = await mutateStashBoxBatchStudioTag({ - names: names, + names: names.length > 0 ? names : undefined, + stash_ids: stashIds.length > 0 ? stashIds : undefined, endpoint: selectedEndpointIndex, refresh: false, exclude_fields: config?.excludedStudioFields ?? [], diff --git a/ui/v2.5/src/components/Tagger/styles.scss b/ui/v2.5/src/components/Tagger/styles.scss index 889d6b1b4..8861d0043 100644 --- a/ui/v2.5/src/components/Tagger/styles.scss +++ b/ui/v2.5/src/components/Tagger/styles.scss @@ -56,6 +56,10 @@ } } +.search-item-check { + cursor: pointer; +} + .search-result { background-color: rgba(61, 80, 92, 0.3); padding: 1rem 0; diff --git a/ui/v2.5/src/components/Tags/EditTagsDialog.tsx b/ui/v2.5/src/components/Tags/EditTagsDialog.tsx index 6780a8ce8..896016098 100644 --- a/ui/v2.5/src/components/Tags/EditTagsDialog.tsx +++ b/ui/v2.5/src/components/Tags/EditTagsDialog.tsx @@ -55,7 +55,7 @@ function Tags(props: { } interface IListOperationProps { - selected: GQL.TagDataFragment[]; + selected: (GQL.TagDataFragment | GQL.TagListDataFragment)[]; onClose: (applied: boolean) => void; } @@ -134,7 +134,7 @@ export const EditTagsDialog: React.FC = ( let updateChildTagIds: string[] = []; let first = true; - state.forEach((tag: GQL.TagDataFragment) => { + state.forEach((tag: GQL.TagDataFragment | GQL.TagListDataFragment) => { getAggregateStateObject(updateState, tag, tagFields, first); const thisParents = (tag.parents ?? []).map((t) => t.id).sort(); diff --git a/ui/v2.5/src/components/Tags/TagCard.tsx b/ui/v2.5/src/components/Tags/TagCard.tsx index 48c11b12b..9e2019287 100644 --- a/ui/v2.5/src/components/Tags/TagCard.tsx +++ b/ui/v2.5/src/components/Tags/TagCard.tsx @@ -14,7 +14,7 @@ import cx from "classnames"; import { useTagUpdate } from "src/core/StashService"; interface IProps { - tag: GQL.TagDataFragment; + tag: GQL.TagDataFragment | GQL.TagListDataFragment; cardWidth?: number; zoomIndex: number; selecting?: boolean; diff --git a/ui/v2.5/src/components/Tags/TagCardGrid.tsx b/ui/v2.5/src/components/Tags/TagCardGrid.tsx index f75a83cdf..d779da7e0 100644 --- a/ui/v2.5/src/components/Tags/TagCardGrid.tsx +++ b/ui/v2.5/src/components/Tags/TagCardGrid.tsx @@ -5,9 +5,10 @@ import { useContainerDimensions, } from "../Shared/GridCard/GridCard"; import { TagCard } from "./TagCard"; +import { PatchComponent } from "src/patch"; interface ITagCardGrid { - tags: GQL.TagDataFragment[]; + tags: (GQL.TagDataFragment | GQL.TagListDataFragment)[]; selectedIds: Set; zoomIndex: number; onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; @@ -15,30 +16,28 @@ interface ITagCardGrid { const zoomWidths = [280, 340, 480, 640]; -export const TagCardGrid: React.FC = ({ - tags, - selectedIds, - zoomIndex, - onSelectChange, -}) => { - const [componentRef, { width: containerWidth }] = useContainerDimensions(); - const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); +export const TagCardGrid: React.FC = PatchComponent( + "TagCardGrid", + ({ tags, selectedIds, zoomIndex, onSelectChange }) => { + const [componentRef, { width: containerWidth }] = useContainerDimensions(); + const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); - return ( -
        - {tags.map((tag) => ( - 0} - selected={selectedIds.has(tag.id)} - onSelectedChanged={(selected: boolean, shiftKey: boolean) => - onSelectChange(tag.id, selected, shiftKey) - } - /> - ))} -
        - ); -}; + return ( +
        + {tags.map((tag) => ( + 0} + selected={selectedIds.has(tag.id)} + onSelectedChanged={(selected: boolean, shiftKey: boolean) => + onSelectChange(tag.id, selected, shiftKey) + } + /> + ))} +
        + ); + } +); diff --git a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx index 7cded1934..76442b639 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx @@ -1,4 +1,4 @@ -import { Tabs, Tab, Dropdown, Form } from "react-bootstrap"; +import { Button, Tabs, Tab, Form } from "react-bootstrap"; import React, { useEffect, useMemo, useState } from "react"; import { useHistory, Redirect, RouteComponentProps } from "react-router-dom"; import { FormattedMessage, useIntl } from "react-intl"; @@ -17,9 +17,8 @@ import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; import { ErrorMessage } from "src/components/Shared/ErrorMessage"; import { ModalComponent } from "src/components/Shared/Modal"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; -import { Icon } from "src/components/Shared/Icon"; import { useToast } from "src/hooks/Toast"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { tagRelationHook } from "src/core/tags"; import { TagScenesPanel } from "./TagScenesPanel"; import { TagMarkersPanel } from "./TagMarkersPanel"; @@ -29,12 +28,8 @@ import { TagStudiosPanel } from "./TagStudiosPanel"; import { TagGalleriesPanel } from "./TagGalleriesPanel"; import { CompressedTagDetailsPanel, TagDetailsPanel } from "./TagDetailsPanel"; import { TagEditPanel } from "./TagEditPanel"; -import { TagMergeModal } from "./TagMergeDialog"; -import { - faSignInAlt, - faSignOutAlt, - faTrashAlt, -} from "@fortawesome/free-solid-svg-icons"; +import { TagMergeModal } from "../TagMergeDialog"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import { DetailImage } from "src/components/Shared/DetailImage"; import { useLoadStickyHeader } from "src/hooks/detailsPanel"; import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; @@ -49,6 +44,7 @@ import { ExpandCollapseButton } from "src/components/Shared/CollapseButton"; import { FavoriteIcon } from "src/components/Shared/FavoriteIcon"; import { AliasList } from "src/components/Shared/DetailsPage/AliasList"; import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage"; +import { goBackOrReplace } from "src/utils/history"; interface IProps { tag: GQL.TagDataFragment; @@ -292,7 +288,7 @@ const TagPage: React.FC = ({ tag, tabKey }) => { const intl = useIntl(); // Configuration settings - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const uiConfig = configuration?.ui; const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; const enableBackgroundImage = uiConfig?.enableTagBackgroundImage ?? false; @@ -305,7 +301,7 @@ const TagPage: React.FC = ({ tag, tabKey }) => { // Editing state const [isEditing, setIsEditing] = useState(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); - const [mergeType, setMergeType] = useState<"from" | "into" | undefined>(); + const [isMerging, setIsMerging] = useState(false); // Editing tag state const [image, setImage] = useState(); @@ -420,7 +416,7 @@ const TagPage: React.FC = ({ tag, tabKey }) => { return; } - history.goBack(); + goBackOrReplace(history, "/tags"); } function renderDeleteAlert() { @@ -460,41 +456,27 @@ const TagPage: React.FC = ({ tag, tabKey }) => { function renderMergeButton() { return ( - - - - ... - - - setMergeType("from")} - > - - - ... - - setMergeType("into")} - > - - - ... - - - + ); } function renderMergeDialog() { - if (!tag || !mergeType) return; + if (!tag.id) return; return ( setMergeType(undefined)} - show={!!mergeType} - mergeType={mergeType} + show={isMerging} + onClose={(mergedId) => { + setIsMerging(false); + if (mergedId !== undefined && mergedId !== tag.id) { + // By default, the merge destination is the current tag, but + // the user can change it, in which case we need to redirect. + history.replace(`/tags/${mergedId}`); + } + }} + tags={[tag]} /> ); } diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx index abbbaa291..9c8253299 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx @@ -25,7 +25,7 @@ const TagCreate: React.FC = () => { const [createTag] = useTagCreate(); - async function onSave(input: GQL.TagCreateInput) { + async function onSave(input: GQL.TagCreateInput, andNew?: boolean) { const oldRelations = { parents: [], children: [], @@ -39,7 +39,9 @@ const TagCreate: React.FC = () => { parents: created.parents, children: created.children, }); - history.push(`/tags/${created.id}`); + if (!andNew) { + history.push(`/tags/${created.id}`); + } Toast.success( intl.formatMessage( { id: "toast.created_entity" }, diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx index 9e368aa8b..92c92d072 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx @@ -1,6 +1,7 @@ import React from "react"; import { TagLink } from "src/components/Shared/TagLink"; import { DetailItem } from "src/components/Shared/DetailItem"; +import { StashIDPill } from "src/components/Shared/StashID"; import * as GQL from "src/core/generated-graphql"; interface ITagDetails { @@ -51,6 +52,22 @@ export const TagDetailsPanel: React.FC = ({ tag, fullWidth }) => { ); } + function renderStashIDs() { + if (!tag.stash_ids?.length) { + return; + } + + return ( +
          + {tag.stash_ids.map((stashID) => ( +
        • + +
        • + ))} +
        + ); + } + return (
        = ({ tag, fullWidth }) => { value={renderChildrenField()} fullWidth={fullWidth} /> +
        ); }; diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx index da79b6c4e..22c99b80e 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx @@ -3,7 +3,8 @@ import { FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import * as yup from "yup"; import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; -import { Form } from "react-bootstrap"; +import { Button, Form } from "react-bootstrap"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; import ImageUtils from "src/utils/image"; import { useFormik } from "formik"; import { Prompt } from "react-router-dom"; @@ -11,14 +12,18 @@ import Mousetrap from "mousetrap"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import isEqual from "lodash-es/isEqual"; import { useToast } from "src/hooks/Toast"; +import { useConfigurationContext } from "src/hooks/Config"; import { handleUnsavedChanges } from "src/utils/navigation"; import { formikUtils } from "src/utils/form"; -import { yupFormikValidate, yupUniqueAliases } from "src/utils/yup"; +import { yupFormikValidate, yupRequiredStringArray } from "src/utils/yup"; +import { addUpdateStashID, getStashIDs } from "src/utils/stashIds"; import { Tag, TagSelect } from "../TagSelect"; +import { Icon } from "src/components/Shared/Icon"; +import StashBoxIDSearchModal from "src/components/Shared/StashBoxIDSearchModal"; interface ITagEditPanel { tag: Partial; - onSubmit: (tag: GQL.TagCreateInput) => Promise; + onSubmit: (tag: GQL.TagCreateInput, andNew?: boolean) => Promise; onCancel: () => void; onDelete: () => void; setImage: (image?: string | null) => void; @@ -35,9 +40,13 @@ export const TagEditPanel: React.FC = ({ }) => { const intl = useIntl(); const Toast = useToast(); + const { configuration: stashConfig } = useConfigurationContext(); const isNew = tag.id === undefined; + // Editing state + const [isStashIDSearchOpen, setIsStashIDSearchOpen] = useState(false); + // Network state const [isLoading, setIsLoading] = useState(false); @@ -47,11 +56,12 @@ export const TagEditPanel: React.FC = ({ const schema = yup.object({ name: yup.string().required(), sort_name: yup.string().ensure(), - aliases: yupUniqueAliases(intl, "name"), + aliases: yupRequiredStringArray(intl).defined(), description: yup.string().ensure(), parent_ids: yup.array(yup.string().required()).defined(), child_ids: yup.array(yup.string().required()).defined(), ignore_auto_tag: yup.boolean().defined(), + stash_ids: yup.mixed().defined(), image: yup.string().nullable().optional(), }); @@ -63,6 +73,7 @@ export const TagEditPanel: React.FC = ({ parent_ids: (tag?.parents ?? []).map((t) => t.id), child_ids: (tag?.children ?? []).map((t) => t.id), ignore_auto_tag: tag?.ignore_auto_tag ?? false, + stash_ids: getStashIDs(tag?.stash_ids), }; type InputValues = yup.InferType; @@ -111,10 +122,10 @@ export const TagEditPanel: React.FC = ({ }; }); - async function onSave(input: InputValues) { + async function onSave(input: InputValues, andNew?: boolean) { setIsLoading(true); try { - await onSubmit(input); + await onSubmit(input, andNew); formik.resetForm(); } catch (e) { Toast.error(e); @@ -122,6 +133,11 @@ export const TagEditPanel: React.FC = ({ setIsLoading(false); } + async function onSaveAndNewClick() { + const input = schema.cast(formik.values); + onSave(input, true); + } + const encodingImage = ImageUtils.usePasteImage(onImageLoad); useEffect(() => { @@ -140,10 +156,21 @@ export const TagEditPanel: React.FC = ({ ImageUtils.onImageChange(event, onImageLoad); } - const { renderField, renderInputField, renderStringListField } = formikUtils( - intl, - formik - ); + function onStashIDSelected(item?: GQL.StashIdInput) { + if (!item) return; + const allowMultiple = true; + formik.setFieldValue( + "stash_ids", + addUpdateStashID(formik.values.stash_ids, item, allowMultiple) + ); + } + + const { + renderField, + renderInputField, + renderStringListField, + renderStashIDsField, + } = formikUtils(intl, formik); function renderParentTagsField() { const title = intl.formatMessage({ id: "parent_tags" }); @@ -181,53 +208,86 @@ export const TagEditPanel: React.FC = ({ // TODO: CSS class return ( -
        - {isNew && ( -

        - -

        + <> + {/* allow many stash-ids from the same stash box */} + {isStashIDSearchOpen && ( + { + onStashIDSelected(item); + setIsStashIDSearchOpen(false); + }} + initialQuery={tag?.name ?? ""} + /> )} - { - // Check if it's a redirect after tag creation - if (action === "PUSH" && location.pathname.startsWith("/tags/")) { - return true; +
        + {isNew && ( +

        + +

        + )} + + { + // Check if it's a redirect after tag creation + if (action === "PUSH" && location.pathname.startsWith("/tags/")) { + return true; + } + + return handleUnsavedChanges(intl, "tags", tag.id)(location); + }} + /> + + + {renderInputField("name")} + {renderInputField("sort_name", "text")} + {renderStringListField("aliases", "aliases", { orderable: false })} + {renderInputField("description", "textarea")} + {renderParentTagsField()} + {renderSubTagsField()} + {renderStashIDsField( + "stash_ids", + "tags", + "stash_ids", + undefined, + + )} +
        + {renderInputField("ignore_auto_tag", "checkbox")} + + + - -
        - {renderInputField("name")} - {renderInputField("sort_name", "text")} - {renderStringListField("aliases")} - {renderInputField("description", "textarea")} - {renderParentTagsField()} - {renderSubTagsField()} -
        - {renderInputField("ignore_auto_tag", "checkbox")} -
        - - onImageLoad(null)} - onDelete={onDelete} - acceptSVG - /> -
        + onImageChange={onImageChange} + onImageChangeURL={onImageLoad} + onClearImage={() => onImageLoad(null)} + onDelete={onDelete} + acceptSVG + /> +
        + ); }; diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagGalleriesPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagGalleriesPanel.tsx index bb95a7ea1..f5df9946b 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagGalleriesPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagGalleriesPanel.tsx @@ -1,7 +1,7 @@ import React from "react"; import * as GQL from "src/core/generated-graphql"; import { useTagFilterHook } from "src/core/tags"; -import { GalleryList } from "src/components/Galleries/GalleryList"; +import { FilteredGalleryList } from "src/components/Galleries/GalleryList"; import { View } from "src/components/List/views"; interface ITagGalleriesPanel { @@ -17,7 +17,7 @@ export const TagGalleriesPanel: React.FC = ({ }) => { const filterHook = useTagFilterHook(tag, showSubTagContent); return ( - void; - tag: Pick; - mergeType: "from" | "into"; -} - -export const TagMergeModal: React.FC = ({ - show, - onClose, - tag, - mergeType, -}) => { - const [src, setSrc] = useState([]); - const [dest, setDest] = useState(null); - - const [running, setRunning] = useState(false); - - const [mergeTags] = useTagsMerge(); - - const intl = useIntl(); - const Toast = useToast(); - const history = useHistory(); - - const title = intl.formatMessage({ - id: mergeType === "from" ? "actions.merge_from" : "actions.merge_into", - }); - - async function onMerge() { - const source = mergeType === "from" ? src.map((s) => s.id) : [tag.id]; - const destination = mergeType === "from" ? tag.id : dest?.id ?? null; - - if (!destination) return; - - try { - setRunning(true); - const result = await mergeTags({ - variables: { - source, - destination, - }, - }); - if (result.data?.tagsMerge) { - Toast.success(intl.formatMessage({ id: "toast.merged_tags" })); - onClose(); - history.replace(`/tags/${destination}`); - } - } catch (e) { - Toast.error(e); - } finally { - setRunning(false); - } - } - - function canMerge() { - return ( - (mergeType === "from" && src.length > 0) || - (mergeType === "into" && dest !== null) - ); - } - - return ( - onMerge(), - }} - disabled={!canMerge()} - cancel={{ - variant: "secondary", - onClick: () => onClose(), - }} - isRunning={running} - > -
        -
        - {mergeType === "from" && ( - - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "dialogs.merge_tags.source" }), - labelProps: { - column: true, - sm: 3, - xl: 12, - }, - })} - - setSrc(items)} - values={src} - excludeIds={tag?.id ? [tag.id] : []} - menuPortalTarget={document.body} - /> - - - )} - {mergeType === "into" && ( - - {FormUtils.renderLabel({ - title: intl.formatMessage({ - id: "dialogs.merge_tags.destination", - }), - labelProps: { - column: true, - sm: 3, - xl: 12, - }, - })} - - setDest(items[0])} - values={dest ? [dest] : undefined} - excludeIds={tag?.id ? [tag.id] : []} - menuPortalTarget={document.body} - /> - - - )} -
        -
        -
        - ); -}; diff --git a/ui/v2.5/src/components/Tags/TagList.tsx b/ui/v2.5/src/components/Tags/TagList.tsx index cbde60a5c..e30f6071b 100644 --- a/ui/v2.5/src/components/Tags/TagList.tsx +++ b/ui/v2.5/src/components/Tags/TagList.tsx @@ -8,9 +8,9 @@ import { Button } from "react-bootstrap"; import { Link, useHistory } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; import { - queryFindTags, + queryFindTagsForList, mutateMetadataAutoTag, - useFindTags, + useFindTagsForList, useTagDestroy, useTagsDestroy, } from "src/core/StashService"; @@ -23,357 +23,399 @@ import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog"; import { ExportDialog } from "../Shared/ExportDialog"; import { tagRelationHook } from "../../core/tags"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; +import { TagMergeModal } from "./TagMergeDialog"; +import { Tag } from "./TagSelect"; import { TagCardGrid } from "./TagCardGrid"; import { EditTagsDialog } from "./EditTagsDialog"; import { View } from "../List/views"; +import { IItemListOperation } from "../List/FilteredListToolbar"; +import { PatchComponent } from "src/patch"; -function getItems(result: GQL.FindTagsQueryResult) { +function getItems(result: GQL.FindTagsForListQueryResult) { return result?.data?.findTags?.tags ?? []; } -function getCount(result: GQL.FindTagsQueryResult) { +function getCount(result: GQL.FindTagsForListQueryResult) { return result?.data?.findTags?.count ?? 0; } interface ITagList { filterHook?: (filter: ListFilterModel) => ListFilterModel; alterQuery?: boolean; + extraOperations?: IItemListOperation[]; } -export const TagList: React.FC = ({ filterHook, alterQuery }) => { - const Toast = useToast(); - const [deletingTag, setDeletingTag] = - useState | null>(null); +export const TagList: React.FC = PatchComponent( + "TagList", + ({ filterHook, alterQuery, extraOperations = [] }) => { + const Toast = useToast(); + const [deletingTag, setDeletingTag] = + useState | null>(null); - const filterMode = GQL.FilterMode.Tags; - const view = View.Tags; + const filterMode = GQL.FilterMode.Tags; + const view = View.Tags; - function getDeleteTagInput() { - const tagInput: Partial = {}; - if (deletingTag) { - tagInput.id = deletingTag.id; - } - return tagInput as GQL.TagDestroyInput; - } - const [deleteTag] = useTagDestroy(getDeleteTagInput()); - - const intl = useIntl(); - const history = useHistory(); - const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); - const [isExportAll, setIsExportAll] = useState(false); - - const otherOperations = [ - { - text: intl.formatMessage({ id: "actions.view_random" }), - onClick: viewRandom, - }, - { - text: intl.formatMessage({ id: "actions.export" }), - onClick: onExport, - isDisplayed: showWhenSelected, - }, - { - text: intl.formatMessage({ id: "actions.export_all" }), - onClick: onExportAll, - }, - ]; - - function addKeybinds( - result: GQL.FindTagsQueryResult, - filter: ListFilterModel - ) { - Mousetrap.bind("p r", () => { - viewRandom(result, filter); - }); - - return () => { - Mousetrap.unbind("p r"); - }; - } - - async function viewRandom( - result: GQL.FindTagsQueryResult, - filter: ListFilterModel - ) { - // query for a random tag - if (result.data?.findTags) { - const { count } = result.data.findTags; - - const index = Math.floor(Math.random() * count); - const filterCopy = cloneDeep(filter); - filterCopy.itemsPerPage = 1; - filterCopy.currentPage = index + 1; - const singleResult = await queryFindTags(filterCopy); - if (singleResult.data.findTags.tags.length === 1) { - const { id } = singleResult.data.findTags.tags[0]; - // navigate to the tag page - history.push(`/tags/${id}`); + function getDeleteTagInput() { + const tagInput: Partial = {}; + if (deletingTag) { + tagInput.id = deletingTag.id; } + return tagInput as GQL.TagDestroyInput; } - } + const [deleteTag] = useTagDestroy(getDeleteTagInput()); - async function onExport() { - setIsExportAll(false); - setIsExportDialogOpen(true); - } + const intl = useIntl(); + const history = useHistory(); + const [mergeTags, setMergeTags] = useState(undefined); + const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); + const [isExportAll, setIsExportAll] = useState(false); - async function onExportAll() { - setIsExportAll(true); - setIsExportDialogOpen(true); - } + const otherOperations = [ + ...extraOperations, + { + text: intl.formatMessage({ id: "actions.view_random" }), + onClick: viewRandom, + }, + { + text: `${intl.formatMessage({ id: "actions.merge" })}…`, + onClick: merge, + isDisplayed: showWhenSelected, + }, + { + text: intl.formatMessage({ id: "actions.export" }), + onClick: onExport, + isDisplayed: showWhenSelected, + }, + { + text: intl.formatMessage({ id: "actions.export_all" }), + onClick: onExportAll, + }, + ]; - async function onAutoTag(tag: GQL.TagDataFragment) { - if (!tag) return; - try { - await mutateMetadataAutoTag({ tags: [tag.id] }); - Toast.success(intl.formatMessage({ id: "toast.started_auto_tagging" })); - } catch (e) { - Toast.error(e); - } - } - - async function onDelete() { - try { - const oldRelations = { - parents: deletingTag?.parents ?? [], - children: deletingTag?.children ?? [], - }; - await deleteTag(); - tagRelationHook(deletingTag as GQL.TagDataFragment, oldRelations, { - parents: [], - children: [], + function addKeybinds( + result: GQL.FindTagsForListQueryResult, + filter: ListFilterModel + ) { + Mousetrap.bind("p r", () => { + viewRandom(result, filter); }); - Toast.success( - intl.formatMessage( - { id: "toast.delete_past_tense" }, - { - count: 1, - singularEntity: intl.formatMessage({ id: "tag" }), - pluralEntity: intl.formatMessage({ id: "tags" }), - } - ) - ); - setDeletingTag(null); - } catch (e) { - Toast.error(e); - } - } - function renderContent( - result: GQL.FindTagsQueryResult, - filter: ListFilterModel, - selectedIds: Set, - onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void - ) { - function maybeRenderExportDialog() { - if (isExportDialogOpen) { - return ( - setIsExportDialogOpen(false)} - /> - ); + return () => { + Mousetrap.unbind("p r"); + }; + } + + async function viewRandom( + result: GQL.FindTagsForListQueryResult, + filter: ListFilterModel + ) { + // query for a random tag + if (result.data?.findTags) { + const { count } = result.data.findTags; + + const index = Math.floor(Math.random() * count); + const filterCopy = cloneDeep(filter); + filterCopy.itemsPerPage = 1; + filterCopy.currentPage = index + 1; + const singleResult = await queryFindTagsForList(filterCopy); + if (singleResult.data.findTags.tags.length === 1) { + const { id } = singleResult.data.findTags.tags[0]; + // navigate to the tag page + history.push(`/tags/${id}`); + } } } - function renderTags() { - if (!result.data?.findTags) return; + async function merge( + result: GQL.FindTagsForListQueryResult, + filter: ListFilterModel, + selectedIds: Set + ) { + const selected = + result.data?.findTags.tags.filter((t) => selectedIds.has(t.id)) ?? []; + setMergeTags(selected); + } - if (filter.displayMode === DisplayMode.Grid) { - return ( - - ); + async function onExport() { + setIsExportAll(false); + setIsExportDialogOpen(true); + } + + async function onExportAll() { + setIsExportAll(true); + setIsExportDialogOpen(true); + } + + async function onAutoTag(tag: GQL.TagListDataFragment) { + if (!tag) return; + try { + await mutateMetadataAutoTag({ tags: [tag.id] }); + Toast.success(intl.formatMessage({ id: "toast.started_auto_tagging" })); + } catch (e) { + Toast.error(e); } - if (filter.displayMode === DisplayMode.List) { - const deleteAlert = ( - {}} - show={!!deletingTag} - icon={faTrashAlt} - accept={{ - onClick: onDelete, - variant: "danger", - text: intl.formatMessage({ id: "actions.delete" }), - }} - cancel={{ onClick: () => setDeletingTag(null) }} - > - - - - - ); + } - const tagElements = result.data.findTags.tags.map((tag) => { + async function onDelete() { + try { + const oldRelations = { + parents: deletingTag?.parents ?? [], + children: deletingTag?.children ?? [], + }; + await deleteTag(); + tagRelationHook(deletingTag as GQL.TagListDataFragment, oldRelations, { + parents: [], + children: [], + }); + Toast.success( + intl.formatMessage( + { id: "toast.delete_past_tense" }, + { + count: 1, + singularEntity: intl.formatMessage({ id: "tag" }), + pluralEntity: intl.formatMessage({ id: "tags" }), + } + ) + ); + setDeletingTag(null); + } catch (e) { + Toast.error(e); + } + } + + function renderContent( + result: GQL.FindTagsForListQueryResult, + filter: ListFilterModel, + selectedIds: Set, + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void + ) { + function renderMergeDialog() { + if (mergeTags) { return ( -
        - {tag.name} + { + setMergeTags(undefined); + if (mergedId) { + history.push(`/tags/${mergedId}`); + } + }} + show + /> + ); + } + } -
        - - + + + + + + :{" "} + - : - - - - - - - :{" "} - - - + + +
        + ); + }); + + return ( +
        + {tagElements} + {deleteAlert}
        ); - }); - - return ( -
        - {tagElements} - {deleteAlert} -
        - ); - } - if (filter.displayMode === DisplayMode.Wall) { - return

        TODO

        ; + } + if (filter.displayMode === DisplayMode.Wall) { + return

        TODO

        ; + } } + return ( + <> + {renderMergeDialog()} + {maybeRenderExportDialog()} + {renderTags()} + + ); } + + function renderEditDialog( + selectedTags: GQL.TagListDataFragment[], + onClose: (confirmed: boolean) => void + ) { + return ; + } + + function renderDeleteDialog( + selectedTags: GQL.TagListDataFragment[], + onClose: (confirmed: boolean) => void + ) { + return ( + { + selectedTags.forEach((t) => + tagRelationHook( + t, + { parents: t.parents ?? [], children: t.children ?? [] }, + { parents: [], children: [] } + ) + ); + }} + /> + ); + } + return ( - <> - {maybeRenderExportDialog()} - {renderTags()} - - ); - } - - function renderEditDialog( - selectedTags: GQL.TagDataFragment[], - onClose: (confirmed: boolean) => void - ) { - return ; - } - - function renderDeleteDialog( - selectedTags: GQL.TagDataFragment[], - onClose: (confirmed: boolean) => void - ) { - return ( - { - selectedTags.forEach((t) => - tagRelationHook( - t, - { parents: t.parents ?? [], children: t.children ?? [] }, - { parents: [], children: [] } - ) - ); - }} - /> - ); - } - - return ( - - - - ); -}; + selectable + > + + + ); + } +); diff --git a/ui/v2.5/src/components/Tags/TagMergeDialog.tsx b/ui/v2.5/src/components/Tags/TagMergeDialog.tsx new file mode 100644 index 000000000..15b648af5 --- /dev/null +++ b/ui/v2.5/src/components/Tags/TagMergeDialog.tsx @@ -0,0 +1,157 @@ +import { Button, Form, Col, Row } from "react-bootstrap"; +import React, { useEffect, useState } from "react"; +import { Icon } from "../Shared/Icon"; +import { ModalComponent } from "src/components/Shared/Modal"; +import * as FormUtils from "src/utils/form"; +import { useTagsMerge } from "src/core/StashService"; +import { useIntl } from "react-intl"; +import { useToast } from "src/hooks/Toast"; +import { faExchangeAlt, faSignInAlt } from "@fortawesome/free-solid-svg-icons"; +import { Tag, TagSelect } from "./TagSelect"; + +interface ITagMergeModalProps { + show: boolean; + onClose: (mergedID?: string) => void; + tags: Tag[]; +} + +export const TagMergeModal: React.FC = ({ + show, + onClose, + tags, +}) => { + const [src, setSrc] = useState([]); + const [dest, setDest] = useState(null); + + const [running, setRunning] = useState(false); + + const [mergeTags] = useTagsMerge(); + + const intl = useIntl(); + const Toast = useToast(); + + const title = intl.formatMessage({ + id: "actions.merge", + }); + + useEffect(() => { + if (tags.length > 0) { + setDest(tags[0]); + setSrc(tags.slice(1)); + } + }, [tags]); + + async function onMerge() { + if (!dest) return; + + const source = src.map((s) => s.id); + const destination = dest.id; + + try { + setRunning(true); + const result = await mergeTags({ + variables: { + source, + destination, + }, + }); + if (result.data?.tagsMerge) { + Toast.success(intl.formatMessage({ id: "toast.merged_tags" })); + onClose(dest.id); + } + } catch (e) { + Toast.error(e); + } finally { + setRunning(false); + } + } + + function canMerge() { + return src.length > 0 && dest !== null; + } + + function switchTags() { + if (src.length && dest !== null) { + const newDest = src[0]; + setSrc([...src.slice(1), dest]); + setDest(newDest); + } + } + + return ( + onMerge(), + }} + disabled={!canMerge()} + cancel={{ + variant: "secondary", + onClick: () => onClose(), + }} + isRunning={running} + > +
        +
        + + {FormUtils.renderLabel({ + title: intl.formatMessage({ id: "dialogs.merge.source" }), + labelProps: { + column: true, + sm: 3, + xl: 12, + }, + })} + + setSrc(items)} + values={src} + menuPortalTarget={document.body} + /> + + + + + + + {FormUtils.renderLabel({ + title: intl.formatMessage({ + id: "dialogs.merge.destination", + }), + labelProps: { + column: true, + sm: 3, + xl: 12, + }, + })} + + setDest(items[0])} + values={dest ? [dest] : undefined} + menuPortalTarget={document.body} + /> + + +
        +
        +
        + ); +}; diff --git a/ui/v2.5/src/components/Tags/TagPopover.tsx b/ui/v2.5/src/components/Tags/TagPopover.tsx index 9e3f0d80b..ef3aa950a 100644 --- a/ui/v2.5/src/components/Tags/TagPopover.tsx +++ b/ui/v2.5/src/components/Tags/TagPopover.tsx @@ -4,7 +4,7 @@ import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { HoverPopover } from "../Shared/HoverPopover"; import { useFindTag } from "../../core/StashService"; import { TagCard } from "./TagCard"; -import { ConfigurationContext } from "../../hooks/Config"; +import { useConfigurationContext } from "../../hooks/Config"; import { Placement } from "react-bootstrap/esm/Overlay"; interface ITagPopoverCardProps { @@ -47,7 +47,7 @@ export const TagPopover: React.FC = ({ placement = "top", target, }) => { - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const showTagCardOnHover = config?.ui.showTagCardOnHover ?? true; diff --git a/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx b/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx index 9d10d7333..27e9e8dce 100644 --- a/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx +++ b/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx @@ -7,6 +7,7 @@ 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 { PatchComponent } from "src/patch"; interface IProps { isTouch: boolean; @@ -14,38 +15,41 @@ interface IProps { header: string; } -export const TagRecommendationRow: React.FC = (props) => { - const result = useFindTags(props.filter); - const cardCount = result.data?.findTags.count; +export const TagRecommendationRow: React.FC = PatchComponent( + "TagRecommendationRow", + (props) => { + const result = useFindTags(props.filter); + const cardCount = result.data?.findTags.count; - if (!result.loading && !cardCount) { - return null; - } + if (!result.loading && !cardCount) { + return null; + } - return ( - - - - } - > - + + + } > - {result.loading - ? [...Array(props.filter.itemsPerPage)].map((i) => ( -
        - )) - : result.data?.findTags.tags.map((p) => ( - - ))} -
        -
        - ); -}; + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
        + )) + : result.data?.findTags.tags.map((p) => ( + + ))} +
        + + ); + } +); diff --git a/ui/v2.5/src/components/Tags/TagSelect.tsx b/ui/v2.5/src/components/Tags/TagSelect.tsx index 2f6fb9a3e..c9ed83fea 100644 --- a/ui/v2.5/src/components/Tags/TagSelect.tsx +++ b/ui/v2.5/src/components/Tags/TagSelect.tsx @@ -13,7 +13,7 @@ import { queryFindTagsByIDForSelect, queryFindTagsForSelect, } from "src/core/StashService"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { useIntl } from "react-intl"; import { defaultMaxOptionsShown } from "src/core/config"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -38,7 +38,7 @@ export type SelectObject = { export type Tag = Pick< GQL.Tag, - "id" | "name" | "sort_name" | "aliases" | "image_path" + "id" | "name" | "sort_name" | "aliases" | "image_path" | "stash_ids" >; type Option = SelectOption; @@ -67,12 +67,11 @@ export type TagSelectProps = IFilterProps & const _TagSelect: React.FC = (props) => { const [createTag] = useTagCreate(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const intl = useIntl(); const maxOptionsShown = configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; - const defaultCreatable = - !configuration?.interface.disableDropdownCreate.tag ?? true; + const defaultCreatable = !configuration?.interface.disableDropdownCreate.tag; const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]); @@ -199,6 +198,7 @@ const _TagSelect: React.FC = (props) => { id, name, aliases: [], + stash_ids: [], }; }; diff --git a/ui/v2.5/src/components/TroubleshootingMode/TroubleshootingModeButton.tsx b/ui/v2.5/src/components/TroubleshootingMode/TroubleshootingModeButton.tsx new file mode 100644 index 000000000..164774446 --- /dev/null +++ b/ui/v2.5/src/components/TroubleshootingMode/TroubleshootingModeButton.tsx @@ -0,0 +1,67 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; +import { FormattedMessage, useIntl } from "react-intl"; +import { faBug } from "@fortawesome/free-solid-svg-icons"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { useTroubleshootingMode } from "./useTroubleshootingMode"; + +const DIALOG_ITEMS = [ + "config.ui.troubleshooting_mode.dialog_item_plugins", + "config.ui.troubleshooting_mode.dialog_item_css", + "config.ui.troubleshooting_mode.dialog_item_js", + "config.ui.troubleshooting_mode.dialog_item_locales", +] as const; + +export const TroubleshootingModeButton: React.FC = () => { + const intl = useIntl(); + const [showDialog, setShowDialog] = useState(false); + const { enable, isLoading } = useTroubleshootingMode(); + + return ( + <> +
        + +
        + + setShowDialog(false)} + header={intl.formatMessage({ + id: "config.ui.troubleshooting_mode.dialog_title", + })} + icon={faBug} + accept={{ + text: intl.formatMessage({ + id: "config.ui.troubleshooting_mode.enable", + }), + variant: "primary", + onClick: enable, + }} + cancel={{ + onClick: () => setShowDialog(false), + variant: "secondary", + }} + isRunning={isLoading} + > +

        + +

        +
          + {DIALOG_ITEMS.map((id) => ( +
        • + +
        • + ))} +
        +

        + +

        +

        + +

        +
        + + ); +}; diff --git a/ui/v2.5/src/components/TroubleshootingMode/TroubleshootingModeOverlay.tsx b/ui/v2.5/src/components/TroubleshootingMode/TroubleshootingModeOverlay.tsx new file mode 100644 index 000000000..bf2b38f8a --- /dev/null +++ b/ui/v2.5/src/components/TroubleshootingMode/TroubleshootingModeOverlay.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { Button } from "react-bootstrap"; +import { FormattedMessage } from "react-intl"; +import { faBug } from "@fortawesome/free-solid-svg-icons"; +import { Icon } from "src/components/Shared/Icon"; +import { useTroubleshootingMode } from "./useTroubleshootingMode"; + +export const TroubleshootingModeOverlay: React.FC = () => { + const { isActive, isLoading, disable } = useTroubleshootingMode(); + + if (!isActive) { + return null; + } + + return ( +
        +
        + + + + + +
        +
        + ); +}; diff --git a/ui/v2.5/src/components/TroubleshootingMode/useTroubleshootingMode.ts b/ui/v2.5/src/components/TroubleshootingMode/useTroubleshootingMode.ts new file mode 100644 index 000000000..63b4edd4f --- /dev/null +++ b/ui/v2.5/src/components/TroubleshootingMode/useTroubleshootingMode.ts @@ -0,0 +1,83 @@ +import { useState, useRef, useEffect } from "react"; +import { + useConfigureInterface, + useConfigureGeneral, + useConfiguration, +} from "src/core/StashService"; + +const ORIGINAL_LOG_LEVEL_KEY = "troubleshootingMode_originalLogLevel"; + +export function useTroubleshootingMode() { + const [isLoading, setIsLoading] = useState(false); + const isMounted = useRef(true); + + const { data: config } = useConfiguration(); + const [configureInterface] = useConfigureInterface(); + const [configureGeneral] = useConfigureGeneral(); + + const isActive = + config?.configuration?.interface?.disableCustomizations ?? false; + const currentLogLevel = config?.configuration?.general?.logLevel || "Info"; + + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + + async function enable() { + setIsLoading(true); + try { + // Store original log level for restoration later + localStorage.setItem(ORIGINAL_LOG_LEVEL_KEY, currentLogLevel); + + // Enable troubleshooting mode and set log level to Debug + await Promise.all([ + configureInterface({ + variables: { input: { disableCustomizations: true } }, + }), + configureGeneral({ + variables: { input: { logLevel: "Debug" } }, + }), + ]); + + window.location.reload(); + } catch (e) { + if (isMounted.current) { + setIsLoading(false); + } + throw e; + } + } + + async function disable() { + setIsLoading(true); + try { + // Restore original log level + const originalLogLevel = + localStorage.getItem(ORIGINAL_LOG_LEVEL_KEY) || "Info"; + + // Disable troubleshooting mode and restore log level + await Promise.all([ + configureInterface({ + variables: { input: { disableCustomizations: false } }, + }), + configureGeneral({ + variables: { input: { logLevel: originalLogLevel } }, + }), + ]); + + // Clean up localStorage + localStorage.removeItem(ORIGINAL_LOG_LEVEL_KEY); + + window.location.reload(); + } catch (e) { + if (isMounted.current) { + setIsLoading(false); + } + throw e; + } + } + + return { isActive, isLoading, enable, disable }; +} diff --git a/ui/v2.5/src/components/Wall/WallItem.tsx b/ui/v2.5/src/components/Wall/WallItem.tsx index 5811b7543..959ac1617 100644 --- a/ui/v2.5/src/components/Wall/WallItem.tsx +++ b/ui/v2.5/src/components/Wall/WallItem.tsx @@ -12,7 +12,7 @@ import TextUtils from "src/utils/text"; import NavUtils from "src/utils/navigation"; import cx from "classnames"; import { SceneQueue } from "src/models/sceneQueue"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { markerTitle } from "src/core/markers"; import { objectTitle } from "src/core/files"; @@ -128,7 +128,7 @@ export const WallItem = ({ }: IWallItemProps) => { const [active, setActive] = useState(false); const itemEl = useRef(null); - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const showTextContainer = config?.interface.wallShowTitle ?? true; diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index a7679a5d5..6aaf17125 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -201,6 +201,15 @@ export const useFindImages = (filter?: ListFilterModel) => }, }); +export const useFindImagesMetadata = (filter?: ListFilterModel) => + GQL.useFindImagesMetadataQuery({ + skip: filter === undefined, + variables: { + filter: filter?.makeFindFilter(), + image_filter: filter?.makeFilter(), + }, + }); + export const queryFindImages = (filter: ListFilterModel) => client.query({ query: GQL.FindImagesDocument, @@ -343,6 +352,14 @@ export const queryFindPerformers = (filter: ListFilterModel) => }, }); +export const queryFindPerformersByID = (performerIDs: number[]) => + client.query({ + query: GQL.FindPerformersDocument, + variables: { + performer_ids: performerIDs, + }, + }); + export const queryFindPerformersByIDForSelect = (performerIDs: string[]) => client.query({ query: GQL.FindPerformersForSelectDocument, @@ -411,6 +428,12 @@ export const useFindTag = (id: string) => { return GQL.useFindTagQuery({ variables: { id }, skip }); }; +export const queryFindTag = (id: string) => + client.query({ + query: GQL.FindTagDocument, + variables: { id }, + }); + export const useFindTags = (filter?: ListFilterModel) => GQL.useFindTagsQuery({ skip: filter === undefined, @@ -420,6 +443,16 @@ export const useFindTags = (filter?: ListFilterModel) => }, }); +// Optimized query for tag list page - excludes expensive recursive *_count_all fields +export const useFindTagsForList = (filter?: ListFilterModel) => + GQL.useFindTagsForListQuery({ + skip: filter === undefined, + variables: { + filter: filter?.makeFindFilter(), + tag_filter: filter?.makeFilter(), + }, + }); + export const queryFindTags = (filter: ListFilterModel) => client.query({ query: GQL.FindTagsDocument, @@ -429,6 +462,16 @@ export const queryFindTags = (filter: ListFilterModel) => }, }); +// Optimized query for tag list page +export const queryFindTagsForList = (filter: ListFilterModel) => + client.query({ + query: GQL.FindTagsForListDocument, + variables: { + filter: filter.makeFindFilter(), + tag_filter: filter.makeFilter(), + }, + }); + export const queryFindTagsByIDForSelect = (tagIDs: string[]) => client.query({ query: GQL.FindTagsForSelectDocument, @@ -874,6 +917,10 @@ export const mutateSceneMerge = ( deleteObject(cache, obj, GQL.FindSceneDocument); } + cache.evict({ + id: cache.identify({ __typename: "Scene", id: destination }), + }); + evictTypeFields(cache, sceneMutationImpactedTypeFields); evictQueries(cache, [ ...sceneMutationImpactedQueries, @@ -1486,6 +1533,16 @@ export const useSceneMarkerUpdate = () => }, }); +export const useBulkSceneMarkerUpdate = () => + GQL.useBulkSceneMarkerUpdateMutation({ + update(cache, result) { + if (!result.data?.bulkSceneMarkerUpdate) return; + + evictTypeFields(cache, sceneMarkerMutationImpactedTypeFields); + evictQueries(cache, sceneMarkerMutationImpactedQueries); + }, + }); + export const useSceneMarkerDestroy = () => GQL.useSceneMarkerDestroyMutation({ update(cache, result, { variables }) { @@ -1613,17 +1670,30 @@ export const mutateAddGalleryImages = (input: GQL.GalleryAddInput) => }, }); +function evictCover(cache: ApolloCache, gallery_id: string) { + const fields: Partial, "paths" | "cover">> = {}; + fields.paths = (paths) => { + if (!("cover" in paths)) { + return paths; + } + const coverUrl = new URL(paths.cover); + coverUrl.search = "?t=" + Math.floor(Date.now() / 1000); + return { ...paths, cover: coverUrl.toString() }; + }; + fields.cover = (_value, { DELETE }) => DELETE; + cache.modify({ + id: cache.identify({ __typename: "Gallery", id: gallery_id }), + fields, + }); +} + export const mutateSetGalleryCover = (input: GQL.GallerySetCoverInput) => client.mutate({ mutation: GQL.SetGalleryCoverDocument, variables: input, update(cache, result) { if (!result.data?.setGalleryCover) return; - - cache.evict({ - id: cache.identify({ __typename: "Gallery", id: input.gallery_id }), - fieldName: "cover", - }); + evictCover(cache, input.gallery_id); }, }); @@ -1633,11 +1703,7 @@ export const mutateResetGalleryCover = (input: GQL.GalleryResetCoverInput) => variables: input, update(cache, result) { if (!result.data?.resetGalleryCover) return; - - cache.evict({ - id: cache.identify({ __typename: "Gallery", id: input.gallery_id }), - fieldName: "cover", - }); + evictCover(cache, input.gallery_id); }, }); @@ -1796,7 +1862,6 @@ export const usePerformerDestroy = () => }); evictQueries(cache, [ ...performerMutationImpactedQueries, - GQL.FindPerformersDocument, // appears with GQL.FindGroupsDocument, // filter by performers GQL.FindSceneMarkersDocument, // filter by performers ]); @@ -1836,13 +1901,48 @@ export const usePerformersDestroy = ( }); evictQueries(cache, [ ...performerMutationImpactedQueries, - GQL.FindPerformersDocument, // appears with GQL.FindGroupsDocument, // filter by performers GQL.FindSceneMarkersDocument, // filter by performers ]); }, }); +export const mutatePerformerMerge = ( + destination: string, + source: string[], + values: GQL.PerformerUpdateInput +) => + client.mutate({ + mutation: GQL.PerformerMergeDocument, + variables: { + input: { + source, + destination, + values, + }, + }, + update(cache, result) { + if (!result.data?.performerMerge) return; + + for (const id of source) { + const obj = { __typename: "Performer", id }; + deleteObject(cache, obj, GQL.FindPerformerDocument); + } + + cache.evict({ + id: cache.identify({ __typename: "Performer", id: destination }), + }); + + evictTypeFields(cache, performerMutationImpactedTypeFields); + evictQueries(cache, [ + ...performerMutationImpactedQueries, + GQL.FindGroupsDocument, // filter by performers + GQL.FindSceneMarkersDocument, // filter by performers + GQL.StatsDocument, // performer count + ]); + }, + }); + const studioMutationImpactedTypeFields = { Studio: ["child_studios"], }; @@ -1898,6 +1998,16 @@ export const useStudioUpdate = () => }, }); +export const useBulkStudioUpdate = () => + GQL.useBulkStudioUpdateMutation({ + update(cache, result) { + if (!result.data?.bulkStudioUpdate) return; + + evictTypeFields(cache, studioMutationImpactedTypeFields); + evictQueries(cache, studioMutationImpactedQueries); + }, + }); + export const useStudioDestroy = (input: GQL.StudioDestroyInput) => GQL.useStudioDestroyMutation({ variables: input, @@ -1941,6 +2051,8 @@ const tagMutationImpactedTypeFields = { }; const tagMutationImpactedQueries = [ + GQL.FindGroupsDocument, // filter by tags + GQL.FindSceneMarkersDocument, // filter by tags GQL.FindScenesDocument, // filter by tags GQL.FindImagesDocument, // filter by tags GQL.FindGalleriesDocument, // filter by tags @@ -2048,16 +2160,14 @@ export const useTagsMerge = () => deleteObject(cache, obj, GQL.FindTagDocument); } - updateStats(cache, "tag_count", -source.length); + cache.evict({ + id: cache.identify({ __typename: "Tag", id: destination }), + }); - const obj = { __typename: "Tag", id: destination }; - evictTypeFields( - cache, - tagMutationImpactedTypeFields, - cache.identify(obj) // don't evict destination tag - ); - - evictQueries(cache, tagMutationImpactedQueries); + evictQueries(cache, [ + ...tagMutationImpactedQueries, + GQL.StatsDocument, // tag count + ]); }, }); @@ -2284,6 +2394,39 @@ export const stashBoxStudioQuery = ( fetchPolicy: "network-only", }); +export const stashBoxSceneQuery = (query: string, stashBoxEndpoint: string) => + client.query( + { + query: GQL.ScrapeSingleSceneDocument, + variables: { + source: { + stash_box_endpoint: stashBoxEndpoint, + }, + input: { + query: query, + }, + }, + fetchPolicy: "network-only", + } + ); + +export const stashBoxTagQuery = ( + query: string | null, + stashBoxEndpoint: string +) => + client.query({ + query: GQL.ScrapeSingleTagDocument, + variables: { + source: { + stash_box_endpoint: stashBoxEndpoint, + }, + input: { + query: query, + }, + }, + fetchPolicy: "network-only", + }); + export const mutateStashBoxBatchPerformerTag = ( input: GQL.StashBoxBatchTagInput ) => diff --git a/ui/v2.5/src/core/config.ts b/ui/v2.5/src/core/config.ts index c1aa404f2..b0dc15c9d 100644 --- a/ui/v2.5/src/core/config.ts +++ b/ui/v2.5/src/core/config.ts @@ -45,6 +45,7 @@ export interface IUIConfig { showChildTagContent?: boolean; showChildStudioContent?: boolean; + showLinksOnPerformerCard?: boolean; showTagCardOnHover?: boolean; abbreviateCounters?: boolean; @@ -102,6 +103,8 @@ export interface IUIConfig { defaultFilters?: DefaultFilters; taggerConfig?: ITaggerConfig; + + title?: string; } export function getFrontPageContent( @@ -165,5 +168,6 @@ export function generatePremadeFrontPageContent(intl: IntlShape) { recentlyAdded(intl, FilterMode.Groups, "groups"), recentlyAdded(intl, FilterMode.Studios, "studios"), recentlyAdded(intl, FilterMode.Performers, "performers"), + recentlyAdded(intl, FilterMode.SceneMarkers, "markers"), ]; } diff --git a/ui/v2.5/src/core/performers.ts b/ui/v2.5/src/core/performers.ts index 455ada9f9..9712c9824 100644 --- a/ui/v2.5/src/core/performers.ts +++ b/ui/v2.5/src/core/performers.ts @@ -84,9 +84,14 @@ export function sortPerformers(performers: T[]) { } export const scrapedPerformerToCreateInput = ( - toCreate: GQL.ScrapedPerformer + toCreate: GQL.ScrapedPerformer, + endpoint?: string ) => { - const aliases = toCreate.aliases?.split(",").map((a) => a.trim()); + const aliases = + toCreate.aliases + ?.split(",") + .map((a) => a.trim()) + .filter((a) => a) || []; const input: GQL.PerformerCreateInput = { name: toCreate.name ?? "", @@ -118,5 +123,15 @@ export const scrapedPerformerToCreateInput = ( : undefined, circumcised: stringToCircumcised(toCreate.circumcised), }; + + if (endpoint && toCreate.remote_site_id) { + input.stash_ids = [ + { + endpoint, + stash_id: toCreate.remote_site_id, + }, + ]; + } + return input; }; diff --git a/ui/v2.5/src/core/tags.ts b/ui/v2.5/src/core/tags.ts index b62e69547..4740397e7 100644 --- a/ui/v2.5/src/core/tags.ts +++ b/ui/v2.5/src/core/tags.ts @@ -58,7 +58,7 @@ interface ITagRelationTuple { } export const tagRelationHook = ( - tag: GQL.SlimTagDataFragment | GQL.TagDataFragment, + tag: GQL.SlimTagDataFragment | GQL.TagDataFragment | GQL.TagListDataFragment, old: ITagRelationTuple, updated: ITagRelationTuple ) => { diff --git a/ui/v2.5/src/docs/en/Changelog/v0290.md b/ui/v2.5/src/docs/en/Changelog/v0290.md new file mode 100644 index 000000000..51ca93e5d --- /dev/null +++ b/ui/v2.5/src/docs/en/Changelog/v0290.md @@ -0,0 +1,76 @@ +### ✨ New Features +* Redesigned the scenes page with filter sidebar. ([#5714](https://github.com/stashapp/stash/pull/5714)) +* Added Performers tab to Group details page. ([#5895](https://github.com/stashapp/stash/pull/5895)) +* Added configurable rate limit to stash-box connection options. ([#5764](https://github.com/stashapp/stash/pull/5764)) + + +### 🎨 Improvements +* **[0.29.2]** Returned saved filters button to the top toolbar in the Scene list. ([#6215](https://github.com/stashapp/stash/pull/6215)) +* **[0.29.2]** Top pagination can now be optionally shown in the scene list with [custom css](https://github.com/stashapp/stash/pull/6234#issue-3593190476). ([#6234](https://github.com/stashapp/stash/pull/6234)) +* **[0.29.2]** Restyled the scene list toolbar based on user feedback. ([#6215](https://github.com/stashapp/stash/pull/6215)) +* **[0.29.2]** Sidebar section collapsed state is now saved in the browser history. ([#6217](https://github.com/stashapp/stash/pull/6217)) +* **[0.29.2]** Increased the number of pages in pagination dropdown to 1000. ([#6207](https://github.com/stashapp/stash/pull/6207)) +* Revamped the scene and marker wall views. ([#5816](https://github.com/stashapp/stash/pull/5816)) +* Added zoom functionality to wall views. ([#6011](https://github.com/stashapp/stash/pull/6011)) +* Added search term field to the Edit Filter dialog. ([#6082](https://github.com/stashapp/stash/pull/6082)) +* Added load and save filter buttons to the Edit Filter dialog. ([#6092](https://github.com/stashapp/stash/pull/6092)) +* Restyled UI error messages. ([#5813](https://github.com/stashapp/stash/pull/5813)) +* Changed default modifier of `path` criterion to `includes` instead of `equals`. ([#5968](https://github.com/stashapp/stash/pull/5968)) +* Added internationalisation to login page. ([#5765](https://github.com/stashapp/stash/pull/5765)) +* Added Performer and Tag popovers to scene edit page. ([#5739](https://github.com/stashapp/stash/pull/5739)) +* Tags are now sorted by name in scrape and merge dialogs. ([#5752](https://github.com/stashapp/stash/pull/5752)) +* Related stash-box is now shown with IDs in tagger view. ([#5879](https://github.com/stashapp/stash/pull/5879)) +* UI now navigates to previous page when deleting an item. ([#5818](https://github.com/stashapp/stash/pull/5818)) +* All URLs will now be submitted when submitting a draft to stash-box. ([#5894](https://github.com/stashapp/stash/pull/5894)) +* Made funscript parsing more fault tolerant. ([#5978](https://github.com/stashapp/stash/pull/5978)) +* Added link to gallery in image lightbox. ([#6012](https://github.com/stashapp/stash/pull/6012)) +* Provide correct filename when downloading scene video. ([#6119](https://github.com/stashapp/stash/pull/6119)) +* Support hardware next/previous keys for scene navigation. ([#5553](https://github.com/stashapp/stash/pull/5553)) +* Duplicate checker now sorts largest file groups first. ([#6133](https://github.com/stashapp/stash/pull/6133)) +* Show gallery cover in Gallery edit panel. ([#5935](https://github.com/stashapp/stash/pull/5935)) +* Backups will now be created in the same directory as the database, then moved to the configured backup directory. This avoids potential corruption when backing up over a network share. ([#6137](https://github.com/stashapp/stash/pull/6137)) +* Added graphql playground link to tools panel. ([#5807](https://github.com/stashapp/stash/pull/5807)) +* Include IP address in login errors in log. ([#5760](https://github.com/stashapp/stash/pull/5760)) + +### 🐛 Bug fixes +* **[0.29.3]** Fixed sidebar filter contents not loading. ([#6240](https://github.com/stashapp/stash/pull/6240)) +* **[0.29.2]** Fixed Play Random not playing from the current filtered scenes on scene list sub-pages. ([#6202](https://github.com/stashapp/stash/pull/6202)) +* **[0.29.2]** Fixed infinite loop in Group Sub-Groups panel. ([#6212](https://github.com/stashapp/stash/pull/6212)) +* **[0.29.2]** Page no longer scrolls when selecting criterion for the first time in the Edit Filter dialog. ([#6205](https://github.com/stashapp/stash/pull/6205)) +* **[0.29.2]** Zoom slider is no longer shown on mobile devices. ([#6206](https://github.com/stashapp/stash/pull/6206)) +* **[0.29.2]** Fixed trailing space sometimes being trimmed from query string when querying. ([#6211](https://github.com/stashapp/stash/pull/6211)) +* **[0.29.2]** Page now redirects to list page when deleting an object in a new browser tab. ([#6203](https://github.com/stashapp/stash/pull/6203)) +* **[0.29.2]** Related groups can now be scraped when scraping a scene. ([#6228](https://github.com/stashapp/stash/pull/6228)) +* **[0.29.2]** Fixed panic when a scraper configuration contains an unknown field. ([#6220](https://github.com/stashapp/stash/pull/6220)) +* **[0.29.2]** Fixed panic when using `stash_box_index` input in scrape API calls. ([#6201](https://github.com/stashapp/stash/pull/6201)) +* **[0.29.1]** Fixed password with special characters not allowing login. ([#6163](https://github.com/stashapp/stash/pull/6163)) +* **[0.29.1]** Fixed layout issues using column direction for image wall. ([#6168](https://github.com/stashapp/stash/pull/6168)) +* **[0.29.1]** Fixed layout issues for scene list table. ([#6169](https://github.com/stashapp/stash/pull/6169)) +* **[0.29.1]** Fixed UI loop when sorting by random without seed using URL. ([#6167](https://github.com/stashapp/stash/pull/6167)) +* Fixed ordering studios by tag count returning error. ([#5776](https://github.com/stashapp/stash/pull/5776)) +* Fixed error when submitting fingerprints for scenes that have been deleted. ([#5799](https://github.com/stashapp/stash/pull/5799)) +* Fixed errors when scraping groups. ([#5793](https://github.com/stashapp/stash/pull/5793), [#5974](https://github.com/stashapp/stash/pull/5974)) +* Fixed UI crash when viewing a gallery in the Performer details page. ([#5824](https://github.com/stashapp/stash/pull/5824)) +* Fixed scraped performer stash ID being saved when cancelling scrape operation. ([#5839](https://github.com/stashapp/stash/pull/5839)) +* Fixed groups not transferring when merging tags. ([#6127](https://github.com/stashapp/stash/pull/6127)) +* Fixed URLs and stash IDs not transferring during scene merge operation. ([#6151](https://github.com/stashapp/stash/pull/6151), [#6152](https://github.com/stashapp/stash/pull/6152)) +* Fixed empty exclusion patterns being applied when scanning and cleaning. ([#6023](https://github.com/stashapp/stash/pull/6023)) +* Fixed login page being included in browser history. ([#5747](https://github.com/stashapp/stash/pull/5747)) +* Fixed gallery card resizing while scrubbing. ([#5844](https://github.com/stashapp/stash/pull/5844)) +* Fixed incorrectly positioned scene markers in the scene player timeline. ([#5801](https://github.com/stashapp/stash/pull/5801), [#5804](https://github.com/stashapp/stash/pull/5804)) +* Fixed incorrect marker colours in the scene player timeline. ([#6141](https://github.com/stashapp/stash/pull/6141)) +* Fixed custom fields not being displayed in Performer page with `Compact Expanded Details` enabled. ([#5833](https://github.com/stashapp/stash/pull/5833)) +* Fixed issue in tagger where creating a parent studio would not map it to the other results. ([#5810](https://github.com/stashapp/stash/pull/5810), [#5996](https://github.com/stashapp/stash/pull/5996)) +* Fixed generation options not being respected when generating using the Tasks page. ([#6139](https://github.com/stashapp/stash/pull/6139)) +* Related tags are now ordered by name. ([#5945](https://github.com/stashapp/stash/pull/5945)) +* Fixed error message not being displayed when failing at startup. ([#5798](https://github.com/stashapp/stash/pull/5798)) +* Fixed incorrect paths in confirm step of the setup wizard. ([#6138](https://github.com/stashapp/stash/pull/6138)) +* Fixed values being lost when navigating back from the confirmation step of the setup wizard. ([#6138](https://github.com/stashapp/stash/pull/6138)) +* Fixed incorrect paths generated in HLS when using a reverse proxy prefix. ([#5791](https://github.com/stashapp/stash/pull/5791)) +* Fixed marker preview being deleted when modifying a marker with a duration. ([#5800](https://github.com/stashapp/stash/pull/5800)) +* Fixed marker end seconds not being included in import/export. ([#5777](https://github.com/stashapp/stash/pull/5777)) +* Fixed parent tags missing in export if including dependencies. ([#5780](https://github.com/stashapp/stash/pull/5780)) +* Add short hash of basename when generating export file names to prevent the same filename being generated. ([#5780](https://github.com/stashapp/stash/pull/5780)) +* Fixed invalid studio and performer links in the tagger view. ([#5876](https://github.com/stashapp/stash/pull/5876)) +* Fixed clickable area for tag links. ([#6129](https://github.com/stashapp/stash/pull/6129)) +* ffmpeg hardware encoding checks now timeout after 1 second to prevent startup hangs. ([#6154](https://github.com/stashapp/stash/pull/6154)) \ No newline at end of file diff --git a/ui/v2.5/src/docs/en/Changelog/v0300.md b/ui/v2.5/src/docs/en/Changelog/v0300.md new file mode 100644 index 000000000..a783932bb --- /dev/null +++ b/ui/v2.5/src/docs/en/Changelog/v0300.md @@ -0,0 +1,77 @@ +### ✨ New Features +* Added SFW content mode option to settings and setup wizard. ([#6262](https://github.com/stashapp/stash/pull/6262)) +* Added stash-ids to Tags. ([#6255](https://github.com/stashapp/stash/pull/6255)) +* Added support for manually adding stash-ids to scenes, performers, studio and tags. ([#6374](https://github.com/stashapp/stash/pull/6374)) +* Added option to link a scraped tag to an existing tag in the tagger and scrape dialog. ([#6389](https://github.com/stashapp/stash/pull/6389)) +* Partial dates (year only or month/year) are now supported for all date fields. ([#6359](https://github.com/stashapp/stash/pull/6359)) +* Added support for specifying a trash location where deleted files will be moved instead of being permanently deleted. ([#6237](https://github.com/stashapp/stash/pull/6237)) +* Logs can now be compressed after reaching a configurable size. ([#5696](https://github.com/stashapp/stash/pull/5696)) +* Added ability to edit multiple studios at once. ([#6238](https://github.com/stashapp/stash/pull/6238)) +* Added ability to edit multiple scene markers at once. ([#6239](https://github.com/stashapp/stash/pull/6239)) +* Added support for multiple Studio URLs. ([#6223](https://github.com/stashapp/stash/pull/6223)) +* Added option to add markers to front page. ([#6065](https://github.com/stashapp/stash/pull/6065)) +* Added option for instant transitions in the image lightbox. ([#6354](https://github.com/stashapp/stash/pull/6354)) +* Added duration filter to scene list sidebar. ([#6264](https://github.com/stashapp/stash/pull/6264)) +* Added support for avif images. ([#6288](https://github.com/stashapp/stash/pull/6288), [#6337](https://github.com/stashapp/stash/pull/6337)) +* Added experimental support for JPEG XL images. ([#6184](https://github.com/stashapp/stash/pull/6184)) + +### 🎨 Improvements +* Reverted scene toolbar to previous layout, consistent with other query pages. ([#6322](https://github.com/stashapp/stash/pull/6322)) +* Restored display mode button group to list pages. ([#6317](https://github.com/stashapp/stash/pull/6317)) +* Added sticky selection toolbar to all list views. ([#6320](https://github.com/stashapp/stash/pull/6320)) +* Added performer age slider to scene filter sidebar. ([#6267](https://github.com/stashapp/stash/pull/6267)) +* Added markers option to scene filter sidebar. ([#6270](https://github.com/stashapp/stash/pull/6270)) +* Selected stash-box is now remembered in the scene tagger view. ([#6192](https://github.com/stashapp/stash/pull/6192)) +* Replaced `Show male performers` tagger option with a list of genders to include. ([#6321](https://github.com/stashapp/stash/pull/6321)) +* Galleries can now be created using the gallery select control. ([#6376](https://github.com/stashapp/stash/pull/6376)) +* String list inputs can now be re-ordered. ([#6397](https://github.com/stashapp/stash/pull/6397)) +* Added auto-start button to scene player. ([#6368](https://github.com/stashapp/stash/pull/6368)) +* Bulk add tasks now accept stash ids in addition to names. ([#6310](https://github.com/stashapp/stash/pull/6310)) +* Image query metadata (total file size and megapixels) is now performed as a separate query to the main query to improve performance. ([#6370](https://github.com/stashapp/stash/pull/6370)) +* Removed some unused fields in the tag list query to improve performance. ([#6398](https://github.com/stashapp/stash/pull/6398)) +* Added hardware encoding support for Rockchip RKMPP devices. ([#6182](https://github.com/stashapp/stash/pull/6182)) +* stash now uses the Media Session API when playing scenes. ([#6298](https://github.com/stashapp/stash/pull/6298)) +* Screen sleeping is now prevented when playing scenes (only in secure contexts: `localhost` or https). ([#6331](https://github.com/stashapp/stash/pull/6331)) +* Whitespace is now trimmed from the start and end of text fields. ([#6226](https://github.com/stashapp/stash/pull/6226)) +* Added `inputURL` and `inputHostname` fields to scraper specs. ([#6250](https://github.com/stashapp/stash/pull/6250)) +* Added extra studio fields to scraper specs. ([#6249](https://github.com/stashapp/stash/pull/6249)) +* Added o-count to studio cards and details page. ([#5982](https://github.com/stashapp/stash/pull/5982)) +* Added o-count to group cards. ([#6122](https://github.com/stashapp/stash/pull/6122)) +* Added options to filter and sort groups by o-count. ([#6122](https://github.com/stashapp/stash/pull/6122)) +* Added o-count to performer details page. ([#6171](https://github.com/stashapp/stash/pull/6171)) +* Added option to sort by total scene direction for performers, studios and tags. ([#6172](https://github.com/stashapp/stash/pull/6172)) +* Added option to sort scenes by Performer age. ([#6009](https://github.com/stashapp/stash/pull/6009)) +* Added option to sort scenes by Studio. ([#6155](https://github.com/stashapp/stash/pull/6155)) +* Added option to show external links on Performer cards. ([#6153](https://github.com/stashapp/stash/pull/6153)) +* Improved dimension detection for webp files. ([#6342](https://github.com/stashapp/stash/pull/6342)) +* Added keyboard shortcuts to generate scene screenshot at current time (`c c`) and to regenerate default screenshot (`c d`). ([#5984](https://github.com/stashapp/stash/pull/5984)) +* Added keyboard shortcut for tagger view (`v t`). ([#6261](https://github.com/stashapp/stash/pull/6261)) +* Custom field values are now displayed truncated to 5 lines. ([#6361](https://github.com/stashapp/stash/pull/6361)) + +### 🐛 Bug fixes +* **[0.30.1]** fixed hardware encode tests preventing desktop features from working correctly. ([#6417](https://github.com/stashapp/stash/pull/6417)) +* **[0.30.1]** fixed Handy integration not functioning correctly. ([#6425](https://github.com/stashapp/stash/pull/6425)) +* **[0.30.1]** fixed gallery create graphql interface not setting organised flag. ([#6418](https://github.com/stashapp/stash/pull/6418)) +* stash-ids are now set when creating new objects from the scrape dialog. ([#6269](https://github.com/stashapp/stash/pull/6269)) +* partial dates are now correctly handled when scraping scenes. ([#6305](https://github.com/stashapp/stash/pull/6305)) +* Fixed zoom keyboard shortcuts not working. ([#6317](https://github.com/stashapp/stash/pull/6317)) +* Fixed inline videos showing as full-screen on iPhone devices. ([#6259](https://github.com/stashapp/stash/pull/6259)) +* Fixed external player not loading on Android when a scene title has special characters. ([#6297](https://github.com/stashapp/stash/pull/6297)) +* Play activity will now be recorded correctly when reaching the end of a video. ([#6334](https://github.com/stashapp/stash/pull/6334)) +* Fixed markers appearing in the wrong location when player is in fullscreen mode. ([#6323](https://github.com/stashapp/stash/pull/6323)) +* Fixed selected studio/performer being reset when saving a scene in the tagger view. ([#6391](https://github.com/stashapp/stash/pull/6391), [#6409](https://github.com/stashapp/stash/pull/6409)) +* Fixed performer becoming unmatched when creating a new performer with the same name is created. ([#6308](https://github.com/stashapp/stash/pull/6308)) +* Fixed tagger options and buttons not being visible when there are no scenes in the result list. ([#6316](https://github.com/stashapp/stash/pull/6316)) +* Fixed error when scraping a studio if the alias field was empty. ([#6313](https://github.com/stashapp/stash/pull/6313)) +* Fixed existing match stash ID sometimes not being displayed in the performer scrape dialog. ([#6257](https://github.com/stashapp/stash/pull/6257)) +* Fixed download backup function not working when generated directory is on a different filesystem. ([#6244](https://github.com/stashapp/stash/pull/6244)) +* Fixed issue where duplicate file entries would be created if a file was modified and renamed with a different case on case-insensitive filesystems. ([#6327](https://github.com/stashapp/stash/pull/6327)) +* Hardware encoding tests are now performed concurrently at startup to reduce startup time. ([#6414](https://github.com/stashapp/stash/pull/6414)) +* Fixed scraper and plugin locations being converted to absolute paths during setup. ([#6373](https://github.com/stashapp/stash/pull/6373)) +* Fixed Macos version check pointing to incorrect location. ([#6289](https://github.com/stashapp/stash/pull/6289)) +* stash will no longer try to generate marker previews where a marker start is set after the end of a scene's duration. ([#6290](https://github.com/stashapp/stash/pull/6290)) +* Fixed panic when scraping a performer with no measurement value. ([#6367](https://github.com/stashapp/stash/pull/6367)) + +### Api Changes + +* added `remove` field to `CustomFieldsInput` to allow removing specific custom fields when updating objects. ([#6362](https://github.com/stashapp/stash/pull/6362)) \ No newline at end of file diff --git a/ui/v2.5/src/docs/en/Manual/AutoTagging.md b/ui/v2.5/src/docs/en/Manual/AutoTagging.md index c311bed7a..4c4265e19 100644 --- a/ui/v2.5/src/docs/en/Manual/AutoTagging.md +++ b/ui/v2.5/src/docs/en/Manual/AutoTagging.md @@ -1,18 +1,48 @@ -# Auto Tagging +# Auto Tag -When media filepaths or filenames contain a Performer, Studio, or Tag name, it is assigned those Performers, Studios, and Tags. It will **only** tag based on Performer, Studio, and Tag names that exist in your database. +Auto Tag automatically assigns Performers, Studios, and Tags to your media based on their names found in file paths or filenames. This task works for scenes, images, and galleries. -When the Performer/Studio/Tag name has multiple words, the search will include paths/filenames where the Performer/Studio/Tag name is separated with `.`, `-`, `_`, and whitespace characters. +This task is part of the advanced settings mode. -For example, auto tagging for performer `Jane Doe` will match the following filenames: +## Rules -* `Jane.Doe.1.mp4` -* `Jane_Doe.2.mp4` -* `Jane-Doe.3.mp4` -* `Jane Doe.4.mp4` +> **⚠️ Important:** Auto Tag only works for names that already exist in your Stash database. It does not create new Performers, Studios, or Tags. -Matching is case insensitive, and should only match exact wording within word boundaries. For example, the tag `Jane Doe` will not match `Maryjane-Doe` or `Jane-Doen`, but will match `Mary-Jane-Doe`, `Jane-Doe_n`, and `[OF]jane doe`. + - Multi-word names are matched when words appear in order and are separated by any of these characters: `.`, `-`, `_`, or whitespace. These separators are treated as word boundaries. + - Matching is case-insensitive but requires complete words within word boundaries. Partial words or misspelled words will not match. + - Auto Tag does not match performer aliases. Aliases will not be considered during matching. -Auto tagging for specific Performers, Studios, and Tags can be performed from the individual Performer/Studio/Tag page. +### Examples (performer "Jane Doe") -> **Note:** Performer autotagging does not currently match on performer aliases. \ No newline at end of file +**Matches:** + +| Example | Explanation | +|---|---| +| `Jane.Doe.1.mp4` | Dot as separator. | +| `Jane_Doe.2.mp4` | Underscore as separator. | +| `Jane-Doe.3.mp4` | Hyphen as separator. | +| `Jane Doe.4.mp4` | Whitespace as separator. | +| `Mary-Jane-Doe` | Extra characters around word boundaries are allowed. | +| `Jane-Doe_n` | Extra characters around word boundaries are allowed. | +| `[OF]jane doe` | Extra characters around word boundaries are allowed. | + +**Does not match:** + +| Example | Explanation | +|---|---| +| `Maryjane-Doe` | Combined words without separator. | +| `Jane-Doen` | Spelling mismatch. | + +### Organized flag + +Scenes, images, and galleries that have the Organized flag added to them will not be modified by Auto Tag. You can also use Organized flag status as a filter. + +### Ignore Auto Tag flag + +Performers or Tags that have Ignore Auto Tag flag added to them will be skipped by the Auto Tag task. + +## Running task + +- **Auto Tag:** You can run the Auto Tag task on your entire library from the Tasks page. +- **Selective Auto Tag:** You can run the Auto Tag task on specific directories from the Tasks page. +- **Individual pages:** You can run Auto Tag tasks for specific Performers, Studios, and Tags from their respective pages. diff --git a/ui/v2.5/src/docs/en/Manual/Captions.md b/ui/v2.5/src/docs/en/Manual/Captions.md index df2bee8bc..4e3849fac 100644 --- a/ui/v2.5/src/docs/en/Manual/Captions.md +++ b/ui/v2.5/src/docs/en/Manual/Captions.md @@ -15,4 +15,4 @@ Where `{language_code}` is defined by the [ISO-6399-1](https://en.wikipedia.org/ Scenes with captions can be filtered with the `captions` criterion. -**Note:** If the caption file was added after the scene was initially added during scan, you will need to run a Selective Scan task for it to show up. +> **⚠️ Note:** If the caption file was added after the scene was initially added during scan, you will need to run a Selective Scan task for it to show up. diff --git a/ui/v2.5/src/docs/en/Manual/Configuration.md b/ui/v2.5/src/docs/en/Manual/Configuration.md index 9b0469114..d5841b559 100644 --- a/ui/v2.5/src/docs/en/Manual/Configuration.md +++ b/ui/v2.5/src/docs/en/Manual/Configuration.md @@ -2,7 +2,13 @@ ## Library -This section allows you to add and remove directories from your library list. Files in these directories will be included when scanning. Files that are outside of these directories will be removed when running the Clean task. +This section enables you to add or remove directories that will be discoverable by Stash. The directories you add will be utilized for scanning new files and for updating their locations in Stash database. + +You can configure these directories to apply specifically to: + +- **Videos** +- **Images** +- **Both** > **⚠️ Note:** Don't forget to click `Save` after updating these directories! @@ -25,7 +31,7 @@ Some examples: - `"^/stash/videos/exclude/"` will exclude all directories that match `/stash/videos/exclude/` pattern. - `"\\\\stash\\network\\share\\excl\\"` will exclude specific Windows network path `\\stash\network\share\excl\`. -> **Note:** If a directory is excluded for images and videos, then the directory will be excluded from scans completely. +> **⚠️ Note:** If a directory is excluded for images and videos, then the directory will be excluded from scans completely. _There is a useful [regex101](https://regex101.com/) site that can help test and experiment with regexps._ @@ -81,7 +87,7 @@ This setting can be used to increase/decrease overall CPU utilisation in two sce 1. High performance 4+ core cpus. 2. Media files stored on remote/cloud filesystem. -Note: If this is set too high it will decrease overall performance and causes failures (out of memory). +> **⚠️ Note:** If this is set too high it will decrease overall performance and causes failures (out of memory). ## Hardware accelerated live transcoding @@ -111,7 +117,7 @@ Some scrapers require a Chrome instance to function correctly. If left empty, st `Chrome CDP path` can be set to a path to the chrome executable, or an http(s) address to remote chrome instance (for example: `http://localhost:9222/json/version`). -> **Important**: As of Chrome 136 you need to specify `--user-data-dir` alongside `--remote-debugging-port`. Read more on their [official post](https://developer.chrome.com/blog/remote-debugging-port). +> **⚠️ Important:** As of Chrome 136 you need to specify `--user-data-dir` alongside `--remote-debugging-port`. Read more on their [official post](https://developer.chrome.com/blog/remote-debugging-port). ## Authentication @@ -159,6 +165,12 @@ The following environment variables are also supported: |----------------------|---------| | `STASH_SQLITE_CACHE_SIZE` | Sets the SQLite cache size. See https://www.sqlite.org/pragma.html#pragma_cache_size. Default is `-2000` which is 2MB. | +### Custom favicon + +You can provide a custom favicon by placing a `favicon.ico` file in the configuration directory. The configuration directory is located alongside the `config.yml` file. + +When a custom favicon is provided, it will be served instead of the default embedded favicon. + ### Custom served folders Custom served folders are served when the server handles a request with the `/custom` URL prefix. The following is an example configuration: diff --git a/ui/v2.5/src/docs/en/Manual/Contributing.md b/ui/v2.5/src/docs/en/Manual/Contributing.md index 9e55dbd6d..2d62dde08 100644 --- a/ui/v2.5/src/docs/en/Manual/Contributing.md +++ b/ui/v2.5/src/docs/en/Manual/Contributing.md @@ -26,4 +26,4 @@ We welcome ideas for future improvements and features, and bug reports help ever ## Providing support -Offering support for new users on [Discord](https://discord.gg/2TsNFKt) is also welcomed. +Offering support for new users on our [Community forum](https://discourse.stashapp.cc/) and [Discord](https://discord.gg/2TsNFKt) is also welcomed. diff --git a/ui/v2.5/src/docs/en/Manual/Deduplication.md b/ui/v2.5/src/docs/en/Manual/Deduplication.md index 24c0fb391..d842fcc68 100644 --- a/ui/v2.5/src/docs/en/Manual/Deduplication.md +++ b/ui/v2.5/src/docs/en/Manual/Deduplication.md @@ -2,8 +2,10 @@ [The dupe checker](/sceneDuplicateChecker) searches your collection for scenes that are perceptually similar. This means that the files don't need to be identical, and will be identified even with different bitrates, resolutions, and intros/outros. -To achieve this stash needs to generate what's called a phash, or perceptual hash. Similar to sprite generation stash will generate a set of 25 images from fixed points in the scene. These images will be stitched together, and then hashed using the phash algorithm. The phash can then be used to find scenes that are the same or similar to others in the database. Phash generation can be run during scan, or as a separate task. Note that generation can take a while due to the work involved with extracting screenshots. +To achieve this stash needs to generate what's called a phash, or perceptual hash. Similar to sprite generation stash will generate a set of 25 images from fixed points in the scene. These images will be stitched together, and then hashed using the phash algorithm. The phash can then be used to find scenes that are the same or similar to others in the database. Phash generation can be run during scan, or as a separate task. + +> **⚠️ Note:** Generation can take a while due to the work involved with extracting screenshots. The dupe checker can be run with four different levels of accuracy. `Exact` looks for scenes that have exactly the same phash. This is a fast and accurate operation that should not yield any false positives except in very rare cases. The other accuracy levels look for duplicate files within a set distance of each other. This means the scenes don't have exactly the same phash, but are very similar. `High` and `Medium` should still yield very good results with few or no false positives. `Low` is likely to produce some false positives, but might still be useful for finding dupes. -Note that to generate a phash stash requires an uncorrupted file. If any errors are encountered during sprite generation the phash will not be generated. This is to prevent false positives. +> **⚠️ Note:** To generate a pHash Stash requires an uncorrupted file. If any errors are encountered during sprite generation the pHash will not be generated. This is to prevent false positives. \ No newline at end of file diff --git a/ui/v2.5/src/docs/en/Manual/EmbeddedPlugins.md b/ui/v2.5/src/docs/en/Manual/EmbeddedPlugins.md index 1fc217ffc..9d54010e6 100644 --- a/ui/v2.5/src/docs/en/Manual/EmbeddedPlugins.md +++ b/ui/v2.5/src/docs/en/Manual/EmbeddedPlugins.md @@ -10,7 +10,9 @@ Stash currently supports Javascript embedded plugin tasks using [goja](https://g ### Plugin input -The input is provided to Javascript plugin tasks using the `input` global variable, and is an object based on the structure provided in the `Plugin input` section of the [Plugins](/help/Plugins.md) page. Note that the `server_connection` field should not be necessary in most embedded plugins. +The input is provided to Javascript plugin tasks using the `input` global variable, and is an object based on the structure provided in the `Plugin input` section of the [Plugins](/help/Plugins.md) page. + +> **⚠️ Note:** `server_connection` field should not be necessary in most embedded plugins. ### Plugin output diff --git a/ui/v2.5/src/docs/en/Manual/Help.md b/ui/v2.5/src/docs/en/Manual/Help.md index f5d1d69e5..c9c4ab9d3 100644 --- a/ui/v2.5/src/docs/en/Manual/Help.md +++ b/ui/v2.5/src/docs/en/Manual/Help.md @@ -1,5 +1,7 @@ # Where to get further help +Join our [Community forum](https://discourse.stashapp.cc/). + Join our [Discord](https://discord.gg/2TsNFKt). The [Stash-Docs](https://docs.stashapp.cc) covers some areas not covered in the in-app help. diff --git a/ui/v2.5/src/docs/en/Manual/Identify.md b/ui/v2.5/src/docs/en/Manual/Identify.md index cacd27923..724a392a3 100644 --- a/ui/v2.5/src/docs/en/Manual/Identify.md +++ b/ui/v2.5/src/docs/en/Manual/Identify.md @@ -1,35 +1,50 @@ # Identify -This task iterates through your Scenes and attempts to identify the scene using a selection of scraping sources. +The Identify task iterates through your Scenes and attempts to identify them using a selection of scraping sources. If a result is found in a source, the Scene is updated, and no further sources are checked for that scene. -This task accepts one or more scraper sources. Valid scraper sources for the Identify task are stash-box instances, and scene scrapers which support scraping via Scene Fragment. The order of the sources may be rearranged. +This task is part of the advanced settings mode. -For each Scene, the Identify task iterates through the scraper sources, in the order provided, and tries to identify the scene using each source. If a result is found in a source, then the Scene is updated, and no further sources are checked for that scene. +## Rules + +- The task accepts one or more scraper sources, including stash-box instances and scene scrapers that support scraping via Scene Fragment. The order of the sources can be rearranged. +- The task iterates through the scraper sources in the provided order. +- If a result is found in a source, the Scene is updated, and further sources are not checked for that scene. + +### Organized flag + +Scenes that have the Organized flag added to them will not be modified by Identify. You can also use Organized flag status as a filter. ## Options -The following options can be set: +The following options can be configured: | Option | Description | |--------|-------------| -| Include male performers | If false, then male performers will not be created or set on scenes. | -| Set cover images | If false, then scene cover images will not be modified. | -| Set organised flag | If true, the organised flag is set to true when a scene is organised. | +| Include male performers | If false, male performers will not be created or set on scenes. | +| Set cover images | If false, scene cover images will not be modified. | +| Set organized flag | If true, the organized flag is set to true when a scene is organized. | | Skip matches that have more than one result | If this is not enabled and more than one result is returned, one will be randomly chosen to match | | Tag skipped matches with | If the above option is set and a scene is skipped, this will add the tag so that you can filter for it in the Scene Tagger view and choose the correct match by hand | | Skip single name performers with no disambiguation | If this is not enabled, performers that are often generic like Samantha or Olga will be matched | -| Tag skipped performers with | If the above options is set and a performer is skipped, this will add the tag so that you can filter for in it the Scene Tagger view and choose how you want to handle those performers | +| Tag skipped performers with | If the above option is set and a performer is skipped, this will add the tag so that you can filter for it in the Scene Tagger view and choose how you want to handle those performers | -Field specific options may be set as well. Each field may have a Strategy. The behaviour for each strategy value is as follows: +### Field specific options + +Each field may have a strategy. The behavior for each strategy is as follows: | Strategy | Description | |----------|-------------| -| Ignore | Not set. | -| Overwrite | Overwrite existing value. | +| Ignore | The field is not set. | +| Overwrite | Existing values are overwritten. | | Merge (*default*) | For multi-value fields, adds to existing values. For single-value fields, only sets if not already set. | -For Studio, Performers and Tags, an option is also available to Create Missing objects. This is enabled by default. When true, if a Studio/Performer/Tag is included during the identification process and does not exist in the system, then it will be created. +For Studio, Performers, and Tags, an option is available to **Create Missing Objects**. This is enabled by default. When true, if a Studio/Performer/Tag is included during the identification process and does not exist in the system, it will be created. -Default Options are applied to all sources unless overridden in specific source options. +## Running task + +- **Identify...:** Run the Identify task on your entire library from the Tasks page. +- **Selective Identify:** Configure and run the Identify task on specific directories from Tasks > Identify... page. At the top of the page click folder icon to select directories. + +## Logs The result of the identification process for each scene is output to the log. diff --git a/ui/v2.5/src/docs/en/Manual/Images.md b/ui/v2.5/src/docs/en/Manual/Images.md index 7b384596b..f08f5241c 100644 --- a/ui/v2.5/src/docs/en/Manual/Images.md +++ b/ui/v2.5/src/docs/en/Manual/Images.md @@ -11,8 +11,12 @@ You can add images to every gallery manually in the gallery detail page. Deletin For best results, images in zip file should be stored without compression (copy, store or no compression options depending on the software you use. Eg on linux: `zip -0 -r gallery.zip foldertozip/`). This impacts **heavily** on the zip read performance. +> **⚠️ Note:** AVIF files in ZIP archives are currently unsupported. + If a filename of an image in the gallery zip file ends with `cover.jpg`, it will be treated like a cover and presented first in the gallery view page and as a gallery cover in the gallery list view. If more than one images match the name the first one found in natural sort order is selected. +You can also manually select any image from a gallery as its cover. On the gallery details page, select the desired cover image, and then select **Set as Cover** in the ⋯ menu. + ## Image clips/gifs Images can also be clips/gifs. These are meant to be short video loops. Right now they are not possible in zipfiles. To declare video files to be images, there are two ways: diff --git a/ui/v2.5/src/docs/en/Manual/Interface.md b/ui/v2.5/src/docs/en/Manual/Interface.md index 31c7e25d4..cf5911405 100644 --- a/ui/v2.5/src/docs/en/Manual/Interface.md +++ b/ui/v2.5/src/docs/en/Manual/Interface.md @@ -4,6 +4,15 @@ Setting the language affects the formatting of numbers and dates. +## SFW Content Mode + +SFW Content Mode is used to indicate that the content being managed is _not_ adult content. + +When SFW Content Mode is enabled, the following changes are made to the UI: +- default performer images are changed to less adult-oriented images +- certain adult-specific metadata fields are hidden (e.g. performer genital fields) +- `O`-Counter is replaced with `Like`-counter + ## Scene/Marker Wall Preview Type The Scene Wall and Marker pages display scene preview videos (mp4) by default. This can be changed to animated image (webp) or static image. diff --git a/ui/v2.5/src/docs/en/Manual/Introduction.md b/ui/v2.5/src/docs/en/Manual/Introduction.md index 1496ad2b1..f32b84681 100644 --- a/ui/v2.5/src/docs/en/Manual/Introduction.md +++ b/ui/v2.5/src/docs/en/Manual/Introduction.md @@ -2,6 +2,8 @@ Stash works by cataloging your media using the paths that you provide. Once you have [configured](/settings?tab=library) the locations where your media is stored, you can click the Scan button in [`Settings -> Tasks`](/settings?tab=tasks) and stash will begin scanning and importing your media into its library. -For the best experience, it is recommended that after a scan is finished, that video previews and sprites are generated. You can do this in [`Settings -> Tasks`](/settings?tab=tasks). Note that currently it is only possible to perform one task at a time and but there is a task queue, so the generate tasks should be performed after scan is complete. +For the best experience, it is recommended that after a scan is finished, that video previews and sprites are generated. You can do this in [`Settings -> Tasks`](/settings?tab=tasks). + +> **⚠️ Note:** Currently it is only possible to perform one task at a time and but there is a task queue, so the generate tasks should be performed after scan is complete. Once your media is imported, you are ready to begin creating Performers, Studios and Tags, and curating your content! \ No newline at end of file diff --git a/ui/v2.5/src/docs/en/Manual/JSONSpec.md b/ui/v2.5/src/docs/en/Manual/JSONSpec.md index 0a53d09f2..b071f26cc 100644 --- a/ui/v2.5/src/docs/en/Manual/JSONSpec.md +++ b/ui/v2.5/src/docs/en/Manual/JSONSpec.md @@ -24,7 +24,7 @@ When exported, files are named with different formats depending on the object ty | Studios | `.json` | | Groups | `.json` | -Note that the file naming is not significant when importing. All json files will be read from the subdirectories. +> **⚠️ Note:** The file naming is not significant when importing. All json files will be read from the subdirectories. ## Content of the json files diff --git a/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md b/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md index 870de61b5..f6cd29334 100644 --- a/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md +++ b/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md @@ -30,6 +30,7 @@ | `v g` | Set view to grid | | `v l` | Set view to list | | `v w` | Set view to wall | +| `v t` | Set view to tagger | | `+` | Increase zoom slider | | `-` | Decrease zoom slider | | `←` | Previous page of results | @@ -40,6 +41,7 @@ | `Ctrl + End` | Go to last page of results | | `s a` | Select all on page | | `s n` | Unselect all | +| `s i` | Invert selection | | `e` | Edit selected | | `d d` | Delete selected | @@ -67,6 +69,9 @@ | `r 0` | Unset rating (stars) | | `r {0-9} {0-9}` | Set rating (decimal - `00` for `10.0`) | | ``r ` `` | Unset rating (decimal) | +| Cover generation || +| `c c` | Generate screenshot at current time | +| `c d` | Generate default screenshot | | Playback || | `p n` | Play next scene in queue | | `p p` | Play previous scene in queue | diff --git a/ui/v2.5/src/docs/en/Manual/Plugins.md b/ui/v2.5/src/docs/en/Manual/Plugins.md index f7517aa2c..5e403af92 100644 --- a/ui/v2.5/src/docs/en/Manual/Plugins.md +++ b/ui/v2.5/src/docs/en/Manual/Plugins.md @@ -65,8 +65,11 @@ Plugins provide tasks which can be run from the Tasks page. The basic structure of a plugin configuration file is as follows: -``` -name: +```yaml +name: +# optional list of dependencies to be included +# "#" is is part of the config - do not remove +# requires: description: version: url: @@ -121,6 +124,8 @@ tasks: The `name`, `description`, `version` and `url` fields are displayed on the plugins page. +`# requires` will make the plugin manager select plugins matching the specified IDs to be automatically installed as dependencies. Only works with plugins within the same index. + The `exec`, `interface`, `errLog` and `tasks` fields are used only for plugins with tasks. The `settings` field is used to display plugin settings on the plugins page. Plugin settings can also be set using the graphql mutation `configurePlugin` - the settings set this way do _not_ need to be specified in the `settings` field unless they are to be displayed in the stock plugin settings UI. @@ -235,7 +240,7 @@ hooks: argKey: argValue ``` -**Note:** it is possible for hooks to trigger eachother or themselves if they perform mutations. For safety, hooks will not be triggered if they have already been triggered in the context of the operation. Stash uses cookies to track this context, so it's important for plugins to send cookies when performing operations. +**⚠️ Note:** It is possible for hooks to trigger eachother or themselves if they perform mutations. For safety, hooks will not be triggered if they have already been triggered in the context of the operation. Stash uses cookies to track this context, so it's important for plugins to send cookies when performing operations. #### Trigger types diff --git a/ui/v2.5/src/docs/en/Manual/ScraperDevelopment.md b/ui/v2.5/src/docs/en/Manual/ScraperDevelopment.md index 0ee1c0880..4c97e3fcf 100644 --- a/ui/v2.5/src/docs/en/Manual/ScraperDevelopment.md +++ b/ui/v2.5/src/docs/en/Manual/ScraperDevelopment.md @@ -325,10 +325,58 @@ Alternatively, an attribute value may be set to a fixed value, rather than scrap ```yaml performer: - Gender: + Gender: fixed: Female ``` +### Input URL placeholders + +The `{inputURL}` and `{inputHostname}` placeholders can be used in both `fixed` values and `selector` expressions to access information about the original URL that was used to scrape the content. + +#### {inputURL} + +The `{inputURL}` placeholder provides access to the full URL. This is useful when you want to return or reference the source URL as part of the scraped data. + +For example: + +```yaml +scene: + URL: + fixed: "{inputURL}" + Title: + selector: //h1[@class="title"] +``` + +When scraping from `https://example.com/scene/12345`, the `{inputURL}` placeholder will be replaced with `https://example.com/scene/12345`. + +#### {inputHostname} + +The `{inputHostname}` placeholder extracts just the hostname from the URL. This is useful when you need to reference the domain without manually parsing the URL. + +For example: + +```yaml +scene: + Studio: + fixed: "{inputHostname}" + Details: + selector: //div[@data-domain="{inputHostname}"]//p[@class="description"] +``` + +When scraping from `https://example.com/scene/12345`, the `{inputHostname}` placeholder will be replaced with `example.com`. + +These placeholders can also be used within selectors for more advanced use cases: + +```yaml +scene: + Details: + selector: //div[@data-url="{inputURL}"]//p[@class="description"] + Site: + selector: //div[@data-host="{inputHostname}"]//span[@class="site-name"] +``` + +> **⚠️ Note:** These placeholders represent the actual URL used to fetch the content, after any URL replacements have been applied. + ### Common fragments The `common` field is used to configure selector fragments that can be referenced in the selector strings. These are key-value pairs where the key is the string to reference the fragment, and the value is the string that the fragment will be replaced with. For example: @@ -343,6 +391,7 @@ performer: The `Measurements` xpath string will replace `$infoPiece` with `//div[@class="infoPiece"]/span`, resulting in: `//div[@class="infoPiece"]/span[text() = 'Measurements:']/../span[@class="smallInfo"]`. > **⚠️ Note:** Recursive common fragments are **not** supported. + Referencing a common fragment within another common fragment will cause an error. For example: ```yaml common: @@ -691,7 +740,11 @@ xPathScrapers: URL: $performer/@href Studio: Name: $studio - URL: $studio/@href + URL: $studio/@href + Details: //div[@class="studioDescription"] + Aliases: //div[@class="studioAliases"]/span + Tags: + Name: //div[@class="studioTags"]/a ``` See also [#333](https://github.com/stashapp/stash/pull/333) for more examples. @@ -774,6 +827,11 @@ jsonScrapers: Name: data.performers.#.name Studio: Name: data.site.name + URL: data.site.url + Details: data.site.description + Aliases: data.site.aliases + Tags: + Name: data.site.tags.#.name Tags: Name: data.tags.#.tag @@ -791,6 +849,11 @@ jsonScrapers: Name: $data.performers.#.name Studio: Name: $data.site.name + URL: $data.site.url + Details: $data.site.description + Aliases: $data.site.aliases + Tags: + Name: $data.site.tags.#.name Tags: Name: $data.tags.#.tag driver: @@ -819,7 +882,7 @@ Title URLs ``` -> **Important**: `Title` field is required. +> **⚠️ Important:** `Title` field is required. ### Group @@ -838,7 +901,7 @@ Tags (see Tag fields) URLs ``` -> **Important**: `Name` field is required. +> **⚠️ Important:** `Name` field is required. ### Image @@ -882,9 +945,9 @@ URLs Weight ``` -> **Important**: `Name` field is required. +> **⚠️ Important:** `Name` field is required. -> **Note:** - `Gender` must be one of `male`, `female`, `transgender_male`, `transgender_female`, `intersex`, `non_binary` (case insensitive). +> **⚠️ Note:** `Gender` must be one of `male`, `female`, `transgender_male`, `transgender_female`, `intersex`, `non_binary` (case insensitive). ### Scene @@ -902,16 +965,19 @@ Title URLs ``` -> **Important**: `Title` field is required only if fileless. +> **⚠️ Important:** `Title` field is required only if fileless. ### Studio ``` +Aliases +Details Name +Tags (see Tag fields) URL ``` -> **Important**: `Name` field is required. +> **⚠️ Important:** `Name` field is required. ### Tag @@ -919,4 +985,4 @@ URL Name ``` -> **Important**: `Name` field is required. +> **⚠️ Important:** `Name` field is required. diff --git a/ui/v2.5/src/docs/en/Manual/Tagger.md b/ui/v2.5/src/docs/en/Manual/Tagger.md index ba9e5f17a..7c2d12a87 100644 --- a/ui/v2.5/src/docs/en/Manual/Tagger.md +++ b/ui/v2.5/src/docs/en/Manual/Tagger.md @@ -4,9 +4,9 @@ Stash can be integrated with stash-box which acts as a centralized metadata data ## Searching -The fingerprint search matches your current selection of files against the remote stash-box instance. Any scenes with a matching fingerprint will be returned, although there is currently no validation of fingerprints so it’s recommended to double-check the validity before saving. +The fingerprint search matches your current selection of files against the remote stash-box instance. Any scenes with a matching fingerprint will be returned, although there is currently no validation of fingerprints so it's recommended to double-check the validity before saving. -If no fingerprint match is found it’s possible to search by keywords. The search works by matching the query against a scene’s _title_, _release date_, _studio name_, and _performer names_. By default the tagger uses metadata set on the file, or parses the filename, this can be changed in the config. +If no fingerprint match is found it's possible to search by keywords. The search works by matching the query against a scene's _title_, _release date_, _studio name_, and _performer names_. By default the tagger uses metadata set on the file, or parses the filename, this can be changed in the config. An important thing to note is that it only returns a match *if all query terms are a match*. As an example, if a scene is titled `"A Trip to the Mall"` with the performer `"Jane Doe"`, a search for `"Trip to the Mall 1080p"` will *not* match, however `"trip mall doe"` would. Usually a few pieces of info is enough, for instance performer name + release date or studio name. To avoid common non-related keywords you can add them to the blacklist in the tagger config. Any items in the blacklist are stripped out of the query. diff --git a/ui/v2.5/src/docs/en/Manual/Tasks.md b/ui/v2.5/src/docs/en/Manual/Tasks.md index aa46f72bb..063d02277 100644 --- a/ui/v2.5/src/docs/en/Manual/Tasks.md +++ b/ui/v2.5/src/docs/en/Manual/Tasks.md @@ -18,10 +18,11 @@ The scan task accepts the following options: | Generate previews | Generates video previews (mp4) which play when hovering over a scene. | | Generate animated image previews* | *Accessible in Advanced Mode* - Also generate animated (webp) previews, only required when Scene/Marker Wall Preview Type is set to Animated Image. When browsing they use less CPU than the video previews, but are generated in addition to them and are larger files.| | Generate scrubber sprites | The set of images displayed below the video player for easy navigation. | -| Generate perceptual hashes | Generates perceptual hashes for scene deduplication and identification. | +| Generate video perceptual hashes | Generates perceptual hashes for scene deduplication and identification. | | Generate thumbnails for images | Generates thumbnails for image files. | +| Generate image perceptual hashes | Generates perceptual hashes for image deduplication and identification. | | Generate previews for image clips | Generates a gif/looping video as thumbnail for image clips/gifs. | -| Rescan | By default, Stash will only rescan existing files if the file's modified date has been updated since its previous scan. Stash will rescan files in the path when this option is enabled, regardless of the file modification time. Only required Stash needs to recalculate video/image metadata, or to rescan gallery zips. | +| Rescan | By default, Stash will only rescan existing files if the file's modified date has been updated since its previous scan. Stash will rescan files in the path when this option is enabled, regardless of the file modification time. Only required if Stash needs to recalculate video/image metadata, or to rescan gallery zips. | ## Auto Tagging See the [Auto Tagging](/help/AutoTagging.md) page. @@ -31,14 +32,16 @@ See the [Scene Filename Parser](/help/SceneFilenameParser.md) page. ## Generated Content -The scanning function automatically generates a screenshot of each scene. The generated content provides the following: +The generated content provides the following: +* Scene covers - screenshot of the scene used as the cover image * Video or image previews that are played when mousing over the scene card -* Perceptual hashes - helps match against StashDB, and feeds the duplicate finder +* Video Perceptual hashes - helps match against StashDB, and feeds the duplicate finder * Sprites (scene stills for parts of each scene) that are shown in the scene scrubber * Marker video previews that are shown in the markers page * Transcoded versions of scenes. See below * Image thumbnails of galleries +* Image Perceptual hashes - can be used for identification and deduplication The generate task accepts the following options: @@ -52,9 +55,11 @@ The generate task accepts the following options: | Marker Animated Image Previews | *Accessible in Advanced Mode* - Also generate animated (webp) previews, only required when Scene/Marker Wall Preview Type is set to Animated Image. When browsing they use less CPU than the video previews, but are generated in addition to them and are larger files. | | Marker Screenshots | Generates static JPG images for markers. Only required if Preview Type is set to Static Image. Requires Marker Previews to be enabled. | | Transcodes | *Accessible in Advanced Mode* - MP4 conversions of unsupported video formats. Allows direct streaming instead of live transcoding. | -| Perceptual hashes (for deduplication) | Generates perceptual hashes for scene deduplication and identification. | +| Video Perceptual hashes (for deduplication) | Generates perceptual hashes for scene deduplication and identification. | | Generate heatmaps and speeds for interactive scenes | Generates heatmaps and speeds for interactive scenes. | | Image Clip Previews | Generates a gif/looping video as thumbnail for image clips/gifs. | +| Image Thumbnails | Generates thumbnails for image files. | +| Image Perceptual hashes (for deduplication) | Generates perceptual hashes for image deduplication and identification. | | Overwrite existing generated files | By default, where a generated file exists, it is not regenerated. When this flag is enabled, then the generated files are regenerated. | ### Transcodes diff --git a/ui/v2.5/src/docs/en/Manual/TroubleshootingMode.md b/ui/v2.5/src/docs/en/Manual/TroubleshootingMode.md new file mode 100644 index 000000000..d7a2c1cee --- /dev/null +++ b/ui/v2.5/src/docs/en/Manual/TroubleshootingMode.md @@ -0,0 +1,7 @@ +# Troubleshooting Mode + +Troubleshooting Mode disables all plugins and all custom CSS, JavaScript, and locales. It also temporarily sets the log level to `DEBUG`. This is useful when you are experiencing issues with your Stash instance to eliminate the possibility that a plugin or custom code is causing the issue. + +Troubleshooting Mode is enabled from the Settings page, by clicking the `Troubleshooting Mode` button at the bottom left of the page. + +When Troubleshooting Mode is enabled, a red border and a banner will be displayed to remind you that you are in Troubleshooting Mode. To exit Troubleshooting Mode, click the `Exit` button in the banner. \ No newline at end of file diff --git a/ui/v2.5/src/docs/en/Manual/UIPluginApi.md b/ui/v2.5/src/docs/en/Manual/UIPluginApi.md index f010deb38..54ef3a20f 100644 --- a/ui/v2.5/src/docs/en/Manual/UIPluginApi.md +++ b/ui/v2.5/src/docs/en/Manual/UIPluginApi.md @@ -23,6 +23,7 @@ This namespace contains the generated graphql client interface. This is a low-le ### `libraries` `libraries` provides access to the following UI libraries: + - `ReactRouterDOM` - `Bootstrap` - `Apollo` @@ -32,6 +33,7 @@ This namespace contains the generated graphql client interface. This is a low-le - `FontAwesomeBrands` - `Mousetrap` - `MousetrapPause` +- `ReactFontAwesome` - `ReactSelect` ### `register` @@ -66,7 +68,7 @@ This namespace contains all of the components available to plugins. These includ ### `utils` -This namespace provides access to the `NavUtils` and `StashService` namespaces. It also provides access to the `loadComponents` method. +This namespace provides access to the `NavUtils` , `StashService` and `InteractiveUtils` namespaces. It also provides access to the `loadComponents` method. #### `PluginApi.utils.loadComponents` @@ -80,9 +82,76 @@ In general, `PluginApi.hooks.useLoadComponents` hook should be used instead. Returns a `Promise` that resolves when all of the components have been loaded. +#### `PluginApi.utils.InteractiveUtils` +This namespace provides access to `interactiveClientProvider` and `getPlayer` + - `getPlayer` returns the current `videojs` player object + - `interactiveClientProvider` takes `IInteractiveClientProvider` which allows a developer to hook into the lifecycle of funscripts. +```ts + export interface IDeviceSettings { + connectionKey: string; + scriptOffset: number; + estimatedServerTimeOffset?: number; + useStashHostedFunscript?: boolean; + [key: string]: unknown; +} + +export interface IInteractiveClientProviderOptions { + handyKey: string; + scriptOffset: number; + defaultClientProvider?: IInteractiveClientProvider; + stashConfig?: GQL.ConfigDataFragment; +} +export interface IInteractiveClientProvider { + (options: IInteractiveClientProviderOptions): IInteractiveClient; +} + +/** + * Interface that is used for InteractiveProvider + */ +export interface IInteractiveClient { + connect(): Promise; + handyKey: string; + uploadScript: (funscriptPath: string, apiKey?: string) => Promise; + sync(): Promise; + configure(config: Partial): Promise; + play(position: number): Promise; + pause(): Promise; + ensurePlaying(position: number): Promise; + setLooping(looping: boolean): Promise; + readonly connected: boolean; + readonly playing: boolean; +} + +``` +##### Example +For instance say I wanted to add extra logging when `IInteractiveClient.connect()` is called. +In my plugin you would install your own client provider as seen below + +```ts +InteractiveUtils.interactiveClientProvider = ( + opts +) => { + if (!opts.defaultClientProvider) { + throw new Error('invalid setup'); + } + + const client = opts.defaultClientProvider(opts); + const connect = client.connect; + client.connect = async () => { + console.log('patching connect method'); + return connect.call(client); + }; + + return client; +}; + +``` + + ### `hooks` This namespace provides access to the following core utility hooks: + - `useGalleryLightbox` - `useLightbox` - `useSpriteInfo` @@ -155,10 +224,13 @@ Returns `void`. - `CountrySelect` - `CustomFieldInput` - `CustomFields` +- `CustomFieldsInput` - `DateInput` - `DetailImage` - `ExternalLinkButtons` - `ExternalLinksButton` +- `FilteredGalleryList` +- `FilteredSceneList` - `FolderSelect` - `FrontPage` - `GalleryCard` @@ -166,17 +238,31 @@ Returns `void`. - `GalleryCard.Image` - `GalleryCard.Overlays` - `GalleryCard.Popovers` +- `GalleryCardGrid` - `GalleryIDSelect` +- `GalleryList` +- `GalleryRecommendationRow` - `GallerySelect` - `GallerySelect.sort` +- `GridCard` +- `GroupCard` +- `GroupCardGrid` - `GroupIDSelect` +- `GroupRecommendationRow` - `GroupSelect` - `GroupSelect.sort` - `HeaderImage` - `HoverPopover` - `Icon` +- `ImageCard` +- `ImageCard.Details` +- `ImageCard.Image` +- `ImageCard.Overlays` +- `ImageCard.Popovers` - `ImageDetailPanel` +- `ImageGridCard` - `ImageInput` +- `ImageRecommendationRow` - `LightboxLink` - `LoadingIndicator` - `MainNavBar.MenuItems` @@ -192,6 +278,7 @@ Returns `void`. - `PerformerCard.Overlays` - `PerformerCard.Popovers` - `PerformerCard.Title` +- `PerformerCardGrid` - `PerformerDetailsPanel` - `PerformerDetailsPanel.DetailGroup` - `PerformerGalleriesPanel` @@ -200,6 +287,7 @@ Returns `void`. - `PerformerIDSelect` - `PerformerImagesPanel` - `PerformerPage` +- `PerformerRecommendationRow` - `PerformerScenesPanel` - `PerformerSelect` - `PerformerSelect.sort` @@ -208,17 +296,27 @@ Returns `void`. - `RatingNumber` - `RatingStars` - `RatingSystem` +- `RecommendationRow` - `SceneCard` - `SceneCard.Details` - `SceneCard.Image` - `SceneCard.Overlays` - `SceneCard.Popovers` +- `SceneCardsGrid` - `SceneFileInfoPanel` - `SceneIDSelect` +- `SceneMarkerCard` +- `SceneMarkerCard.Details` +- `SceneMarkerCard.Image` +- `SceneMarkerCard.Popovers` +- `SceneMarkerCardsGrid` +- `SceneMarkerRecommendationRow` +- `SceneList` - `ScenePage` - `ScenePage.TabContent` - `ScenePage.Tabs` - `ScenePlayer` +- `SceneRecommendationRow` - `SceneSelect` - `SceneSelect.sort` - `SelectSetting` @@ -227,7 +325,11 @@ Returns `void`. - `SettingModal` - `StringListSetting` - `StringSetting` +- `StudioCard` +- `StudioCardGrid` +- `StudioDetailsPanel` - `StudioIDSelect` +- `StudioRecommendationRow` - `StudioSelect` - `StudioSelect.sort` - `SweatDrops` @@ -238,8 +340,10 @@ Returns `void`. - `TagCard.Overlays` - `TagCard.Popovers` - `TagCard.Title` +- `TagCardGrid` - `TagIDSelect` - `TagLink` +- `TagRecommendationRow` - `TagSelect` - `TagSelect.sort` - `TruncatedText` @@ -250,4 +354,4 @@ Allows plugins to listen for Stash's events. ```js PluginApi.Event.addEventListener("stash:location", (e) => console.log("Page Changed", e.detail.data.location.pathname)) -``` +``` \ No newline at end of file diff --git a/ui/v2.5/src/docs/en/ReleaseNotes/index.ts b/ui/v2.5/src/docs/en/ReleaseNotes/index.ts index 8e2f503d4..78e5e4b37 100644 --- a/ui/v2.5/src/docs/en/ReleaseNotes/index.ts +++ b/ui/v2.5/src/docs/en/ReleaseNotes/index.ts @@ -4,6 +4,7 @@ import v0240 from "./v0240.md"; import v0250 from "./v0250.md"; import v0260 from "./v0260.md"; import v0270 from "./v0270.md"; +import v0290 from "./v0290.md"; export interface IReleaseNotes { // handle should be in the form of YYYYMMDD @@ -13,6 +14,11 @@ export interface IReleaseNotes { } export const releaseNotes: IReleaseNotes[] = [ + { + date: 20251026, + version: "v0.29.0", + content: v0290, + }, { date: 20240826, version: "v0.27.0", diff --git a/ui/v2.5/src/docs/en/ReleaseNotes/v0290.md b/ui/v2.5/src/docs/en/ReleaseNotes/v0290.md new file mode 100644 index 000000000..6dfe0b209 --- /dev/null +++ b/ui/v2.5/src/docs/en/ReleaseNotes/v0290.md @@ -0,0 +1,5 @@ +The Scenes page and related scene list views have been updated with a filter sidebar and a toolbar for filtering and other actions. This design is intended to be applied to other query pages in the following release. The design will be refined based on user feedback. + +You can help steer the direction of this design by providing feedback in the [forum thread](https://discourse.stashapp.cc/t/query-page-redesign-feedback-thread-0-29/3935). + +Old userscripts and plugins that intercept GraphQL with content-type `application/json` will stop working, as gqlenc uses the updated content-type `application/graphql-response+json` \ No newline at end of file diff --git a/ui/v2.5/src/hooks/Config.tsx b/ui/v2.5/src/hooks/Config.tsx index 0b00d0dc5..65ad7122a 100644 --- a/ui/v2.5/src/hooks/Config.tsx +++ b/ui/v2.5/src/hooks/Config.tsx @@ -2,14 +2,28 @@ import React from "react"; import * as GQL from "src/core/generated-graphql"; export interface IContext { - configuration?: GQL.ConfigDataFragment; - loading?: boolean; + configuration: GQL.ConfigDataFragment; } -export const ConfigurationContext = React.createContext({}); +export const ConfigurationContext = React.createContext(null); + +export const useConfigurationContext = () => { + const context = React.useContext(ConfigurationContext); + + if (context === null) { + throw new Error( + "useConfigurationContext must be used within a ConfigurationProvider" + ); + } + + return context; +}; + +export const useConfigurationContextOptional = () => { + return React.useContext(ConfigurationContext); +}; export const ConfigurationProvider: React.FC = ({ - loading, configuration, children, }) => { @@ -17,7 +31,6 @@ export const ConfigurationProvider: React.FC = ({ {children} diff --git a/ui/v2.5/src/hooks/Interactive/context.tsx b/ui/v2.5/src/hooks/Interactive/context.tsx index a42f0aa7b..ccdc948b4 100644 --- a/ui/v2.5/src/hooks/Interactive/context.tsx +++ b/ui/v2.5/src/hooks/Interactive/context.tsx @@ -1,7 +1,11 @@ import React, { useCallback, useContext, useEffect, useState } from "react"; -import { ConfigurationContext } from "../Config"; +import { useConfigurationContext } from "../Config"; import { useLocalForage } from "../LocalForage"; import { Interactive as InteractiveAPI } from "./interactive"; +import InteractiveUtils, { + IInteractiveClient, + IInteractiveClientProvider, +} from "./utils"; export enum ConnectionState { Missing, @@ -34,7 +38,7 @@ export function connectionStateLabel(s: ConnectionState) { } export interface IState { - interactive: InteractiveAPI; + interactive: IInteractiveClient; state: ConnectionState; serverOffset: number; initialised: boolean; @@ -69,13 +73,20 @@ interface IInteractiveState { lastSyncTime: number; } +export const defaultInteractiveClientProvider: IInteractiveClientProvider = ({ + handyKey, + scriptOffset, +}): IInteractiveClient => { + return new InteractiveAPI(handyKey, scriptOffset); +}; + export const InteractiveProvider: React.FC = ({ children }) => { const [{ data: config }, setConfig] = useLocalForage( LOCAL_FORAGE_KEY, { serverOffset: 0, lastSyncTime: 0 } ); - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const [state, setState] = useState(ConnectionState.Missing); const [handyKey, setHandyKey] = useState(undefined); @@ -85,7 +96,22 @@ export const InteractiveProvider: React.FC = ({ children }) => { const [scriptOffset, setScriptOffset] = useState(0); const [useStashHostedFunscript, setUseStashHostedFunscript] = useState(false); - const [interactive] = useState(new InteractiveAPI("", 0)); + + const resolveInteractiveClient = useCallback(() => { + const interactiveClientProvider = + InteractiveUtils.interactiveClientProvider ?? + defaultInteractiveClientProvider; + + return interactiveClientProvider({ + handyKey: "", + scriptOffset: 0, + defaultClientProvider: defaultInteractiveClientProvider, + stashConfig, + }); + }, [stashConfig]); + + // fetch client provider from PluginApi if not found use default provider + const [interactive] = useState(resolveInteractiveClient); const [initialised, setInitialised] = useState(false); const [error, setError] = useState(); @@ -104,7 +130,9 @@ export const InteractiveProvider: React.FC = ({ children }) => { } if (config?.serverOffset) { - interactive.setServerTimeOffset(config.serverOffset); + await interactive.configure({ + estimatedServerTimeOffset: config.serverOffset, + }); setState(ConnectionState.Connecting); try { await interactive.connect(); @@ -138,13 +166,17 @@ export const InteractiveProvider: React.FC = ({ children }) => { const oldKey = interactive.handyKey; - interactive.handyKey = handyKey ?? ""; - interactive.scriptOffset = scriptOffset; - interactive.useStashHostedFunscript = useStashHostedFunscript; - - if (oldKey !== interactive.handyKey && interactive.handyKey) { - initialise(); - } + interactive + .configure({ + connectionKey: handyKey ?? "", + offset: scriptOffset, + useStashHostedFunscript, + }) + .then(() => { + if (oldKey !== interactive.handyKey && interactive.handyKey) { + initialise(); + } + }); }, [ handyKey, scriptOffset, @@ -171,7 +203,7 @@ export const InteractiveProvider: React.FC = ({ children }) => { const uploadScript = useCallback( async (funscriptPath: string) => { - interactive.pause(); + await interactive.pause(); if ( !interactive.handyKey || !funscriptPath || diff --git a/ui/v2.5/src/hooks/Interactive/interactive.ts b/ui/v2.5/src/hooks/Interactive/interactive.ts index ef34bd2ef..2b1227243 100644 --- a/ui/v2.5/src/hooks/Interactive/interactive.ts +++ b/ui/v2.5/src/hooks/Interactive/interactive.ts @@ -5,6 +5,7 @@ import { CsvUploadResponse, HandyFirmwareStatus, } from "thehandy/lib/types"; +import { IDeviceSettings } from "./utils"; interface IFunscript { actions: Array; @@ -108,6 +109,13 @@ export class Interactive { this._playing = false; } + get connected() { + return this._connected; + } + get playing() { + return this._playing; + } + async connect() { const connected = await this._handy.getConnected(); if (!connected) { @@ -170,6 +178,10 @@ export class Interactive { this._connected = await this._handy .setHsspSetup(funscriptUrl) .then((result) => result === HsspSetupResult.downloaded); + + // for some reason we need to call getStatus after setup to ensure proper state + // see https://github.com/defucilis/thehandy/issues/3 + await this._handy.getStatus(); } async sync() { @@ -180,6 +192,15 @@ export class Interactive { this._handy.estimatedServerTimeOffset = offset; } + async configure(config: Partial) { + this._scriptOffset = config.scriptOffset ?? this._scriptOffset; + this.handyKey = config.connectionKey ?? this.handyKey; + this._handy.estimatedServerTimeOffset = + config.estimatedServerTimeOffset ?? this._handy.estimatedServerTimeOffset; + this.useStashHostedFunscript = + config.useStashHostedFunscript ?? this.useStashHostedFunscript; + } + async play(position: number) { if (!this._connected) { return; diff --git a/ui/v2.5/src/hooks/Interactive/utils.ts b/ui/v2.5/src/hooks/Interactive/utils.ts new file mode 100644 index 000000000..c1d066e86 --- /dev/null +++ b/ui/v2.5/src/hooks/Interactive/utils.ts @@ -0,0 +1,51 @@ +import { getPlayer } from "src/components/ScenePlayer/util"; +import type { VideoJsPlayer } from "video.js"; +import * as GQL from "src/core/generated-graphql"; + +export interface IDeviceSettings { + connectionKey: string; + scriptOffset: number; + estimatedServerTimeOffset?: number; + useStashHostedFunscript?: boolean; + [key: string]: unknown; +} + +export interface IInteractiveClientProviderOptions { + handyKey: string; + scriptOffset: number; + defaultClientProvider?: IInteractiveClientProvider; + stashConfig?: GQL.ConfigDataFragment; +} +export interface IInteractiveClientProvider { + (options: IInteractiveClientProviderOptions): IInteractiveClient; +} + +/** + * Interface that is used for InteractiveProvider + */ +export interface IInteractiveClient { + connect(): Promise; + handyKey: string; + uploadScript: (funscriptPath: string, apiKey?: string) => Promise; + sync(): Promise; + configure(config: Partial): Promise; + play(position: number): Promise; + pause(): Promise; + ensurePlaying(position: number): Promise; + setLooping(looping: boolean): Promise; + readonly connected: boolean; + readonly playing: boolean; +} + +export interface IInteractiveUtils { + getPlayer: () => VideoJsPlayer | undefined; + interactiveClientProvider: IInteractiveClientProvider | undefined; +} +const InteractiveUtils = { + // hook to allow to customize the interactive client + interactiveClientProvider: undefined, + // returns the active player + getPlayer, +}; + +export default InteractiveUtils; diff --git a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx index 6e4eb856a..65c15024c 100644 --- a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx +++ b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx @@ -19,7 +19,7 @@ import usePageVisibility from "../PageVisibility"; import { useToast } from "../Toast"; import { FormattedMessage, useIntl } from "react-intl"; import { LightboxImage } from "./LightboxImage"; -import { ConfigurationContext } from "../Config"; +import { useConfigurationContext } from "../Config"; import { Link } from "react-router-dom"; import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton"; import { @@ -154,7 +154,7 @@ export const LightboxComponent: React.FC = ({ const Toast = useToast(); const intl = useIntl(); - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const [interfaceLocalForage, setInterfaceLocalForage] = useInterfaceLocalForage(); @@ -200,6 +200,8 @@ export const LightboxComponent: React.FC = ({ config?.interface.imageLightbox.scrollAttemptsBeforeChange ?? 0 ); + const disableAnimation = config?.interface.imageLightbox.disableAnimation; + function setSlideshowDelay(v: number) { setLightboxSettings({ slideshowDelay: v }); } @@ -340,6 +342,10 @@ export const LightboxComponent: React.FC = ({ (isUserAction = true) => { if (isSwitchingPage || index === -1) return; + if (disableAnimation) { + setInstant(); + } + setShowChapters(false); setMovingLeft(true); @@ -357,13 +363,25 @@ export const LightboxComponent: React.FC = ({ resetIntervalCallback.current(); } }, - [images, pageCallback, isSwitchingPage, resetIntervalCallback, index] + [ + images, + pageCallback, + isSwitchingPage, + resetIntervalCallback, + index, + disableAnimation, + setInstant, + ] ); const handleRight = useCallback( (isUserAction = true) => { if (isSwitchingPage) return; + if (disableAnimation) { + setInstant(); + } + setMovingLeft(false); setShowChapters(false); @@ -388,6 +406,8 @@ export const LightboxComponent: React.FC = ({ isSwitchingPage, resetIntervalCallback, index, + disableAnimation, + setInstant, ] ); @@ -455,6 +475,7 @@ export const LightboxComponent: React.FC = ({ React.createElement(image.paths.preview != "" ? "video" : "img", { loop: image.paths.preview != "", autoPlay: image.paths.preview != "", + playsInline: image.paths.preview != "", src: image.paths.preview != "" ? image.paths.preview ?? "" diff --git a/ui/v2.5/src/hooks/Toast.tsx b/ui/v2.5/src/hooks/Toast.tsx index 9be27e928..3590e0efb 100644 --- a/ui/v2.5/src/hooks/Toast.tsx +++ b/ui/v2.5/src/hooks/Toast.tsx @@ -150,3 +150,21 @@ export const useToast = () => { [addToast] ); }; + +export function toastOperation( + toast: ReturnType, + o: () => Promise, + successMessage: string +) { + async function operation() { + try { + await o(); + + toast.success(successMessage); + } catch (e) { + toast.error(e); + } + } + + return operation; +} diff --git a/ui/v2.5/src/hooks/tagsEdit.tsx b/ui/v2.5/src/hooks/tagsEdit.tsx index 7654081cf..ebd831feb 100644 --- a/ui/v2.5/src/hooks/tagsEdit.tsx +++ b/ui/v2.5/src/hooks/tagsEdit.tsx @@ -50,6 +50,7 @@ export function useTagsEdit( id: result.data.tagCreate.id, name: toCreate.name ?? "", aliases: [], + stash_ids: result.data.tagCreate.stash_ids, }, ]) ); @@ -93,6 +94,7 @@ export function useTagsEdit( id: p.stored_id!, name: p.name ?? "", aliases: [], + stash_ids: [], }; }) ); diff --git a/ui/v2.5/src/hooks/title.ts b/ui/v2.5/src/hooks/title.ts index 8dd311e47..193a3f920 100644 --- a/ui/v2.5/src/hooks/title.ts +++ b/ui/v2.5/src/hooks/title.ts @@ -1,10 +1,13 @@ import { MessageDescriptor, useIntl } from "react-intl"; +import { useConfigurationContext } from "./Config"; export const TITLE = "Stash"; export const TITLE_SEPARATOR = " | "; export function useTitleProps(...messages: (string | MessageDescriptor)[]) { const intl = useIntl(); + const config = useConfigurationContext(); + const title = config.configuration.ui.title || TITLE; const parts = messages.map((msg) => { if (typeof msg === "object") { @@ -14,13 +17,13 @@ export function useTitleProps(...messages: (string | MessageDescriptor)[]) { } }); - return makeTitleProps(...parts); + return makeTitleProps(title, ...parts); } -export function makeTitleProps(...parts: string[]) { - const title = [...parts, TITLE].join(TITLE_SEPARATOR); +export function makeTitleProps(title: string, ...parts: string[]) { + const fullTitle = [...parts, title].join(TITLE_SEPARATOR); return { - titleTemplate: `%s | ${title}`, - defaultTitle: title, + titleTemplate: `%s | ${fullTitle}`, + defaultTitle: fullTitle, }; } diff --git a/ui/v2.5/src/hooks/useTableColumns.ts b/ui/v2.5/src/hooks/useTableColumns.ts index 09d5357d2..ed6380bdb 100644 --- a/ui/v2.5/src/hooks/useTableColumns.ts +++ b/ui/v2.5/src/hooks/useTableColumns.ts @@ -1,6 +1,5 @@ -import { useContext } from "react"; import { useConfigureUI } from "src/core/StashService"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { useToast } from "./Toast"; export const useTableColumns = ( @@ -9,7 +8,7 @@ export const useTableColumns = ( ) => { const Toast = useToast(); - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const [saveUI] = useConfigureUI(); const ui = configuration?.ui; diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index d73cd3b2a..cadd1ad2f 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -5,9 +5,12 @@ $navbar-height: 48.75px; $sticky-detail-header-height: 50px; +$sidebar-width: 250px; + @import "styles/theme"; @import "styles/range"; @import "styles/scrollbars"; +@import "sfw-mode.scss"; @import "src/components/Changelog/styles.scss"; @import "src/components/Galleries/styles.scss"; @import "src/components/Help/styles.scss"; @@ -523,8 +526,6 @@ textarea.text-input { } .zoom-1 { - width: 320px; - .gallery-card-image, .tag-card-image { height: 240px; @@ -684,6 +685,11 @@ div.dropdown-menu { .edit-button { margin-right: 10px; + + // Show caret on split button dropdown toggle + &.btn-group .dropdown-toggle-split::after { + content: ""; + } } .wrap-tags { @@ -698,29 +704,43 @@ div.dropdown-menu { } .tag-item { + align-items: center; background-color: $muted-gray; color: $dark-text; + display: inline-flex; font-size: 12px; font-weight: 400; line-height: 16px; margin: 5px; padding: 2px 6px; + // if link, move padding to link to make full tag clickable + &.tag-link { + padding: 0; + + a { + padding: 2px 6px; + } + } + &:hover { cursor: pointer; } + .search-term svg { + margin-left: 0; + } + .btn { background: none; border: none; bottom: 2px; color: $dark-text; font-size: 12px; - line-height: 1rem; + line-height: 16px; margin-right: -0.5rem; opacity: 0.5; padding: 0 0.5rem; - position: relative; &:active, &:hover { @@ -1416,3 +1436,40 @@ select { h3 .TruncatedText { line-height: 1.5; } + +// Troubleshooting Mode overlay banner +.troubleshooting-mode-overlay { + border: 5px solid $danger; + bottom: 0; + left: 0; + opacity: 0.75; + pointer-events: none; + position: fixed; + right: 0; + top: 0; + z-index: 1040; + + .troubleshooting-mode-alert { + align-items: baseline; + border-radius: 0; + bottom: 0.5rem; + display: inline-flex; + margin: 0; + position: fixed; + right: 0.5rem; + + @include media-breakpoint-down(xs) { + @media (orientation: portrait) { + bottom: $navbar-height; + + & > span { + font-size: 0.75rem; + } + } + } + } + + .btn { + pointer-events: auto; + } +} diff --git a/ui/v2.5/src/locales/bg-BG.json b/ui/v2.5/src/locales/bg-BG.json new file mode 100644 index 000000000..5c933c9ce --- /dev/null +++ b/ui/v2.5/src/locales/bg-BG.json @@ -0,0 +1,400 @@ +{ + "actions": { + "add": "Добави", + "add_directory": "Добави Директория", + "add_entity": "Добави {entityType}", + "add_manual_date": "Добави дата ръчно", + "add_sub_groups": "Добави Подгрупа", + "add_o": "Добави О", + "add_play": "Добави Гледане", + "add_to_entity": "Добави към {entityType}", + "allow": "Позволи", + "allow_temporarily": "Позволи временно", + "anonymise": "Анонимизирай", + "apply": "Приложи", + "auto_tag": "Автоматично Тагване", + "backup": "Резервно копие", + "browse_for_image": "Преглеждане за картина…", + "cancel": "Отмяна", + "choose_date": "Избери дата", + "clean": "Изчисти", + "clean_generated": "Изчисти генерирани файлове", + "clear": "Изчисти", + "clear_back_image": "Изчисти задна картина", + "clear_date_data": "Изчисти дата на данни", + "clear_front_image": "Изчисти предна картина", + "clear_image": "Изичисти картина", + "close": "Затвори", + "confirm": "Потвърди", + "continue": "Продължи", + "copy_to_clipboard": "Копиране в буфера", + "create": "Създай", + "create_chapters": "Създай Глава", + "create_entity": "Създай {entityType}", + "create_marker": "Създай Маркер", + "create_parent_studio": "Създай родителско студио", + "created_entity": "Създаден {entity_type}: {entity_name}", + "customise": "Персонализирай", + "delete": "Изтрий", + "delete_entity": "Изтрий {entityType}", + "delete_file": "Изтрий файла", + "delete_file_and_funscript": "Изтрий файл (и funscript)", + "delete_generated_supporting_files": "Изтрий генерирани поддържащи файлове", + "disable": "Изключи", + "disallow": "Забрани", + "download": "Свали", + "download_anonymised": "Свали анонимизирана", + "download_backup": "Свали резевно копие", + "edit": "Редакция", + "edit_entity": "Редакция {entityType}", + "enable": "Активирай", + "encoding_image": "Кодиране на картина…", + "export": "Експортиране", + "export_all": "Експортирай всичко…", + "reshuffle": "Пренареди", + "assign_stashid_to_parent_studio": "Присвояване на Stash ID към съществуващо родителско студио и актуализиране на метаданните", + "find": "Намери", + "finish": "Приключи", + "from_file": "От файл…", + "from_url": "От URL…", + "full_export": "Пълен експорт", + "full_import": "Пълен Импорт", + "generate": "Генерирай", + "generate_thumb_default": "Генерирай тъмбнайл по подразбиране", + "generate_thumb_from_current": "Генерирай тъмбнайл от сегашният кадър", + "hash_migration": "миграция на хеш", + "hide": "Скрий", + "hide_configuration": "Скрий конфигурация", + "identify": "Индентифицирай", + "ignore": "Игнорирай", + "import": "Импорт…", + "import_from_file": "Импорт от файл", + "load": "Зареди", + "load_filter": "Зареди филтър", + "logout": "Изход", + "make_primary": "Направи Основен", + "merge": "Слей", + "merge_from": "Слей от", + "merge_into": "Слей към", + "migrate_blobs": "Мигрирай Блобове", + "migrate_scene_screenshots": "Мигрирай Скрийншоти от Сцени", + "next_action": "Следващ", + "not_running": "не върви", + "open_in_external_player": "Отвори в външет плейер", + "open_random": "Отвори Случайно", + "optimise_database": "Оптимизирай База Данни", + "overwrite": "Презапиши", + "play": "Пусни", + "play_random": "Пусни Случайно", + "play_selected": "Пусни избраните", + "preview": "Преглеждане", + "previous_action": "Назад", + "reassign": "Презадай", + "refresh": "Опресни", + "reload": "Презареди", + "reload_plugins": "Презареди плъгините", + "reload_scrapers": "Презареди търкачите", + "remove": "Премахни", + "remove_date": "Премахни дата", + "remove_from_containing_group": "Премахни от Група", + "remove_from_gallery": "Премахни от Галерия", + "rename_gen_files": "Преименувай генериран файл", + "rescan": "Пресканирай", + "reset_play_duration": "Нулирай период на пускане", + "reset_resume_time": "Нулирай преме за възтановяване", + "reset_cover": "Възтанови Първоначална Корица", + "running": "върви", + "save": "Запази", + "save_delete_settings": "Изполвай тези настройки по подразвиране при изтриване", + "save_filter": "Запази филтър", + "scan": "Сканирай", + "scrape": "Изтъркай", + "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_cover": "Сложи Като Корица", + "set_front_image": "Предна Картина…", + "set_image": "Сложи картина…", + "show": "Покажи", + "show_configuration": "Покажи Конфигурация", + "show_results": "Покажи резултат", + "show_count_results": "Покажи {count} резултати", + "sidebar": { + "close": "Затвори странично меню", + "open": "Отвори странично меню", + "toggle": "Превключи странично меню" + }, + "skip": "Пропусни", + "split": "Раздели", + "stop": "Спри", + "submit": "Подай", + "submit_stash_box": "Подай към Stash-Box", + "submit_update": "Подай обноваване", + "swap": "Подмени", + "tasks": { + "clean_confirm_message": "Сигурен ли си че искаш да Изчистиш? Това ще изтрие иформацията от база данни и генерирано съдържание за всички сцени и галерий който не мога да бъдат намерени на файловата система.", + "dry_mode_selected": "Избрано е Сухо Пускане. Нищо няма да бъде изтрито на истина, са ще бъде записано в логовете.", + "import_warning": "Сигурен ли си че искаш да импортираш? Това ще изтрие базата данни и че вкара на ново твоите експортирани метаданни." + }, + "temp_disable": "Спри временно…", + "temp_enable": "Включи временно…", + "unset": "Премахни", + "use_default": "Използвай на стойностите по подразбиране", + "view_history": "Виж история", + "view_random": "Виж Случайно" + }, + "actions_name": "Действия", + "age": "Години", + "age_on_date": "{age} по време на продукция", + "aliases": "Псевдоними", + "all": "всички", + "also_known_as": "Също така познат/а като", + "appears_with": "Има Участия Със", + "ascending": "Възходящ", + "audio_codec": "Аудио Кодек", + "average_resolution": "Средностатистическа Резолюция", + "between_and": "и", + "birth_year": "Година на раждане", + "birthdate": "Дата на раждане", + "bitrate": "Бит Рейт", + "blobs_storage_type": { + "database": "База Данни", + "filesystem": "Файлова Система" + }, + "captions": "Субтитри", + "career_length": "Подължителност на Кариера", + "chapters": "Глави", + "circumcised": "Образан", + "circumcised_types": { + "CUT": "Обрязан", + "UNCUT": "Необрязан" + }, + "component_tagger": { + "config": { + "active_instance": "Активни инстанции на stash-box:", + "blacklist_desc": "Предмети от черният списък са изключени от заяки. Забележи че те са regular expressions и не гледа главни и малки букви. Някой символи трябва да пъдат escape-нати със наклонка: {chars_require_escape}", + "blacklist_label": "Черен списък", + "errors": { + "blacklist_duplicate": "Дубликирани предмети от черния списък" + }, + "mark_organized_desc": "Веднага отбележи сцена като Организирана след като се натисне бутон Запази.", + "mark_organized_label": "Отбележи като Организирана на запазване", + "query_mode_auto": "Автоматично", + "query_mode_auto_desc": "Използа метаданни ако съществиват, или име на файл", + "query_mode_dir": "Папка", + "query_mode_dir_desc": "Използва само папката която съдържа видео файла", + "query_mode_filename": "Име на файл", + "query_mode_filename_desc": "Използва само име на файл", + "query_mode_label": "Мод на Заявки", + "query_mode_metadata": "Мета данни", + "query_mode_metadata_desc": "Използва само мета данни", + "query_mode_path": "Път", + "query_mode_path_desc": "Използва целият път на файла", + "set_cover_desc": "Замени корицата на сцената ако се намери такава.", + "set_cover_label": "Заложи картина за корица на сцена", + "set_tag_desc": "Закачи тагове към сцената, или чрез презаписване или чрез сливане със съществуващите тагове на сцената.", + "set_tag_label": "Задай тагове", + "source": "Източник" + }, + "noun_query": "Заявка", + "results": { + "duration_off": "Продължителност не съвпада с поне {number} сек.", + "duration_unknown": "Неизвестна продължителност", + "fp_matches": "Продължителността съвпада", + "fp_matches_multi": "Продължителността съвпада {matchCount}/{durationsLength} отпечатъци", + "hash_matches": "{hash_type} съвпада", + "match_failed_already_tagged": "Сцената вече има тагове", + "match_failed_no_result": "Няма намерени резултати", + "match_success": "Счената е успешно тагната", + "phash_matches": "{count} PHashes съвпадат", + "unnamed": "Неименуван" + }, + "verb_match_fp": "Сравни Отпечатъци", + "verb_matched": "Сравнени", + "verb_scrape_all": "Изтъркай Всичко", + "verb_submit_fp": "Подай {fpCount, plural, one{# Отпечатък} other{# Отпечатъци}}", + "verb_toggle_unmatched": "{toggle} несъвпадаци сцени" + }, + "config": { + "about": { + "build_hash": "Хеш на билда:", + "build_time": "Време на билда:", + "check_for_new_version": "Проверка за нова версия", + "latest_version": "Последна Версия", + "latest_version_build_hash": "Хеш на билда на Последна Версия:", + "new_version_notice": "[НОВО]", + "release_date": "Дана за издаване:", + "stash_discord": "Присъедини се към нашият {url} канал", + "stash_home": "Stash home на {url}", + "stash_open_collective": "Подкрепи ни чрез {url}", + "stash_wiki": "Stash {url} страница", + "version": "Версия" + }, + "advanced_mode": "Мод за Напреднали", + "application_paths": { + "heading": "Пътища на Апликация" + }, + "categories": { + "about": "Относно", + "changelog": "Списък на промените", + "interface": "Интерфейс", + "logs": "Дневници", + "metadata_providers": "Доставчици на Мета данни", + "plugins": "Приставки", + "scraping": "Търкане", + "security": "Сигурност", + "services": "Услуги", + "system": "Система", + "tasks": "Задачи", + "tools": "Иструменти" + }, + "dlna": { + "allow_temp_ip": "Позволи {tempIP}", + "allowed_ip_addresses": "Позволени IP адреси", + "allowed_ip_temporarily": "Позволени IP временно", + "default_ip_whitelist": "Основен IP Бъл списък", + "default_ip_whitelist_desc": "Основени IP адреси позволени да достигат DLNA. Изполвай {wildcard} за да позволиш всички IP адреси.", + "disabled_dlna_temporarily": "Временно изключено DLNA", + "disallowed_ip": "Непозволени IP", + "enabled_by_default": "Включено по подразвиране", + "enabled_dlna_temporarily": "Временно включено DLNA", + "network_interfaces": "Интерфейси", + "network_interfaces_desc": "Интерфейси да се достъпн DLNA сървъра. Празен лист ще ползва всички интерфейси. Изисква DLNA рестрат след промяна.", + "recent_ip_addresses": "Скорошни IP адреси", + "server_display_name": "Име за Покаване на Сървъра", + "server_display_name_desc": "Име за покаване на DLNA сървъра. По подразбиране {server_name} ако е празно.", + "server_port": "Порт на Сървъра", + "server_port_desc": "Порт на който да върви DLNA сървъра. Изисква рестарт на DLNA след промяна.", + "successfully_cancelled_temporary_behaviour": "Успешно отказано временно поведение", + "until_restart": "до рестартиране", + "video_sort_order": "Ред на Видеа по подразбиране", + "video_sort_order_desc": "Ред по който да реди видеа по подразбиране." + }, + "general": { + "auth": { + "api_key": "API ключ", + "api_key_desc": "API ключ за външни системи. Нужен само когато име/парола са настроени. Името трябва да бъде запазене преди генерация на API ключ.", + "authentication": "Идентификация", + "clear_api_key": "Изчисти API ключ", + "credentials": { + "description": "Удостоверителни данни за контрол на достъпа до stash.", + "heading": "Удостоверителни данни" + }, + "generate_api_key": "Генерирай API ключ", + "log_file": "Лог файл", + "log_file_desc": "Път към файла за извод на лог. Празен за да спре извод на лог. Изисква рестарт.", + "log_http": "Логвай http достъп", + "log_http_desc": "Логва http достъп на терминала. Изисква рестарт.", + "log_to_terminal": "Лог към терминал", + "log_to_terminal_desc": "Логва към терминал заедно с към файл. Винаги истина ако логване към файл е изключено. Изискава рестарт.", + "maximum_session_age": "Максимална Продължителност на Сесия", + "maximum_session_age_desc": "Максимално време на бездействие, преди сесията за вход да изтече, в секунди. Изисква рестартиране.", + "password": "Парола", + "password_desc": "Парола за достъп до Stash. Остави празно за да изключи достъп чрез потребител", + "stash-box_integration": "Stash-box интеграция", + "username": "Потребителско име", + "username_desc": "Потребителско име за достъп до Stash. Остави празно за да изключи достъп чрез потребител", + "log_file_max_size": "Максимален размер на файла", + "log_file_max_size_desc": "Максимален размер в мегабайти на лог файла преди компресиране. 0 MB означава деактивирано. Изисква рестартиране." + }, + "backup_directory_path": { + "description": "Местоположение на папка за резрвни SQLite бази данни", + "heading": "Път към Папка за Резервни Данни" + }, + "blobs_path": { + "description": "Къде във файловата система да се пазят бинарни данни. Позва се само ако се ползва Файлова система за блоб пазене. ВНИМАНИЕ: промяната ще изисква ръчно местене на съществуващи данни.", + "heading": "Път до файловата система за двоични данни" + } + }, + "ui": { + "custom_locales": { + "option_label": "Персонализирана локализация е активирана" + }, + "delete_options": { + "description": "Настройки по подразбиране при триене на картини, галерий и сцени.", + "heading": "Изтриване на настройки", + "options": { + "delete_file": "Изтриване на файлове по подразбиране", + "delete_generated_supporting_files": "Изтриване на генерираните поддържащи файлове по подразбиране" + } + }, + "desktop_integration": { + "desktop_integration": "Десктоп Интеграция", + "notifications_enabled": "Включване на известяването", + "send_desktop_notifications_for_events": "Изпащане на десктоп известявания за събития", + "skip_opening_browser": "Пропускане на отваряне на браузер", + "skip_opening_browser_on_startup": "Пропускане на автоматично отваряне на броузер по време на стартиране" + }, + "detail": { + "compact_expanded_details": { + "description": "Когато е включена, тази настройка ще предосвати разширени детайли запавайки компактна презентация", + "heading": "Компактни разширени детайли" + }, + "enable_background_image": { + "description": "Покажи фонова картина на станицата с детайли.", + "heading": "Включи фонова картина" + }, + "heading": "Станица с детайли", + "show_all_details": { + "description": "Когато е включена, всичкото съдържание ще бъде показано по подразбиране и всеки детайл ще бъде в собствена колона", + "heading": "Покажи всички детайли" + } + }, + "editing": { + "disable_dropdown_create": { + "description": "Премахни възможноста да се създават нови обекти от падащият селектор", + "heading": "Изключи падащо създаване" + }, + "heading": "Редактиране", + "max_options_shown": { + "label": "Максимален брой неща който се показват в падащ селектор" + }, + "rating_system": { + "star_precision": { + "label": "Точност на звездния рейтинг", + "options": { + "full": "Цели", + "half": "Половинки", + "quarter": "Четвъртинки", + "tenth": "Десетици" + } + }, + "type": { + "label": "Тип система за рейтинг", + "options": { + "decimal": "Десетична", + "stars": "Звезди" + } + } + } + }, + "funscript_offset": { + "description": "Време за разминаване в милисекунди за пускане на интерактивни скриптове.", + "heading": "Funscript разминаване (ms)" + }, + "handy_connection": { + "connect": "Свързване", + "server_offset": { + "heading": "Сървърно разминаване" + }, + "status": { + "heading": "Статус на връзка с Handy" + }, + "sync": "Синхронизиране" + }, + "handy_connection_key": { + "description": "Handy connection key за ползване със интерактивни сцени. Слагането на този ключ ще позволи на Stash да сподели иформация за текущата сцена със handyfeeling.com" + } + } + } +} diff --git a/ui/v2.5/src/locales/bn-BD.json b/ui/v2.5/src/locales/bn-BD.json index f90b61a39..687dcdaa9 100644 --- a/ui/v2.5/src/locales/bn-BD.json +++ b/ui/v2.5/src/locales/bn-BD.json @@ -163,8 +163,6 @@ "set_cover_label": "দৃশ্যের কভার ইমেজ সেট করুন", "set_tag_desc": "দৃশ্যে ট্যাগ লাগান, হয় পুনরায় লিখে নয় দৃশ্যে থাকা ট্যাগ ছেটে।", "set_tag_label": "ট্যাগ সেট করুন", - "show_male_desc": "যেকোনোটি নাড়ান পুরুষ প্রদর্শনকারী ট্যাগ এ উপস্থিত হবে।", - "show_male_label": "পুরুষ প্রদর্শনকারী দেখান", "source": "উৎস" }, "noun_query": "কুয়েরি", diff --git a/ui/v2.5/src/locales/ca-ES.json b/ui/v2.5/src/locales/ca-ES.json index 9ad457bc1..dd9cdb7f6 100644 --- a/ui/v2.5/src/locales/ca-ES.json +++ b/ui/v2.5/src/locales/ca-ES.json @@ -136,7 +136,8 @@ "add_play": "Agregar reproduir", "add_to_entity": "Afegir a {entityType}", "create_entity": "Crear {entityType}", - "add_sub_groups": "Afegeix subgrups" + "add_sub_groups": "Afegeix subgrups", + "remove_from_containing_group": "Suprimeix del grup" }, "appears_with": "Apareix amb", "career_length": "Durada de la carrera", @@ -145,7 +146,6 @@ "blacklist_desc": "Els elements de la llista negra estan exclosos de les consultes. Tingueu en compte que són expressions regulars i també insensibles a majúscules i minúscules. Alguns caràcters s'han d'escapar amb una barra inversa: {chars_require_escape}", "set_cover_desc": "Reemplaça la portada de l'escena si se'n troba una.", "set_tag_label": "Estableix les etiquetes", - "show_male_desc": "Estableix si els intèrprets masculins estaran disponibles per etiquetar.", "active_instance": "Instància activa de Stash-Box:", "blacklist_label": "Llista negra", "mark_organized_desc": "Marqueu immediatament l'escena com a Organitzada després de fer clic al botó Desa.", @@ -163,7 +163,6 @@ "query_mode_path_desc": "Utilitza el camí complet del fitxer", "set_cover_label": "Estableix la imatge de la portada de l'escena", "set_tag_desc": "Adjunta etiquetes a l'escena, ja sigui sobreescrivint o fusionant amb etiquetes existents a l'escena.", - "show_male_label": "Mostra els intèrprets masculins", "source": "Font" }, "results": { diff --git a/ui/v2.5/src/locales/cs-CZ.json b/ui/v2.5/src/locales/cs-CZ.json index 622e57706..d08f1703a 100644 --- a/ui/v2.5/src/locales/cs-CZ.json +++ b/ui/v2.5/src/locales/cs-CZ.json @@ -6,15 +6,15 @@ "add_to_entity": "Přidat do {entityType}", "allow": "Povolit", "allow_temporarily": "Povolit dočasně", - "apply": "Potvrdit", + "apply": "Použít", "auto_tag": "Auto Tag", "backup": "Záloha", "browse_for_image": "Vybrat obrázek…", "cancel": "Zrušit", "clean": "Vyčistit", "clear": "Vyčistit", - "clear_back_image": "Vymazat zadní obrázek", - "clear_front_image": "Vymazat přední obrázek", + "clear_back_image": "Vymazat obrázek zadní strany", + "clear_front_image": "Vymazat obrázek přední strany", "clear_image": "Vymazat obrázek", "close": "Zavřít", "confirm": "Potvrdit", @@ -106,9 +106,9 @@ "submit_update": "Publikovat aktualizaci", "swap": "Prohodit", "tasks": { - "clean_confirm_message": "Jste si jistý, že chcete vyčistit databázi? Tato operace vymaže informace z databáze a generovaný obsah pro všechny scény a galerie, které se již nenacházejí v souborovém systému.", + "clean_confirm_message": "Chcete doopravdy provést vyčištění databáze? Tato operace vymaže informace z databáze a generovaný obsah pro všechny scény a galerie, které se již nenacházejí v souborovém systému.", "dry_mode_selected": "Vybrán \"Dry Mode\". Nic nebude smazáno, pouze logováno.", - "import_warning": "Jste si jisti, že chcete importovat? Tato operace smaže databázi a znovu importuje z Vašich exportovaných metadat." + "import_warning": "Chcete doopravdy provést import? Tato operace smaže databázi a znovu naimportuje Vaše exportovaná metadata." }, "temp_disable": "Zakázat dočasně…", "temp_enable": "Povolit dočasně…", @@ -129,9 +129,9 @@ "assign_stashid_to_parent_studio": "Přiřaď Stash ID k existujícímu nadřazenému studiu a aktualizuj metadata", "choose_date": "Vyberte datum", "create_chapters": "Vytvořit kapitolu", - "clear_date_data": "Vymazat data datumu", + "clear_date_data": "Vymazat informace o datumech", "reload": "Načíst znovu", - "clean_generated": "Vyčistěte vygenerované soubory", + "clean_generated": "Vyčistit vygenerované soubory", "remove_date": "Odstranit datum", "add_manual_date": "Přidat datum ručně", "add_play": "Přidat přehrávání", @@ -139,9 +139,21 @@ "reset_cover": "Obnovit výchozí obal", "add_sub_groups": "Přidat podskupinu", "set_cover": "Nastavit jako obal", - "remove_from_containing_group": "Odebrat ze skupiny", + "remove_from_containing_group": "Odstranit ze skupiny", "reset_resume_time": "Obnovit čas pokračování", - "reset_play_duration": "Obnovit dobu přehrávání" + "reset_play_duration": "Obnovit dobu přehrávání", + "sidebar": { + "close": "Zavřít postranní panel", + "open": "Zobrazit postranní panel", + "toggle": "Boční panel" + }, + "play": "Přehrát", + "show_results": "Zobrazit výsledky", + "show_count_results": "Zobrazit {count} výsledků", + "load": "Načíst", + "load_filter": "Načíst filtr", + "add_stash_id": "Přidat Stash ID", + "create_new": "Vytvořit nový" }, "actions_name": "Akce", "age": "Věk", @@ -176,13 +188,15 @@ "set_cover_label": "Nastavit obrázek obalu scény", "set_tag_desc": "Přidat tagy ke scéně, buď přepsáním nebo sloučením existujících tagů na scéně.", "set_tag_label": "Nastavit tagy", - "show_male_desc": "Zapnout / vypnout, zda mají být mužští umělci tagováni.", - "show_male_label": "Ukázat mužské účinkující", "source": "Zdroj", "mark_organized_desc": "Po kliknutí na tlačítko Uložit okamžitě označte scénu jako Uspořádanou.", "mark_organized_label": "Označit jako Uspořádané při uložení", "errors": { "blacklist_duplicate": "Duplikovat položku blacklistu" + }, + "performer_genders": { + "heading": "Pohlaví účinkujicích", + "description": "Účinkující s těmito pohlavími budou zobrazeni při tagování scén." } }, "noun_query": "Dotaz", @@ -204,7 +218,10 @@ "verb_scrape_all": "Scrape vše", "verb_submit_fp": "Publikovat {fpCount, plural, one{# otisk} other{# otisky}}", "verb_toggle_config": "{toggle} {configuration}", - "verb_toggle_unmatched": "{toggle} neidentifikované scény" + "verb_toggle_unmatched": "{toggle} neidentifikované scény", + "verb_add_as_alias": "Přidat scrapované jméno jako alias", + "verb_link_existing": "Odkaz na existujicí", + "verb_match_tag": "Odpovídajicí tag" }, "config": { "about": { @@ -283,7 +300,9 @@ "password_desc": "Heslo pro přístup do aplikace. Ponechte prázdné pro vypnutí autentizace", "stash-box_integration": "Stash-box integrace", "username": "Přihlašovací jméno", - "username_desc": "Přihlašovací jméno pro přístup do aplikace. Ponechte prázdné pro vypnutí autentizace" + "username_desc": "Přihlašovací jméno pro přístup do aplikace. Ponechte prázdné pro vypnutí autentizace", + "log_file_max_size": "Maximální velikost logu", + "log_file_max_size_desc": "Maximální velikost logu v megabytech před kompresí. 0MB pro deaktivaci. Vyžaduje restart." }, "backup_directory_path": { "description": "Adresář umístění záloh databáze SQLite", @@ -333,7 +352,7 @@ "description": "Cesta ke spustitelnému souboru pythonu (nejen ke složce). Používá se pro script scrappery a pluginy. Pokud je prázdné, python bude vyřešen z prostředí", "heading": "Cesta k Pythonu" }, - "scraper_user_agent": "Scraper User Agent", + "scraper_user_agent": "User Agent Scraperu", "scraper_user_agent_desc": "User-Agent řetězec používaný při scrapování http požadavků", "scrapers_path": { "description": "Adresář konfiguračních souborů scraperů", @@ -399,6 +418,10 @@ "blobs_path": { "description": "Kde v souborovém systému ukládat binární data. Použitelné pouze při použití uložiště typu souborového systému blobů. UPOZORNĚNÍ: převrácení těchto údajů vyžaduje ruční přesun existujících dat.", "heading": "Cesta binárních dat souborového systému" + }, + "delete_trash_path": { + "description": "Cesta, kam budou smazané soubory přesunuty, místo aby byly trvale smazány. Pro trvalé smazání souborů ponechte pole prázdné.", + "heading": "Cesta koše" } }, "library": { @@ -435,7 +458,9 @@ "endpoint": "Koncový bod", "graphql_endpoint": "Koncový bod GraphQL", "name": "Název", - "title": "Stash-box Endpointy" + "title": "Stash-box Endpointy", + "max_requests_per_minute": "Maximální počet dotazů za minutu", + "max_requests_per_minute_description": "Používá základní hodnotu {defaultValue} pokud je nastaven na 0" }, "system": { "transcoding": "Překódování" @@ -561,7 +586,9 @@ "whitespace_chars": "Whitespace znaky", "whitespace_chars_desc": "Tyto znaky v názvu budou nahrazeny prázdným znakem (mezerou)" }, - "scene_tools": "Nástroje pro scény" + "scene_tools": "Nástroje pro scény", + "graphql_playground": "GraphQL hřiště", + "heading": "Nástroje" }, "ui": { "abbreviate_counters": { @@ -710,7 +737,7 @@ "heading": "Automaticky spustit video při přehrávání vybraného" }, "continue_playlist_default": { - "description": "Přehrát následující scénu na seznamu při skončení vida", + "description": "Přehrát následující scénu na seznamu při skončení videa", "heading": "(Výchozí nastavení) Pokračovat v playlistu" }, "show_scrubber": "Zobrazit Scrubber", @@ -786,6 +813,18 @@ "use_stash_hosted_funscript": { "description": "Je-li povoleno, budou funscripty poskytovány přímo ze Stash do vašeho zařízení Handy bez použití serveru Handy třetí strany. Vyžaduje, aby byl Stash dostupný z vašeho zařízení Handy a vygenerovaný API klíč, pokud má stash nakonfigurované údaje.", "heading": "Funscripty podávejte přímo" + }, + "sfw_mode": { + "description": "Zapněte zda použáváte stash k ukládání SFW obsahu. Schová nebo změní některé aspekty uživatelského rozhraní související s obsahem pro dospělé.", + "heading": "Režim obsahu SFW" + }, + "performer_list": { + "heading": "List účinkujicích", + "options": { + "show_links_on_grid_card": { + "heading": "Zobrazit odkazy na mřížce karet účinkujicích" + } + } } }, "advanced_mode": "Pokročilý mód" @@ -840,12 +879,12 @@ "developmentVersion": "Vývojářská verze", "dialogs": { "delete_alert": "Následující {count, plural, one {{singularEntity}} other {{pluralEntity}}} budou permanentně smazány:", - "delete_confirm": "Jste si jisti, že chcete smazat {entityName}?", - "delete_entity_desc": "{count, plural, one {Jste si jisti, že checete smazat toto {singularEntity}? Pokud není soubor rovněž smazán, tato {singularEntity} bude znovu přidána při příštím skenování.} other {Jste si jisti, že chcete smazat tyto {pluralEntity}? Pokud nejsou soubory rovněž smazány, tyto {pluralEntity} budou znovu přidány při příštím skenování.}}", + "delete_confirm": "Chcete doopravdy smazat {entityName}?", + "delete_entity_desc": "{count, plural, one {Doopravdy chcete smazat {singularEntity}? Pokud nebude smazán i soubor, {singularEntity} bude znovu přidán při příštím skenování.} other {Doopravdy chcete smazat tyto {pluralEntity}? Pokud nebudou smazány i soubory, tyto {pluralEntity} budou znovu přidány při příštím skenování.}}", "delete_entity_title": "{count, plural, one {Smazat {singularEntity}} other {Smazat {pluralEntity}}}", "delete_galleries_extra": "…navíc všechny obrazové soubory, které nejsou připojeny k žádné jiné galerii.", "delete_gallery_files": "Smazat složku galerie/zip soubor a jakékoliv obrázky nenapojené na jinou galerii.", - "delete_object_desc": "Jste si jisti, že chcete smazat {count, plural, one {tuto {singularEntity}} other {tyto {pluralEntity}}}?", + "delete_object_desc": "Doopravdy chcete smazat {count, plural, one {{singularEntity}} other {tyto {pluralEntity}}}?", "delete_object_overflow": "…a {count} other {count, plural, one {{singularEntity}} other {{pluralEntity}}}.", "delete_object_title": "Smazat {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "dont_show_until_updated": "Skrýt do příští aktualizace", @@ -872,13 +911,13 @@ "pan_y": "Náklon Y", "zoom": "Zvětšit" }, - "page_header": "Stránka {page} / {total}" + "page_header": "Stránka {page} / {total}", + "disable_animation": "Zakázat animaci přechodu mezi obrázky" }, "merge_tags": { "destination": "Cíl", "source": "Zdroj" }, - "overwrite_filter_confirm": "JSte si jisti, že chcete přepsat aktuálně uložený dotaz {entityName}?", "scene_gen": { "force_transcodes": "Vynutit generování Transkódu", "force_transcodes_tooltip": "Ve výchozím nastavení, transkódy jsou generovány pouze tehdy, když video soubor není podporován prohlížečem. V případě aktivování, transkódy budou generovány i v ostatních případech.", @@ -946,7 +985,15 @@ "clear_o_history_confirm": "Opravdu chcete vymazat historii O?", "clear_play_history_confirm": "Opravdu chcete vymazat historii přehrávání?", "delete_entity_simple_desc": "{count, plural, one {Opravdu chcete smazat tuto {singularEntity}?} other {Opravdu chcete smazat tyto {pluralEntity}?}}", - "performers_found": "{count} nalezených účinkujících" + "performers_found": "{count} nalezených účinkujících", + "overwrite_filter_warning": "Uložený filtr \"{entityName}\" bude přepsán.", + "set_default_filter_confirm": "Chcete doopravdy nastavit tento filtr jako výchozí?", + "clear_o_history_confirm_sfw": "Opravdu chcete vymazat historii lajků?", + "delete_alert_to_trash": "Následující {count, plural, one {{singularEntity}} other {{pluralEntity}}} budou přesunuty do koše:", + "stashid_exists_warning": "Stávající stash id pro tento stash-box bude nahrazeno.", + "studios_found": "{count} studií nalezeno", + "tags_found": "{count} tagů nalezeno", + "scrape_results_missing": "Chybějící" }, "chapters": "Kapitoly", "circumcised": "Obřezán", @@ -991,7 +1038,7 @@ "required": "${path} je vyžadované pole", "unique": "${path} musí být jedinečná", "blank": "${path} nesmí být prázdná", - "date_invalid_form": "${path} musí být ve formátu YYYY-MM-DD (Rok-Měsíc-Den)", + "date_invalid_form": "${path} musí být ve formátu YYYY, YYYY-MM, nebo YYYY-MM-DD (Rok-Měsíc-Den)", "end_time_before_start_time": "Čas ukončení musí být větší nebo roven času zahájení" }, "type": "Typ", @@ -1012,7 +1059,8 @@ "grid": "Mřížka", "tagger": "Tagger", "wall": "Stěna", - "unknown": "Neznámý" + "unknown": "Neznámý", + "label_current": "Mód zobrazení: {current}" }, "effect_filters": { "aspect": "Aspekt", @@ -1121,18 +1169,18 @@ "failed_to_save_performer": "Nepodařilo se uložit umělce „{performer}“", "network_error": "Chyba sítě", "performer_already_tagged": "Účinkujicí již byl označen", - "performer_names_separated_by_comma": "Jména účinkujících oddělená čárkou", "refreshing_will_update_the_data": "Obnovení aktualizuje data všech označených účinkujících z instance stash-boxu.", "status_tagging_job_queued": "Stav: Úloha tagování ve frontě", "name_already_exists": "Název již existuje", "update_performers": "Aktualizovat Účinkující", "updating_untagged_performers_description": "Aktualizování nepřiřazených účinkujících se pokusí nalézt všechny účinkující, kteří postrádají StashID, a aktualizovat metadata.", - "update_performer": "Aktualizovat Účinkující" + "update_performer": "Aktualizovat Účinkující", + "performer_names_or_stashids_separated_by_comma": "Jména účinkujících nebo Stash ID oddělené čárkou" }, "setup": { "paths": { "where_can_stash_store_its_generated_content_description": "Aby Stash mohl poskytovat miniatury, náhledy a sprity generuje Stash obrázky a videa. To zahrnuje také transkódování pro nepodporované formáty souborů. Ve výchozím nastavení Stash vytvoří generated adresář v adresáři obsahujícím váš konfigurační soubor. Pokud chcete změnit, kam se budou tato generovaná media ukládat, zadejte prosím absolutní nebo relativní (k aktuálnímu pracovnímu adresáři) cestu. Stash vytvoří tento adresář, pokud ještě neexistuje.", - "where_is_your_porn_located_description": "Přidejte adresáře obsahující vaše porno videa a obrázky. Stash použije tyto adresáře k vyhledání videí a obrázků během skenování.", + "where_is_your_porn_located_description": "Přidejte adresáře obsahující váše videa a obrázky. Stash použije tyto adresáře k vyhledání videí a obrázků během skenování.", "where_can_stash_store_blobs_description": "Kam může Stash ukládat binární data jako jsou obaly scén, účinkující, studia a obrázky tagů, buď v databázi nebo v souborovém systému. Ve výchozím nastavení bude tato data ukládat do souborového systému v podadresáři blobs v adresáři obsahujícím váš konfigurační soubor. Pokud to chcete změnit, zadejte prosím absolutní nebo relativní (k aktuálnímu pracovnímu adresáři) cestu. Stash vytvoří tento adresář, pokud ještě neexistuje.", "path_to_cache_directory_empty_for_default": "cesta k adresáři mezipaměti (ve výchozím nastavení prázdné)", "path_to_generated_directory_empty_for_default": "cesta k adresáři vygenerovaných souborů (ve výchozím nastavení prázdné)", @@ -1141,16 +1189,19 @@ "where_can_stash_store_its_generated_content": "Kam může Stash ukládat svůj vygenerovaný obsah?", "where_can_stash_store_its_database_warning": "VAROVÁNÍ: Uložení databáze na jiný systém, než ze kterého se spouští Stash (např. uložení databáze na NAS při spuštění serveru Stash na jiném počítači), nepodporováno! SQLite není určen pro použití v síti a pokus o to může velmi snadno způsobit poškození celé vaší databáze.", "database_filename_empty_for_default": "název souboru databáze (ve výchozím nastavení prázdné)", - "description": "Dále musíme určit, kde najdeme vaši sbírku porna a kam uložit databázi Stash, vygenerované soubory a soubory mezipaměti. Tato nastavení lze v případě potřeby později změnit.", + "description": "Dále musíme určit, kde najdeme vaši sbírku obsahu a kam uložit databázi Stash, vygenerované soubory a soubory mezipaměti. Tato nastavení lze v případě potřeby později změnit.", "path_to_blobs_directory_empty_for_default": "cesta k adresáři blobů (ve výchozím nastavení prázdné)", "stash_alert": "Nebyla vybrána žádná cesta knihovny. Žádné médium nebude možné naskenovat do Stash. Jste si jisti?", - "where_is_your_porn_located": "Kde se nachází vaše porno?", + "where_is_your_porn_located": "Kde se nachází váš obsah?", "where_can_stash_store_cache_files_description": "Aby některé funkce, jako je živé transkódování HLS/DASH, fungovaly, vyžaduje Stash adresář mezipaměti pro dočasné soubory. Ve výchozím nastavení Stash vytvoří adresář mezipaměť v adresáři obsahujícím váš konfigurační soubor. Pokud to chcete změnit, zadejte prosím absolutní nebo relativní (k aktuálnímu pracovnímu adresáři) cestu. Stash vytvoří tento adresář, pokud ještě neexistuje.", "where_can_stash_store_its_database": "Kde může Stash uložit svou databázi?", "where_can_stash_store_blobs": "Kde může Stash ukládat binární data databáze?", "where_can_stash_store_blobs_description_addendum": "Případně můžete tato data uložit do databáze. Poznámka: Tím se zvětší velikost souboru databáze a prodlouží se doba migrace databáze.", - "where_can_stash_store_its_database_description": "Stash používá databázi SQLite k ukládání metadat porna. Ve výchozím nastavení bude tento soubor vytvořen jako stash-go.sqlite v adresáři obsahujícím váš konfigurační soubor. Pokud to chcete změnit, zadejte prosím absolutní nebo relativní (k aktuálnímu pracovnímu adresáři) název souboru.", - "store_blobs_in_database": "Uložte bloby do databáze" + "where_can_stash_store_its_database_description": "Stash používá databázi SQLite k ukládání metadat obsahu. Ve výchozím nastavení bude tento soubor vytvořen jako stash-go.sqlite v adresáři obsahujícím váš konfigurační soubor. Pokud to chcete změnit, zadejte prosím absolutní nebo relativní (k aktuálnímu pracovnímu adresáři) název souboru.", + "store_blobs_in_database": "Uložte bloby do databáze", + "sfw_content_settings": "Používáte stash k SFW obsahu?", + "sfw_content_settings_description": "Stash lze použít ke správě SFW obsahu, jako jsou fotografie, umění, komiksy a další. Povolení této možnosti upraví některé vlastnosti uživatelského rozhraní tak, aby byly vhodnější pro SFW obsah.", + "use_sfw_content_mode": "Použít režim obsahu SFW" }, "stash_setup_wizard": "Průvodce nastavením Stash", "success": { @@ -1261,7 +1312,7 @@ "syncing": "Probíhá synchronizace se serverem", "uploading": "Nahrávání skriptu" }, - "hasMarkers": "Má značky", + "hasMarkers": "Značky", "height_cm": "Výška (cm)", "include_sub_studios": "Zahrnout dceřiná studia", "interactive": "Interaktivní", @@ -1294,7 +1345,6 @@ "o_count": "Počet O" }, "megabits_per_second": "{value} mbps", - "o_counter": "O-Počítadlo", "none": "Žádný", "pagination": { "last": "Poslední", @@ -1315,7 +1365,9 @@ "saved_filters": "Uložené filtry", "update_filter": "Aktualizovat filtr", "edit_filter": "Upravit filtr", - "name": "Filtr" + "name": "Filtr", + "more_filter_criteria": "+{count} navíc", + "search_term": "Hledaný výraz" }, "ethnicity": "Etnická příslušnost", "existing_value": "Existujicí hodnota", @@ -1336,7 +1388,7 @@ }, "hair_color": "Barva vlasů", "help": "Pomoc", - "hasChapters": "Má kapitoly", + "hasChapters": "Kapitoly", "height": "Výška", "ignore_auto_tag": "Ignoruj automatické tagování", "instagram": "Instragram", @@ -1444,11 +1496,11 @@ "status_tagging_job_queued": "Status: Úloha označování zařazena do fronty", "status_tagging_studios": "Status: Označování studií", "studio_already_tagged": "Studio již označeno", - "studio_names_separated_by_comma": "Názvy studií oddělené čárkou", "tag_status": "Status označení", "network_error": "Chyba sítě", "studio_selection": "Výběr studia", - "studio_successfully_tagged": "Studio úspěšně označeno" + "studio_successfully_tagged": "Studio úspěšně označeno", + "studio_names_or_stashids_separated_by_comma": "Jména studií nebo Stash ID odělená čárkou" }, "synopsis": "Souhrn", "stashbox": { @@ -1522,5 +1574,23 @@ }, "age_on_date": "{age} během produkce", "sort_name": "Seřadit jména", - "eta": "Přibližný čas dokončení" + "eta": "Přibližný čas dokončení", + "login": { + "password": "Heslo", + "invalid_credentials": "Neplatné uživatelské jméno nebo heslo", + "login": "Přihlášení", + "username": "Uživatelské jméno", + "internal_error": "Neočekávaná interní chyba. Podívej se do logu pro více detailů" + }, + "last_o_at_sfw": "Poslední lajk", + "o_count_sfw": "Lajky", + "o_history_sfw": "Historie lajků", + "odate_recorded_no_sfw": "Žádný datum lajku nebyl zaznamenán", + "scenes_duration": "Trvání scény", + "stashbox_search": { + "header": "Hledej {entityType} ve StashBoxu", + "no_results": "Žádné výsledky nenalezeny.", + "placeholder_name_or_id": "{entityType} jméno nebo StashID...", + "select_stashbox": "Vybrat StashBox..." + } } diff --git a/ui/v2.5/src/locales/da-DK.json b/ui/v2.5/src/locales/da-DK.json index 033f1896e..ab89ec69a 100644 --- a/ui/v2.5/src/locales/da-DK.json +++ b/ui/v2.5/src/locales/da-DK.json @@ -174,8 +174,6 @@ "set_cover_label": "Vælg scene cover billede", "set_tag_desc": "Vedhæft tags til scenen, enten ved at overskrive eller flette med eksisterende tags.", "set_tag_label": "Sæt tags", - "show_male_desc": "Skift, om mandlige kunstnere vil være tilgængelige til at tagge.", - "show_male_label": "Vis mandlige kunstnere", "source": "Kilde", "mark_organized_desc": "Sæt scene som Organiseret efter klik på Save knappen.", "mark_organized_label": "Marker som Organiseret ved gem" @@ -821,7 +819,6 @@ "destination": "Destination", "source": "Kilde" }, - "overwrite_filter_confirm": "Er du sikker på, at du vil overskrive eksisterende gemte forespørgsel {entityName}?", "scene_gen": { "force_transcodes": "Tving omkodningsgenerering", "force_transcodes_tooltip": "Som standard genereres omkoder kun, når videofilen ikke understøttes i browseren. Når det er aktiveret, genereres omkoder, selv når videofilen ser ud til at være understøttet i browseren.", @@ -1033,7 +1030,6 @@ "name": "Navn", "new": "Ny", "none": "Ingen", - "o_counter": "O-tæller", "operations": "Operationer", "organized": "Organiseret", "pagination": { @@ -1074,7 +1070,6 @@ "no_results_found": "Ingen resultater fundet.", "number_of_performers_will_be_processed": "{performer_count} kunstnere vil blive behandlet", "performer_already_tagged": "Kunstner allerede tagget", - "performer_names_separated_by_comma": "Artistnavne adskilt af komma", "performer_selection": "Valg af kunstner", "performer_successfully_tagged": "Kunstner tagget med succes:", "query_all_performers_in_the_database": "Alle kunstnere i databasen", diff --git a/ui/v2.5/src/locales/de-DE.json b/ui/v2.5/src/locales/de-DE.json index df7d089db..af20a8151 100644 --- a/ui/v2.5/src/locales/de-DE.json +++ b/ui/v2.5/src/locales/de-DE.json @@ -141,7 +141,17 @@ "remove_from_containing_group": "Von Gruppe entfernen", "reset_play_duration": "Spieldauer zurücksetzten", "reset_resume_time": "Fortschritt zurücksetzten", - "add_sub_groups": "Untergruppen hinzufügen" + "add_sub_groups": "Untergruppen hinzufügen", + "play": "Abspielen", + "sidebar": { + "toggle": "Seitenleiste umschalten", + "close": "Seitenleiste schließen", + "open": "Seitenleiste öffnen" + }, + "show_count_results": "Zeige {count} Ergebnisse", + "show_results": "Ergebnisse anzeigen", + "load_filter": "Filter laden", + "load": "Laden" }, "actions_name": "Aktionen", "age": "Alter", @@ -187,8 +197,6 @@ "set_cover_label": "Setze Cover-Bild", "set_tag_desc": "Hänge Tags der Szene an, entweder durch Überschreiben oder Zusammenführen mit bereits angehängten Tags.", "set_tag_label": "Tags anhängen", - "show_male_desc": "Auswahl ob männliche Darsteller der Szene hinzugefügt werden können.", - "show_male_label": "Männliche Darsteller anzeigen", "source": "Quelle", "mark_organized_label": "Beim speichern als organisiert markieren", "mark_organized_desc": "Markiere die Szene nach dem klicken auf Speichern als organisiert.", @@ -294,7 +302,9 @@ "password_desc": "Passwort für den Zugriff auf Stash. Feld leer lassen, um Benutzerauthentifizierung zu deaktivieren", "stash-box_integration": "Stash-box Einbindung", "username": "Benutzername", - "username_desc": "Benutzername für den Zugriff auf Stash. Feld leer lassen, um Benutzerauthentifizierung zu deaktivieren" + "username_desc": "Benutzername für den Zugriff auf Stash. Feld leer lassen, um Benutzerauthentifizierung zu deaktivieren", + "log_file_max_size": "Maximale Log Größe", + "log_file_max_size_desc": "Maximale Größe, in Megabytes, von den Log Files bevor es komprimiert wird. 0MB ist deaktiviert. Benötigt Neustart." }, "backup_directory_path": { "description": "Verzeichnisspeicherort für SQLite-Datenbankdateisicherungen", @@ -446,7 +456,9 @@ "endpoint": "Endpunkt", "graphql_endpoint": "GraphQL-Endpunkt", "name": "Name", - "title": "Stash-Box-Endpunkte" + "title": "Stash-Box-Endpunkte", + "max_requests_per_minute": "Max requests pro Minute", + "max_requests_per_minute_description": "Verwendet default Wert {defaultValue} wenn auf 0 gesetzt" }, "system": { "transcoding": "Transcodierung" @@ -572,7 +584,9 @@ "whitespace_chars": "Zwischenraumzeichen", "whitespace_chars_desc": "Diese Zeichen werden im Titel durch Zwischenraumzeichen ersetzt" }, - "scene_tools": "Szenen-Tools" + "scene_tools": "Szenen-Tools", + "heading": "Werkzeuge", + "graphql_playground": "GraphQL-Spielplatz" }, "ui": { "abbreviate_counters": { @@ -737,7 +751,8 @@ }, "show_ab_loop_controls": "Die Steuerungselemente des AB-Loop-Plugins anzeigen", "disable_mobile_media_auto_rotate": "Deaktiviere das automatische Drehen von Vollbildmedien auf Mobilgeräten", - "enable_chromecast": "Chromecast aktivieren" + "enable_chromecast": "Chromecast aktivieren", + "show_range_markers": "Zeige Range der Markierungen" } }, "scene_wall": { @@ -796,6 +811,18 @@ "use_stash_hosted_funscript": { "description": "Wenn aktiviert, werden Funscripts direkt von Stash an dein Handy-Gerät gesendet, ohne Handy-Server von Drittanbietern zu verwenden. Erfordert, dass Stash von deinem Handy-Gerät aus zugänglich ist und ein API-Schlüssel generiert wurde, falls Stash mit Zugangsdaten konfiguriert ist.", "heading": "Funscripts direkt bereitstellen" + }, + "performer_list": { + "options": { + "show_links_on_grid_card": { + "heading": "Zeige den Link auf der Darsteller Gitterkarte" + } + }, + "heading": "Darsteller Liste" + }, + "sfw_mode": { + "description": "Aktivieren wenn man Stash für das speichern von SFW content benutzt. Versteckt oder Ändernt ein paar NFSW eigenschaften des UI.", + "heading": "SFW Content Modus" } }, "advanced_mode": "Fortgeschrittener Modus" @@ -905,7 +932,6 @@ "destination": "Ziel", "source": "Quelle" }, - "overwrite_filter_confirm": "Möchten Sie die vorhandene gespeicherte Anfrage {entityName} wirklich überschreiben?", "reassign_entity_title": "{count, plural, one {Weise {singularEntity} neu zu} other {Weise {pluralEntity} neu zu}}}", "reassign_files": { "destination": "Neu zuweisen an" @@ -958,7 +984,11 @@ "unsaved_changes": "Nicht gespeicherte Änderungen. Bist du sicher dass du die Seite verlassen willst?", "clear_play_history_confirm": "Bist du sicher, dass du den Wiedergabeverlauf löschen möchtest?", "performers_found": "{count} Darsteller:innen gefunden", - "clear_o_history_confirm": "Möchten Sie wirklich den O-Verlauf löschen?" + "clear_o_history_confirm": "Möchten Sie wirklich den O-Verlauf löschen?", + "overwrite_filter_warning": "Der gespeicherte Filter \"{entityName}\" wird überschrieben.", + "set_default_filter_confirm": "Sind Sie sicher, dass Sie diesen Filter als Standard festlegen möchten?", + "clear_o_history_confirm_sfw": "Bist du dir sicher das du den Verlauf löschen willst?", + "tags_found": "{count} Tags gefunden" }, "dimensions": "Maße", "director": "Regisseur", @@ -968,7 +998,8 @@ "list": "Liste", "tagger": "Tagger", "unknown": "Unbekannt", - "wall": "Wand" + "wall": "Wand", + "label_current": "Anzeigemodus: {current}" }, "donate": "Spenden", "dupe_check": { @@ -1115,7 +1146,7 @@ "interactive_speed": "Interaktive Geschwindigkeit", "performer_card": { "age": "{age} {years_old}", - "age_context": "{age} {years_old} in dieser Szene" + "age_context": "{age} {years_old} bei der Produktion" }, "phash": "PHashwert", "play_count": "Anzahl Wiedergaben", @@ -1129,7 +1160,6 @@ "name": "Name", "new": "Neu", "none": "Keiner", - "o_counter": "O-Zähler", "operations": "Operationen", "organized": "Organisiert", "pagination": { @@ -1174,7 +1204,6 @@ "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", @@ -1218,7 +1247,9 @@ "edit_filter": "Filter editieren", "name": "Filter", "saved_filters": "Gespeicherte Filter", - "update_filter": "Filter aktualisieren" + "update_filter": "Filter aktualisieren", + "more_filter_criteria": "+{count} mehr", + "search_term": "Suchbegriff" }, "second": "Sekunde", "seconds": "Sekunden", @@ -1283,7 +1314,8 @@ "where_is_your_porn_located": "Wo finden wir deine Porno-Kollektion?", "where_is_your_porn_located_description": "Füge Ordner hinzu in denen sich deine Porno-Videos und -Bilder befinden. Stash wird diese Ordner nutzen, um Videos und Bilder in das System einzupflegen.", "path_to_blobs_directory_empty_for_default": "Pfad zum Verzeichnis der blobs (standardmäßig leer)", - "store_blobs_in_database": "blobs in der Datenbank speichern" + "store_blobs_in_database": "blobs in der Datenbank speichern", + "sfw_content_settings": "Benutzt du stash auch für SFW Inhalte?" }, "stash_setup_wizard": "Einrichtungshelfer für Stash", "success": { @@ -1420,7 +1452,6 @@ "failed_to_save_studio": "Speichern des Studios \"{studio}\" Fehlgeschlagen", "status_tagging_job_queued": "Status: Tagging Job in der Warteschlange", "studio_already_tagged": "Studio schon getaggt", - "studio_names_separated_by_comma": "Studionamen mit Komma trennen", "studio_selection": "Ausgewählte Studios", "to_use_the_studio_tagger": "Um den Studiotagger zu benutzen, muss eine stash-box Instanz konfiguriert werden.", "untagged_studios": "Nicht getaggte Studios", @@ -1484,7 +1515,9 @@ "custom_fields": { "title": "Benutzerdefinierte Felder", "value": "Wert", - "field": "Feld" + "field": "Feld", + "criteria_format_string": "{criterion} (custom field) {modifierString} {valueString}", + "criteria_format_string_others": "{criterion} (custom field) {modifierString} {valueString} (+{others} others)" }, "distance": "Distanz", "group_count": "Gruppenanzahl", @@ -1517,5 +1550,19 @@ "tag_sub_tag_tooltip": "Hat Untertags", "include_sub_groups": "Untergruppen einbeziehen", "studio_and_parent": "Studio & Mutterstudio", - "eta": "Edited to add" + "eta": "Edited to add", + "login": { + "login": "Login", + "internal_error": "Unvorhergesehener interner Fehler. Weitere Details in den Logs", + "password": "Passwort", + "invalid_credentials": "Ungültiger Nutzername oder Passwort", + "username": "Benutzername" + }, + "age_on_date": "bei Produktion", + "sort_name": "Namen sortieren", + "scenes_duration": "Szenen Dauer", + "last_o_at_sfw": "Letztes mal ein Gefällt mir gegeben am", + "o_count_sfw": "Gefällt mir", + "o_history_sfw": "Gefällt mir Verlauf", + "odate_recorded_no_sfw": "Kein Gefällt mir Datum vermerkt" } diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index bd6308425..76df6cf33 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -7,6 +7,7 @@ "add_sub_groups": "Add Sub-Groups", "add_o": "Add O", "add_play": "Add play", + "add_stash_id": "Add Stash ID", "add_to_entity": "Add to {entityType}", "allow": "Allow", "allow_temporarily": "Allow temporarily", @@ -33,6 +34,7 @@ "create_chapters": "Create Chapter", "create_entity": "Create {entityType}", "create_marker": "Create Marker", + "create_new": "Create new", "create_parent_studio": "Create parent studio", "created_entity": "Created {entity_type}: {entity_name}", "customise": "Customise", @@ -68,11 +70,11 @@ "ignore": "Ignore", "import": "Import…", "import_from_file": "Import from file", + "load": "Load", + "load_filter": "Load filter", "logout": "Log out", "make_primary": "Make Primary", "merge": "Merge", - "merge_from": "Merge from", - "merge_into": "Merge into", "migrate_blobs": "Migrate Blobs", "migrate_scene_screenshots": "Migrate Scene Screenshots", "next_action": "Next", @@ -103,6 +105,7 @@ "reshuffle": "Reshuffle", "running": "running", "save": "Save", + "save_and_new": "Save & New", "save_delete_settings": "Use these options by default when deleting", "save_filter": "Save filter", "scan": "Scan", @@ -115,6 +118,7 @@ "select_entity": "Select {entityType}", "select_folders": "Select folders", "select_none": "Select None", + "invert_selection": "Invert Selection", "selective_auto_tag": "Selective Auto Tag", "selective_clean": "Selective Clean", "selective_scan": "Selective Scan", @@ -187,6 +191,10 @@ }, "mark_organized_desc": "Immediately mark the scene as Organized after the Save button is clicked.", "mark_organized_label": "Mark as Organized on save", + "performer_genders": { + "heading": "Performer genders", + "description": "Performers with these genders will be shown when tagging scenes." + }, "query_mode_auto": "Auto", "query_mode_auto_desc": "Uses metadata if present, or filename", "query_mode_dir": "Dir", @@ -202,8 +210,6 @@ "set_cover_label": "Set scene cover image", "set_tag_desc": "Attach tags to scene, either by overwriting or merging with existing tags on scene.", "set_tag_label": "Set tags", - "show_male_desc": "Toggle whether male performers will be available to tag.", - "show_male_label": "Show male performers", "source": "Source" }, "noun_query": "Query", @@ -220,9 +226,13 @@ "phash_matches": "{count} PHashes match", "unnamed": "Unnamed" }, + "verb_add_as_alias": "Add scraped name as alias", + "verb_link_existing": "Link to existing", "verb_match_fp": "Match Fingerprints", + "verb_match_tag": "Match Tag", "verb_matched": "Matched", "verb_scrape_all": "Scrape All", + "verb_scrape_selected": "Scrape Selected", "verb_submit_fp": "Submit {fpCount, plural, one{# Fingerprint} other{# Fingerprints}}", "verb_toggle_config": "{toggle} {configuration}", "verb_toggle_unmatched": "{toggle} unmatched scenes" @@ -299,6 +309,8 @@ "log_http_desc": "Logs http access to the terminal. Requires restart.", "log_to_terminal": "Log to terminal", "log_to_terminal_desc": "Logs to the terminal in addition to a file. Always true if file logging is disabled. Requires restart.", + "log_file_max_size": "Maximum log size", + "log_file_max_size_desc": "Maximum size in megabytes of the log file before it is compressed. 0MB is disabled. Requires restart.", "maximum_session_age": "Maximum Session Age", "maximum_session_age_desc": "Maximum idle time before a login session is expired, in seconds. Requires restart.", "password": "Password", @@ -311,6 +323,10 @@ "description": "Directory location for SQLite database file backups", "heading": "Backup Directory Path" }, + "delete_trash_path": { + "description": "Path where deleted files will be moved to instead of being permanently deleted. Leave empty to permanently delete files.", + "heading": "Trash Path" + }, "blobs_path": { "description": "Where in the filesystem to store binary data. Applicable only when using the Filesystem blob storage type. WARNING: changing this requires manually moving existing data.", "heading": "Binary data filesystem path" @@ -501,7 +517,9 @@ }, "generate_clip_previews_during_scan": "Generate previews for image clips", "generate_desc": "Generate supporting image, sprite, video, vtt and other files.", - "generate_phashes_during_scan": "Generate perceptual hashes", + "generate_image_phashes_during_scan": "Generate image perceptual hashes", + "generate_image_phashes_during_scan_tooltip": "For deduplication and identification.", + "generate_phashes_during_scan": "Generate video perceptual hashes", "generate_phashes_during_scan_tooltip": "For deduplication and scene identification.", "generate_previews_during_scan": "Generate animated image previews", "generate_previews_during_scan_tooltip": "Also generate animated (webp) previews, only required when Scene/Marker Wall Preview Type is set to Animated Image. When browsing they use less CPU than the video previews, but are generated in addition to them and are larger files.", @@ -600,6 +618,20 @@ "heading": "Custom CSS", "option_label": "Custom CSS enabled" }, + "troubleshooting_mode": { + "button": "Troubleshooting Mode", + "dialog_title": "Enable Troubleshooting Mode", + "dialog_description": "This will temporarily disable all customizations to help diagnose issues:", + "dialog_item_plugins": "All plugins", + "dialog_item_css": "Custom CSS", + "dialog_item_js": "Custom JavaScript", + "dialog_item_locales": "Custom locales", + "dialog_log_level": "Log level will be set to Debug for detailed diagnostics.", + "dialog_reload_note": "The page will reload automatically.", + "enable": "Enable & Reload", + "overlay_message": "Troubleshooting Mode is active - all customizations are disabled", + "exit": "Exit" + }, "custom_javascript": { "description": "Page must be reloaded for changes to take effect. There is no guarantee of compatibility between custom Javascript and future releases of Stash.", "heading": "Custom Javascript", @@ -610,6 +642,10 @@ "heading": "Custom localisation", "option_label": "Custom localisation enabled" }, + "custom_title": { + "description": "Custom text to append to the page title. If empty, defaults to 'Stash'.", + "heading": "Custom Title" + }, "delete_options": { "description": "Default settings when deleting images, galleries, and scenes.", "heading": "Delete Options", @@ -782,6 +818,10 @@ "description": "Number of times to attempt to scroll before moving to the next/previous item. Only applies for Pan Y scroll mode.", "heading": "Scroll attempts before transition" }, + "sfw_mode": { + "description": "Enable if using stash to store SFW content. Hides or changes some adult-content-related aspects of the UI.", + "heading": "SFW Content Mode" + }, "show_tag_card_on_hover": { "description": "Show tag card when hovering tag badges", "heading": "Tag card tooltips" @@ -799,6 +839,14 @@ } } }, + "performer_list": { + "heading": "Performer list", + "options": { + "show_links_on_grid_card": { + "heading": "Display links on performer grid cards" + } + } + }, "tag_panel": { "heading": "Tag view", "options": { @@ -887,9 +935,11 @@ "developmentVersion": "Development Version", "dialogs": { "clear_o_history_confirm": "Are you sure you want to clear the O history?", + "clear_o_history_confirm_sfw": "Are you sure you want to clear the like history?", "clear_play_history_confirm": "Are you sure you want to clear the play history?", "create_new_entity": "Create new {entity}", "delete_alert": "The following {count, plural, one {{singularEntity}} other {{pluralEntity}}} will be deleted permanently:", + "delete_alert_to_trash": "The following {count, plural, one {{singularEntity}} other {{pluralEntity}}} will be moved to trash:", "delete_confirm": "Are you sure you want to delete {entityName}?", "delete_entity_desc": "{count, plural, one {Are you sure you want to delete this {singularEntity}? Unless the file is also deleted, this {singularEntity} will be re-added when scan is performed.} other {Are you sure you want to delete these {pluralEntity}? Unless the files are also deleted, these {pluralEntity} will be re-added when scan is performed.}}", "delete_entity_simple_desc": "{count, plural, one {Are you sure you want to delete this {singularEntity}?} other {Are you sure you want to delete these {pluralEntity}?}}", @@ -913,6 +963,7 @@ }, "lightbox": { "delay": "Delay (Sec)", + "disable_animation": "Disable transition animation between images", "display_mode": { "fit_horizontally": "Fit horizontally", "fit_to_screen": "Fit to screen", @@ -938,13 +989,10 @@ "empty_results": "Destination field values will be unchanged.", "source": "Source" }, - "merge_tags": { - "destination": "Destination", - "source": "Source" - }, "overwrite_filter_warning": "Saved filter \"{entityName}\" will be overwritten.", "performers_found": "{count} performers found", "reassign_entity_title": "{count, plural, one {Reassign {singularEntity}} other {Reassign {pluralEntity}}}", + "stashid_exists_warning": "The existing stash id for this stash-box will be replaced.", "reassign_files": { "destination": "Reassign to" }, @@ -953,6 +1001,8 @@ "covers": "Scene covers", "force_transcodes": "Force Transcode generation", "force_transcodes_tooltip": "By default, transcodes are only generated when the video file is not supported in the browser. When enabled, transcodes will be generated even when the video file appears to be supported in the browser.", + "image_phash": "Image perceptual hashes", + "image_phash_tooltip": "For deduplication and identification", "image_previews": "Animated Image Previews", "image_previews_tooltip": "Also generate animated (webp) previews, only required when Scene/Marker Wall Preview Type is set to Animated Image. When browsing they use less CPU than the video previews, but are generated in addition to them and are larger files.", "image_thumbnails": "Image Thumbnails", @@ -966,7 +1016,7 @@ "override_preview_generation_options": "Override Preview Generation Options", "override_preview_generation_options_desc": "Override Preview Generation Options for this operation. Defaults are set in System -> Preview Generation.", "overwrite": "Overwrite existing files", - "phash": "Perceptual hashes", + "phash": "Video perceptual hashes", "phash_tooltip": "For deduplication and scene identification", "preview_exclude_end_time_desc": "Exclude the last x seconds from scene previews. This can be a value in seconds, or a percentage (eg 2%) of the total scene duration.", "preview_exclude_end_time_head": "Exclude end time", @@ -988,9 +1038,12 @@ "video_previews_tooltip": "Video previews which play when hovering over a scene" }, "scenes_found": "{count} scenes found", + "studios_found": "{count} studios found", + "tags_found": "{count} tags found", "scrape_entity_query": "{entity_type} Scrape Query", "scrape_entity_title": "{entity_type} Scrape Results", "scrape_results_existing": "Existing", + "scrape_results_missing": "Missing", "scrape_results_scraped": "Scraped", "set_default_filter_confirm": "Are you sure you want to set this filter as the default?", "set_image_url_title": "Image URL", @@ -1124,8 +1177,8 @@ "syncing": "Syncing with server", "uploading": "Uploading script" }, - "hasChapters": "Has Chapters", - "hasMarkers": "Has Markers", + "hasChapters": "Chapters", + "hasMarkers": "Markers", "height": "Height", "height_cm": "Height (cm)", "help": "Help", @@ -1148,7 +1201,9 @@ "interactive_speed": "Interactive Speed", "isMissing": "Is Missing", "last_o_at": "Last O At", + "last_o_at_sfw": "Last Like At", "last_played_at": "Last Played At", + "latest_scene": "Latest Scene", "library": "Library", "loading": { "generic": "Loading…", @@ -1188,9 +1243,11 @@ "new": "New", "none": "None", "o_count": "O Count", - "o_counter": "O-Counter", + "o_count_sfw": "Likes", "o_history": "O History", + "o_history_sfw": "Like History", "odate_recorded_no": "No O Date Recorded", + "odate_recorded_no_sfw": "No Like Date Recorded", "operations": "Operations", "organized": "Organised", "orientation": "Orientation", @@ -1268,7 +1325,7 @@ "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_names_or_stashids_separated_by_comma": "Performer names or StashIDs separated by comma", "performer_selection": "Performer selection", "performer_successfully_tagged": "Performer successfully tagged:", "query_all_performers_in_the_database": "All performers in the database", @@ -1306,6 +1363,7 @@ "sceneTagger": "Scene Tagger", "scene_code": "Studio Code", "scene_count": "Scene Count", + "scenes_duration": "Scene Duration", "scene_created_at": "Scene Created At", "scene_date": "Date of Scene", "scene_id": "Scene ID", @@ -1317,6 +1375,7 @@ "edit_filter": "Edit Filter", "name": "Filter", "saved_filters": "Saved filters", + "search_term": "Search term", "update_filter": "Update Filter", "more_filter_criteria": "+{count} more" }, @@ -1365,25 +1424,28 @@ }, "paths": { "database_filename_empty_for_default": "database filename (empty for default)", - "description": "Next up, we need to determine where to find your porn collection, and where to store the Stash database, generated files and cache files. These settings can be changed later if needed.", + "description": "Next up, we need to determine where to find your content, and where to store the Stash database, generated files and cache files. These settings can be changed later if needed.", "path_to_blobs_directory_empty_for_default": "path to blobs directory (empty for default)", "path_to_cache_directory_empty_for_default": "path to cache directory (empty for default)", "path_to_generated_directory_empty_for_default": "path to generated directory (empty for default)", "set_up_your_paths": "Set up your paths", + "sfw_content_settings": "Using stash for SFW content?", + "sfw_content_settings_description": "stash can be used to manage SFW content such as photography, art, comics, and more. Enabling this option will adjust some UI behaviour to be more appropriate for SFW content.", "stash_alert": "No library paths have been selected. No media will be able to be scanned into Stash. Are you sure?", "store_blobs_in_database": "Store blobs in database", + "use_sfw_content_mode": "Use SFW content mode", "where_can_stash_store_blobs": "Where can Stash store database binary data?", "where_can_stash_store_blobs_description": "Stash can store binary data such as scene covers, performer, studio and tag images either in the database or in the filesystem. By default, it will store this data in the filesystem in the subdirectory blobs within the directory containing your config file. If you want to change this, please enter an absolute or relative (to the current working directory) path. Stash will create this directory if it does not already exist.", "where_can_stash_store_blobs_description_addendum": "Alternatively, you can store this data in the database. Note: This will increase the size of your database file, and will increase database migration times.", "where_can_stash_store_cache_files": "Where can Stash store cache files?", "where_can_stash_store_cache_files_description": "In order for some functionality like HLS/DASH live transcoding to function, Stash requires a cache directory for temporary files. By default, Stash will create a cache directory within the directory containing your config file. If you want to change this, please enter an absolute or relative (to the current working directory) path. Stash will create this directory if it does not already exist.", "where_can_stash_store_its_database": "Where can Stash store its database?", - "where_can_stash_store_its_database_description": "Stash uses an SQLite database to store your porn metadata. By default, this will be created as stash-go.sqlite in the directory containing your config file. If you want to change this, please enter an absolute or relative (to the current working directory) filename.", + "where_can_stash_store_its_database_description": "Stash uses an SQLite database to store your content metadata. By default, this will be created as stash-go.sqlite in the directory containing your config file. If you want to change this, please enter an absolute or relative (to the current working directory) filename.", "where_can_stash_store_its_database_warning": "WARNING: storing the database on a different system to where Stash is run from (e.g. storing the database on a NAS while running the Stash server on another computer) is unsupported! SQLite is not intended for use across a network, and attempting to do so can very easily cause your entire database to become corrupted.", "where_can_stash_store_its_generated_content": "Where can Stash store its generated content?", "where_can_stash_store_its_generated_content_description": "In order to provide thumbnails, previews and sprites, Stash generates images and videos. This also includes transcodes for unsupported file formats. By default, Stash will create a generated directory within the directory containing your config file. If you want to change where this generated media will be stored, please enter an absolute or relative (to the current working directory) path. Stash will create this directory if it does not already exist.", - "where_is_your_porn_located": "Where is your porn located?", - "where_is_your_porn_located_description": "Add directories containing your porn videos and images. Stash will use these directories to find videos and images during scanning." + "where_is_your_porn_located": "Where is your content located?", + "where_is_your_porn_located_description": "Add directories containing your videos and images. Stash will use these directories to find videos and images during scanning." }, "stash_setup_wizard": "Stash Setup Wizard", "success": { @@ -1419,8 +1481,15 @@ "welcome_to_stash": "Welcome to Stash" }, "stash_id": "Stash ID", - "stash_id_endpoint": "Stash ID Endpoint", + "stash_id_count": "Stash ID Count", + "stash_id_endpoint": "Stash ID Endpoint URL", "stash_ids": "Stash IDs", + "stashbox_search": { + "header": "Search {entityType} from StashBox", + "no_results": "No results found.", + "placeholder_name_or_id": "{entityType} name or StashID...", + "select_stashbox": "Select StashBox..." + }, "stashbox": { "go_review_draft": "Go to {endpoint_name} to review draft.", "selected_stash_box": "Selected Stash-Box endpoint", @@ -1436,6 +1505,7 @@ "scenes_played": "Scenes Played", "scenes_size": "Scenes size", "total_o_count": "Total O-Count", + "total_o_count_sfw": "Total Likes", "total_play_count": "Total Play Count", "total_play_duration": "Total Play Duration" }, @@ -1472,7 +1542,7 @@ "status_tagging_job_queued": "Status: Tagging job queued", "status_tagging_studios": "Status: Tagging studios", "studio_already_tagged": "Studio already tagged", - "studio_names_separated_by_comma": "Studio names separated by comma", + "studio_names_or_stashids_separated_by_comma": "Studio names or StashIDs separated by comma", "studio_selection": "Studio selection", "studio_successfully_tagged": "Studio successfully tagged", "tag_status": "Tag Status", @@ -1512,6 +1582,7 @@ "delete_past_tense": "Deleted {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "generating_screenshot": "Generating screenshot…", "image_index_too_large": "Error: Image index is larger than the number of images in the Gallery", + "merged_performers": "Merged performers", "merged_scenes": "Merged scenes", "merged_tags": "Merged tags", "reassign_past_tense": "File reassigned", @@ -1533,7 +1604,7 @@ "urls": "URLs", "validation": { "blank": "${path} must not be blank", - "date_invalid_form": "${path} must be in YYYY-MM-DD form", + "date_invalid_form": "${path} must be in YYYY, YYYY-MM, or YYYY-MM-DD form", "end_time_before_start_time": "End time must be greater than or equal to start time", "required": "${path} is a required field", "unique": "${path} must be unique" diff --git a/ui/v2.5/src/locales/en-US.json b/ui/v2.5/src/locales/en-US.json index 1f4f31fc5..7d730601c 100644 --- a/ui/v2.5/src/locales/en-US.json +++ b/ui/v2.5/src/locales/en-US.json @@ -1,13 +1,9 @@ { "actions": { + "anonymise": "Anonymize", + "download_anonymised": "Download anonymized", "customise": "Customize", - "add_sub_groups": "Add Sub-Groups", - "add": "Add", - "add_directory": "Add Directory", - "add_entity": "Add {entityType}", - "add_manual_date": "Add manual date", - "add_o": "Add O", - "add_play": "Add play" + "optimise_database": "Optimize Database" }, "config": { "tools": { @@ -26,5 +22,10 @@ "favourite": "Favorite", "hair_color": "Hair Color", "organized": "Organized", - "performer_favorite": "Performer Favorited" + "performer_favorite": "Performer Favorited", + "component_tagger": { + "config": { + "mark_organized_label": "Mark as Organized on save" + } + } } diff --git a/ui/v2.5/src/locales/es-ES.json b/ui/v2.5/src/locales/es-ES.json index d9585f46e..42df6f91c 100644 --- a/ui/v2.5/src/locales/es-ES.json +++ b/ui/v2.5/src/locales/es-ES.json @@ -138,7 +138,21 @@ "view_history": "Ver historial", "add_sub_groups": "Añadir subgrupos", "remove_from_containing_group": "Eliminar del grupo", - "reset_play_duration": "Reiniciar la duración de la reproducción" + "reset_play_duration": "Reiniciar la duración de la reproducción", + "load": "Cargar", + "load_filter": "Cargar el filtro", + "play": "Reproducir", + "reset_resume_time": "Restablecer el tiempo de reanudación", + "reset_cover": "Restaurar portada por defecto", + "set_cover": "Establecer como portada", + "show_results": "Mostrar resultados", + "show_count_results": "Mostrar {count} resultados", + "sidebar": { + "close": "Cerrar barra lateral", + "open": "Abrir barra lateral", + "toggle": "Alternar barra lateral" + }, + "add_stash_id": "Añadir ID de Stash" }, "actions_name": "Acciones", "age": "Edad", @@ -178,11 +192,16 @@ "set_cover_label": "Seleccionar carátula de la escena", "set_tag_desc": "Adjuntar etiquetas a la escena, ya sea sobreescribiéndolas (elimina las que existieran previamente) o fusionando las nuevas con las que ya existían previamente.", "set_tag_label": "Seleccionar etiquetas", - "show_male_desc": "Marcar esta opción hará que los actores masculinos estén disponibles para su etiquetado.", - "show_male_label": "Mostrar actores", "source": "Fuente", "mark_organized_desc": "Marcar la escena como organizada tras pulsar el botón de Guardar.", - "mark_organized_label": "Marcar como organizado al guardar" + "mark_organized_label": "Marcar como organizado al guardar", + "errors": { + "blacklist_duplicate": "Elemento duplicado en la lista negra" + }, + "performer_genders": { + "heading": "Géneros de los actores", + "description": "Los actores con estos géneros se mostrarán al etiquetar escenas." + } }, "noun_query": "Consulta", "results": { @@ -282,7 +301,9 @@ "password_desc": "Contraseña para acceder a Stash. Dejar en blanco para deshabilitar la exigencia de identificación para acceder a la aplicación", "stash-box_integration": "Integración Stash-box", "username": "Usuario", - "username_desc": "Usuario para acceder a Stash. Dejar en blanco para deshabilitar la exigencia de identificación para acceder a la aplicación" + "username_desc": "Usuario para acceder a Stash. Dejar en blanco para deshabilitar la exigencia de identificación para acceder a la aplicación", + "log_file_max_size": "Tamaño máximo del registro", + "log_file_max_size_desc": "Tamaño máximo en megabytes del archivo de registro antes de comprimirlo. 0 MB está desactivado. Requiere reiniciar." }, "backup_directory_path": { "description": "Ubicación del directorio para copias de seguridad de archivos de bases de datos SQLite", @@ -398,6 +419,10 @@ "plugins_path": { "description": "Ubicación del directorio de los archivos de configuración de complementos", "heading": "Ruta de complementos" + }, + "delete_trash_path": { + "description": "Ruta a la que se moverán los archivos eliminados en lugar de eliminarlos permanentemente. Déjela vacía para eliminar los archivos de forma permanente.", + "heading": "Ruta de la papelera" } }, "library": { @@ -434,7 +459,9 @@ "endpoint": "Terminal de red", "graphql_endpoint": "Terminal de red GraphQL", "name": "Nombre", - "title": "Terminal de red Stash-box" + "title": "Terminal de red Stash-box", + "max_requests_per_minute": "Máximas peticiones por minuto", + "max_requests_per_minute_description": "Utiliza el valor predeterminado {defaultValue} si se establece en 0" }, "system": { "transcoding": "Transcodificación" @@ -486,7 +513,7 @@ "heading": "Identificar", "identifying_from_paths": "Identificación de las escenas que se encuentren en las siguientes rutas", "identifying_scenes": "Identificando {num} {scene}", - "include_male_performers": "Incluir actores (varones)", + "include_male_performers": "Incluir actores varones", "set_cover_images": "Selección automática de carátula de escena", "set_organized": "Marcar escena como \"clasificada\"", "source": "Fuente", @@ -560,7 +587,9 @@ "whitespace_chars": "Espacios en blanco", "whitespace_chars_desc": "Estos caracteres se reemplazarán en el título por espacios en blanco" }, - "scene_tools": "Herramientas de escenas" + "scene_tools": "Herramientas de escenas", + "graphql_playground": "Entorno de pruebas de GraphQL", + "heading": "Herramientas" }, "ui": { "abbreviate_counters": { @@ -725,7 +754,8 @@ "heading": "Etiqueta de VR" }, "enable_chromecast": "Habilitar Chromecast", - "show_ab_loop_controls": "Mostrar controles del plugin de bucle AB" + "show_ab_loop_controls": "Mostrar controles del plugin de bucle AB", + "show_range_markers": "Mostrar marcadores de rango" } }, "scene_wall": { @@ -784,6 +814,18 @@ "heading": "Mostrar contenido de subetiquetas" } } + }, + "sfw_mode": { + "heading": "Modo de contenido SFW", + "description": "Actívelo si utiliza Stash para almacenar contenido SFW. Oculta o modifica algunos aspectos de la interfaz de usuario relacionados con contenido para adultos." + }, + "performer_list": { + "heading": "Lista de actores", + "options": { + "show_links_on_grid_card": { + "heading": "Mostrar enlaces en las tarjetas de la cuadrícula de actores" + } + } } }, "advanced_mode": "Modo avanzado" @@ -868,13 +910,13 @@ "pan_y": "Panorámica eje Y", "zoom": "Zoom" }, - "page_header": "Página {page} / {total}" + "page_header": "Página {page} / {total}", + "disable_animation": "Desactivar la animación de transición entre imágenes" }, "merge_tags": { "destination": "Destino", "source": "Fuente" }, - "overwrite_filter_confirm": "¿Estás seguro de sobreescribir la consulta guardada {entityName}?", "scene_gen": { "force_transcodes": "Forzar generación de transcodificación", "force_transcodes_tooltip": "Por defecto las transcodificaciones son solo generadas cuando el archivo de vídeo no es soportado por el navegador. Cuando están habilitadas, las transcodificaciones se generarán incluso cuando el fichero de vídeo sea soportado por el navegador.", @@ -884,7 +926,7 @@ "marker_image_previews": "Vistas previas de marcadores en formato de imagen animada", "marker_image_previews_tooltip": "Generar también vistas previas animadas (webp), solo requeridas cuando el tipo de vista previa de la pared de escenas/marcadores está configurado como Imagen Animada. Al navegar, consumen menos CPU que las vistas previas de video, pero se generan además de ellas y son archivos más grandes.", "marker_screenshots": "Capturas de pantalla de marcadores", - "marker_screenshots_tooltip": "Imágenes estáticas JPG de marcadores, solo requeridas si el tipo de vista previa seleccionada por defecto es \"Imagen estática\".", + "marker_screenshots_tooltip": "Imágenes estáticas JPG de marcadores", "markers": "Vistas previas de marcadores", "markers_tooltip": "Vídeos de 20 segundos de duración que comienzan a partir del tiempo seleccionado.", "override_preview_generation_options": "Sobrescribir opciones para la generación de vistas previas", @@ -943,7 +985,14 @@ "destination": "Reasignar a" }, "delete_entity_simple_desc": "{count, plural, one {¿Estás seguro de que quieres eliminar esta {singularEntity}?} other {¿Estás seguro de que quieres eliminar estas {pluralEntity}?}}", - "reassign_entity_title": "{count, plural, one {Reasignar {singularEntity}} other {Reasignar {pluralEntity}}}" + "reassign_entity_title": "{count, plural, one {Reasignar {singularEntity}} other {Reasignar {pluralEntity}}}", + "clear_o_history_confirm_sfw": "¿Estás seguro de que quieres borrar el historial de «Me gusta»?", + "overwrite_filter_warning": "El filtro guardado \"{entityName}\" se sobrescribirá.", + "set_default_filter_confirm": "¿Estás seguro de que deseas establecer este filtro como predeterminado?", + "delete_alert_to_trash": "Los siguientes {count, plural, one {{singularEntity}} other {{pluralEntity}}} se moverán a la papelera:", + "stashid_exists_warning": "Se sustituirá el identificador de stash existente para este stash-box.", + "studios_found": "{count} estudios encontrados", + "tags_found": "{count} etiquetas encontradas" }, "dimensions": "Dimensiones", "director": "Director", @@ -952,7 +1001,8 @@ "list": "Lista", "tagger": "Etiquetadora", "unknown": "Desconocido/a", - "wall": "Muro" + "wall": "Muro", + "label_current": "Modo de visualización: {current}" }, "donate": "Donar", "dupe_check": { @@ -1040,7 +1090,7 @@ "uploading": "Subiendo script", "error": "Error al conectar con Handy" }, - "hasMarkers": "Tiene marcadores", + "hasMarkers": "Marcadores", "height": "Estatura", "help": "Ayuda", "ignore_auto_tag": "Ignorar Etiquetado Automático", @@ -1070,21 +1120,20 @@ "interactive_speed": "Velocidad interactiva", "performer_card": { "age": "{age} {years_old}", - "age_context": "{age} {years_old} en esta escena" + "age_context": "{age} {years_old} durante la producción" }, "phash": "Función de hash perceptual", "stream": "Transmisión", "video_codec": "Códec de vídeo", "play_count": "Contador de reproducciones", "play_duration": "Tiempo de reproducción", - "o_count": "Contador de pajas" + "o_count": "Contador de orgasmos" }, "megabits_per_second": "{value} megabits por segundo (mbps)", "metadata": "Metadatos", "name": "Nombre", "new": "Añadir", "none": "Ninguno/a", - "o_counter": "Contador “P”", "operations": "Acciones", "organized": "Clasificadas", "pagination": { @@ -1126,7 +1175,6 @@ "no_results_found": "No se han encontrado resultados.", "number_of_performers_will_be_processed": "{performer_count} actrices/actores serán procesados", "performer_already_tagged": "Actriz/actor ya etiquetada/o", - "performer_names_separated_by_comma": "Nombres de actrices/actores separados por comas", "performer_selection": "Selección de actriz/actor", "performer_successfully_tagged": "Actriz/actor etiquetada/o correctamente:", "query_all_performers_in_the_database": "Todas las actrices/actores en la base de datos", @@ -1139,7 +1187,8 @@ "untagged_performers": "Actrices/actores no etiquetadas/os", "update_performer": "Actualizar actriz/actor", "update_performers": "Actualizar actrices/actores", - "updating_untagged_performers_description": "Actualizar las actrices/actores no etiquetados intentará seleccionar cualquier actriz/actor que carecen de un StashID y actualizará los metadatos." + "updating_untagged_performers_description": "Actualizar las actrices/actores no etiquetados intentará seleccionar cualquier actriz/actor que carecen de un StashID y actualizará los metadatos.", + "performer_names_or_stashids_separated_by_comma": "Nombres de actores o StashIDs separados por comas" }, "performer_tags": "Etiquetas de actriz/actor", "performers": "Actrices/Actores", @@ -1159,7 +1208,9 @@ "name": "Filtro", "saved_filters": "Filtros guardados", "update_filter": "Actualizar filtro", - "edit_filter": "Editar filtro" + "edit_filter": "Editar filtro", + "search_term": "Término de búsqueda", + "more_filter_criteria": "+{count} más" }, "seconds": "Segundos", "settings": "Preferencias", @@ -1181,7 +1232,9 @@ "errors": { "something_went_wrong": "¡OH NO! ¡Algo ha ido mal!", "something_went_wrong_description": "Si sospechas que puede haber un error con los datos aportados, por favor, haz clic en volver para arreglarlos. De lo contrario, abre una incidencia en {githubLink} o busca ayuda en {discordLink}.", - "something_went_wrong_while_setting_up_your_system": "Algo ha salido mal mientras configurábamos tu entorno. Éste es el mensaje de error recibido: {error}" + "something_went_wrong_while_setting_up_your_system": "Algo ha salido mal mientras configurábamos tu entorno. Éste es el mensaje de error recibido: {error}", + "unable_to_retrieve_system_status": "No se puede recuperar el estado del sistema: {error}", + "unexpected_error": "Se ha producido un error inesperado: {error}" }, "folder": { "file_path": "Ruta relativa del fichero", @@ -1203,7 +1256,7 @@ }, "paths": { "database_filename_empty_for_default": "nombre para el archivo de la base de datos (en blanco para usar opción por defecto)", - "description": "A continuación necesitamos saber dónde se encuentra tu colección de porno, y dónde guardar la base de datos de Stash y los ficheros multimedia de soporte generados. Estos ajustes se pueden modificar posteriormente.", + "description": "A continuación necesitamos saber dónde se encuentra tu contenido, y dónde guardar la base de datos de Stash y los ficheros multimedia de soporte generados. Estos ajustes se pueden modificar posteriormente.", "path_to_generated_directory_empty_for_default": "ruta al directorio de ficheros multimedia generados (dejar en blanco para usar opción por defecto)", "set_up_your_paths": "Selecciona tus rutas", "stash_alert": "No se han seleccionado rutas para tu biblioteca. Ningún fichero multimedia podrá ser seleccionado para su inclusión en Stash. ¿Estás seguro?", @@ -1211,8 +1264,8 @@ "where_can_stash_store_its_database_description": "Stash emplea una base de datos SQLite para almacenar los metadatos de tu colección. Por defecto será creada como stash-go.sqlite en el directorio en el que se encuentra tu archivo de configuración. Si quieres cambiar esto, por favor, introduce un nombre de archivo con ruta absoluta o relativa al directorio de trabajo actual.", "where_can_stash_store_its_generated_content": "¿Dónde guarda Stash los ficheros multimedia de soporte generados?", "where_can_stash_store_its_generated_content_description": "Para poder ofrecerte miniaturas, vistas previas y conjuntos de imágenes animadas, Stash genera imágenes y vídeos. Esto incluye también transcodificaciones para formatos de vídeo no soportados. Por defecto, Stash creará el directorio generated en el directorio que contiene tu archivo de configuración. Si quieres cambiar dónde se almacenarán estos archivos generados, por favor, introduce una ruta absoluta o relativa (al directorio de trabajo actual). Stash creará este directorio si no existe.", - "where_is_your_porn_located": "¿Dónde guardas el porno?", - "where_is_your_porn_located_description": "Añade los directorios que contienen tus imágenes y vídeos porno. Stash usará estos directorios para buscar imágenes y vídeos durante el escaneo.", + "where_is_your_porn_located": "¿Dónde guardas tu contenido?", + "where_is_your_porn_located_description": "Añade los directorios que contienen tus imágenes y vídeos. Stash usará estos directorios para buscar imágenes y vídeos durante el escaneo.", "path_to_cache_directory_empty_for_default": "ruta al directorio de la caché (dejar en blanco para usar el directorio por defecto)", "store_blobs_in_database": "Almacenar blobs en la base de datos", "path_to_blobs_directory_empty_for_default": "ruta al directorio con los blobs (dejar en blanco para el valor por defecto)", @@ -1221,7 +1274,10 @@ "where_can_stash_store_its_database_warning": "ADVERTENCIA: ¡almacenar la base de datos en un sistema diferente al que Stash se ejecuta (por ejemplo, almacenar la base de datos en un NAS mientras se ejecuta el servidor Stash en otro equipo) no es compatible! SQLite no está diseñado para su uso a través de una red e intentar hacerlo puede corromper fácilmente toda tu base de datos.", "where_can_stash_store_blobs": "¿Dónde puede Stash guardar los datos binarios de la base de datos?", "where_can_stash_store_blobs_description": "Stash puede almacenar datos binarios como portadas de escenas, imágenes de intérpretes, estudios y etiquetas ya sea en la base de datos o en el sistema de archivos. Por defecto, almacenará estos datos en el sistema de archivos en el subdirectorio blobs dentro del directorio que contiene tu archivo de configuración. Si deseas cambiar esto, por favor ingresa una ruta absoluta o relativa (respecto al directorio de trabajo actual). Stash creará este directorio si aún no existe.", - "where_can_stash_store_cache_files_description": "Para que algunas funciones como la transcodificación en vivo de HLS/DASH funcionen, Stash requiere un directorio de caché para archivos temporales. Por defecto, Stash creará un directorio cache dentro del directorio que contiene tu archivo de configuración. Si deseas cambiar esto, por favor ingresa una ruta absoluta o relativa (respecto al directorio de trabajo actual). Stash creará este directorio si aún no existe." + "where_can_stash_store_cache_files_description": "Para que algunas funciones como la transcodificación en vivo de HLS/DASH funcionen, Stash requiere un directorio de caché para archivos temporales. Por defecto, Stash creará un directorio cache dentro del directorio que contiene tu archivo de configuración. Si deseas cambiar esto, por favor ingresa una ruta absoluta o relativa (respecto al directorio de trabajo actual). Stash creará este directorio si aún no existe.", + "sfw_content_settings": "¿Usar Stash para contenido SFW?", + "sfw_content_settings_description": "Stash se puede utilizar para gestionar contenido SFW, como fotografía, arte, cómics y mucho más. Al habilitar esta opción, se ajustará el comportamiento de la interfaz de usuario para que sea más adecuado para el contenido SFW.", + "use_sfw_content_mode": "Utilizar el modo de contenido SFW" }, "stash_setup_wizard": "Asistente de configuración de Stash", "success": { @@ -1271,7 +1327,7 @@ "scenes_duration": "Duración de las escenas", "scenes_size": "Tamaño de las escenas", "total_play_count": "Contador total de reproducciones", - "total_o_count": "Total de pajas", + "total_o_count": "Total de orgasmos", "total_play_duration": "Tiempo total de reproducciones", "scenes_played": "Escenas reproducidas" }, @@ -1342,7 +1398,7 @@ } }, "file_count": "Conteo de archivos", - "hasChapters": "Tiene capítulos", + "hasChapters": "Capítulos", "index_of_total": "{index} de {total}", "last_played_at": "Última reproducción el", "package_manager": { @@ -1382,8 +1438,8 @@ "date_format": "YYYY-MM-DD", "datetime_format": "YYYY-MM-DD HH:MM", "disambiguation": "Disambiguación", - "o_count": "Contador de pajas", - "o_history": "Historial de pajas", + "o_count": "Contador de orgasmos", + "o_history": "Historial de orgasmos", "orientation": "Orientación", "penis_length_cm": "Longitud del pene (cm)", "play_count": "Contador de reproducciones", @@ -1414,7 +1470,6 @@ "status_tagging_studios": "Estado: etiquetando estudios", "studio_already_tagged": "Estudio ya etiquetado", "untagged_studios": "Estudios no etiquetados", - "studio_names_separated_by_comma": "Nombres de estudios separados por coma", "studio_successfully_tagged": "Estudio etiquetado correctamente", "tag_status": "Estado de etiquetado", "to_use_the_studio_tagger": "Para usar el etiquetador de estudios una instancia de stash-box debe ser configurada.", @@ -1424,7 +1479,8 @@ "refresh_tagged_studios": "Recargar estudios etiquetados", "studio_selection": "Selección de estudio", "batch_update_studios": "Actualizar estudios en lote", - "network_error": "Error de red" + "network_error": "Error de red", + "studio_names_or_stashids_separated_by_comma": "Nombres de estudio o StashID separados por comas" }, "weight_kg": "Peso (kg)", "penis_length": "Longitud del pene", @@ -1434,7 +1490,7 @@ "saved_filter": "Filtro guardado" } }, - "last_o_at": "Última paja a las", + "last_o_at": "Último orgasmo a las", "parent_studio": "Estudio principal", "primary_file": "Archivo principal", "recently_added_objects": "{objects} añadidas recientemente", @@ -1463,16 +1519,17 @@ "second": "Segundo", "validation": { "blank": "${path} no puede estar vacío", - "date_invalid_form": "${path} tiene que tener el formato YYYY-MM-DD", + "date_invalid_form": "${path} debe estar en formato AAAA, AAAA-MM o AAAA-MM-DD.", "required": "${path} es un campo requerido", - "unique": "${path} tiene que ser único" + "unique": "${path} tiene que ser único", + "end_time_before_start_time": "El tiempo de finalización debe ser mayor o igual que el tiempo de inicio" }, "subsidiary_studio_count": "Número de estudios secundarios", "tag_parent_tooltip": "Tiene etiquetas primarias", "tag_sub_tag_tooltip": "Tiene sub-etiquetas", "time": "Hora", "type": "Tipo", - "odate_recorded_no": "No hay registro de pajas grabado", + "odate_recorded_no": "No hay registro de orgasmos grabado", "urls": "URLs", "zip_file_count": "Números de archivos zip", "unknown_date": "Fecha desconocida", @@ -1487,7 +1544,9 @@ "custom_fields": { "field": "Campo", "value": "Valor", - "title": "Campos personalizados" + "title": "Campos personalizados", + "criteria_format_string": "{criterion} (custom field) {modifierString} {valueString}", + "criteria_format_string_others": "{criterion} (custom field) {modifierString} {valueString} (+{others} others)" }, "sub_group_count": "Recuento de subgrupos", "sub_group_of": "Subgrupo de {parent}", @@ -1497,9 +1556,36 @@ "any": "Cualquiera", "any_of": "Cualquiera de", "none": "Ninguno", - "only": "Solamente" + "only": "Solo" }, "include_sub_group_content": "Incluir contenido de subgrupos", "include_sub_groups": "Incluir subgrupos", - "eta": "Tiempo estimado" + "eta": "Tiempo estimado", + "containing_group": "Grupo contenedor", + "containing_group_count": "Contador del grupo contenedor", + "containing_groups": "Grupo de contenedores", + "login": { + "username": "Nombre de usuario", + "password": "Contraseña", + "invalid_credentials": "Nombre de usuario o contraseña incorrecto", + "login": "Iniciar sesión", + "internal_error": "Error interno inesperado. Consulte los registros para obtener más detalles" + }, + "age_on_date": "{age} durante la producción", + "include_sub_studio_content": "Incluir contenido de subestudios", + "include_sub_tag_content": "Incluir contenido de subetiquetas", + "last_o_at_sfw": "Último «Me gusta» en", + "sort_name": "Ordenar por nombre", + "o_count_sfw": "Me gusta", + "o_history_sfw": "Historial de Me gusta", + "odate_recorded_no_sfw": "Sin fecha de Me gusta registrada", + "scenes_duration": "Duración de la escena", + "sub_group_order": "Orden de subgrupo", + "time_end": "Hora de finalización", + "stashbox_search": { + "header": "Buscar {entityType} en StashBox", + "no_results": "No se han encontrado resultados.", + "placeholder_name_or_id": "Nombre de {entityType} o StashID...", + "select_stashbox": "Seleccionar StashBox..." + } } diff --git a/ui/v2.5/src/locales/et-EE.json b/ui/v2.5/src/locales/et-EE.json index 0a6173691..a250674a2 100644 --- a/ui/v2.5/src/locales/et-EE.json +++ b/ui/v2.5/src/locales/et-EE.json @@ -141,7 +141,19 @@ "add_play": "Lisa mängimine", "clear_date_data": "Eemalda kuupäeva andmed", "view_history": "Vaata ajalugu", - "remove_from_containing_group": "Eemalda Grupist" + "remove_from_containing_group": "Eemalda Grupist", + "sidebar": { + "open": "Ava külgriba", + "close": "Sulge külgriba", + "toggle": "Lülita külgriba sisse/välja" + }, + "play": "Esita", + "show_results": "Näita tulemusi", + "show_count_results": "Näita {count} tulemust", + "load": "Lae", + "load_filter": "Lae filter", + "add_stash_id": "Lisa Stash ID", + "create_new": "Loo uus" }, "actions_name": "Tegevused", "age": "Vanus", @@ -182,13 +194,15 @@ "set_cover_label": "Määra stseeni kaanepilt", "set_tag_desc": "Ühenda stseenile külge silte, kas olemasolevate siltide ülekirjutamise või liitmise kaudu.", "set_tag_label": "Määra sildid", - "show_male_desc": "Vali, kas meesnäitlejad on määramiseks saadaval.", - "show_male_label": "Näita meesnäitlejaid", "source": "Allikas", "mark_organized_desc": "Märgi stseen koheselt Orgainiseerituks peale Salvesta nupu vajutamist.", "mark_organized_label": "Märgi salvestamisel Organiseerituks", "errors": { "blacklist_duplicate": "Dubleeritud musta nimekirja ese" + }, + "performer_genders": { + "heading": "Näitlejate sood", + "description": "Stseene märgistades näidatakse nende sugudega näitlejaid." } }, "noun_query": "Päring", @@ -210,7 +224,10 @@ "verb_scrape_all": "Kraabi Kõikjalt", "verb_submit_fp": "Esita {fpCount, plural, one{# Sõrmejälg} other{# Sõrmejälge}}", "verb_toggle_config": "{toggle} {configuration}", - "verb_toggle_unmatched": "{toggle} kokkusobitamata stseenid" + "verb_toggle_unmatched": "{toggle} kokkusobitamata stseenid", + "verb_add_as_alias": "Lisa nimi alisena", + "verb_link_existing": "Ühenda eksisteerivaga", + "verb_match_tag": "Sobita Silt" }, "config": { "about": { @@ -289,7 +306,9 @@ "password_desc": "Parool Stashi pääsemiseks. Jäta tühjaks, kui soovid sisselogimise keelata", "stash-box_integration": "Stash-kasti integratsioon", "username": "Kasutajanimi", - "username_desc": "Kasutajanimi Stashi pääsemiseks. Jäta tühjaks, kui soovid sisselogimise keelata" + "username_desc": "Kasutajanimi Stashi pääsemiseks. Jäta tühjaks, kui soovid sisselogimise keelata", + "log_file_max_size": "Maksimaalne logi suurus", + "log_file_max_size_desc": "Maksimaalne logifaili suurus megabaitides enne tihendamist. 0MB on väljalülitatud. Nõuab taaskäivitust." }, "backup_directory_path": { "description": "Failitee SQLite andmebaasi varundusfailide jaoks", @@ -405,6 +424,10 @@ "plugins_path": { "description": "Plugina konfiguratsioonifailide asukoht kataloogis", "heading": "Pluginate Tee" + }, + "delete_trash_path": { + "description": "Tee, kuhu liigutatakse kustutatud failide lõpliku kustutamise asemel. Jäta tühjaks, et alati faile lõplikult kustutada.", + "heading": "Prügi Tee" } }, "library": { @@ -441,7 +464,9 @@ "endpoint": "Lõpp-punkt", "graphql_endpoint": "GraphQL lõpp-punkt", "name": "Nimi", - "title": "Stash-kasti Lõpp-punktid" + "title": "Stash-kasti Lõpp-punktid", + "max_requests_per_minute": "Maksimaalne arv päringuid minutis", + "max_requests_per_minute_description": "Kasutab vaikimisi väärtust {defaultValue}, kui on 0" }, "system": { "transcoding": "Ümbertöötlemine" @@ -567,7 +592,9 @@ "whitespace_chars": "Tühikumärgid", "whitespace_chars_desc": "Need märgid asendatakse pealkirjas tühikutega" }, - "scene_tools": "Stseeni Tööriistad" + "scene_tools": "Stseeni Tööriistad", + "graphql_playground": "GraphQL mänguplats", + "heading": "Tööriistad" }, "ui": { "abbreviate_counters": { @@ -732,7 +759,8 @@ "vr_tag": { "description": "VR-nupp kuvatakse ainult selle sildiga stseenide puhul.", "heading": "VR Silt" - } + }, + "show_range_markers": "Näita Vahemiku Märke" } }, "scene_wall": { @@ -791,6 +819,18 @@ "heading": "Kompaktselt laiendatud detailid" }, "heading": "Detailide Leht" + }, + "performer_list": { + "heading": "Näitlejate nimekiri", + "options": { + "show_links_on_grid_card": { + "heading": "Kuva linke näitlejate ruudustiku kaartidel" + } + } + }, + "sfw_mode": { + "description": "Luba, kui kasutad stashi SFW sisu jaoks. Peidab või muudab kasutajaliidese mõningaid täiskasvanutele mõeldud sisuga seotud aspekte.", + "heading": "SFW Sisu Režiim" } }, "advanced_mode": "Täpsem Režiim" @@ -889,7 +929,8 @@ "label": "Kerimisrežiim", "pan_y": "Liiguta Y", "zoom": "Suum" - } + }, + "disable_animation": "Lülita piltide vahetamise animatsioon välja" }, "merge": { "destination": "Siihtkoht", @@ -900,7 +941,6 @@ "destination": "Sihtkoht", "source": "Allikas" }, - "overwrite_filter_confirm": "Oled kindel, et tahad üle kirjutada juba eksisteerivat päringut {entityName}?", "reassign_entity_title": "{count, plural, one {Määra Ümber {singularEntity}} other {Määra Ümber {pluralEntity}-d/id}}", "reassign_files": { "destination": "Määra Ümber" @@ -953,7 +993,15 @@ "unsaved_changes": "Salvestamata muudatused. Kas soovid kindlasti lahkuda?", "performers_found": "Leiti {count} esinejat", "clear_o_history_confirm": "Kas oled kindel, et soovid puhastada O ajaloo?", - "clear_play_history_confirm": "Kas oled kindel, et soovid puhastada vaatamise ajaloo?" + "clear_play_history_confirm": "Kas oled kindel, et soovid puhastada vaatamise ajaloo?", + "set_default_filter_confirm": "Kas oled kindel, et soovid määrata seda filtrit vaikimisi valikuks?", + "overwrite_filter_warning": "Salvestatud filter \"{entityName}\" kirjutatakse üle.", + "clear_o_history_confirm_sfw": "Kas oled kindel, et tahad meeldimiste ajalugu tühjendada?", + "delete_alert_to_trash": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} liigutatakse prügilasse:", + "stashid_exists_warning": "Olemasolev stash id asendatakse selle stash-kasti jaoks.", + "studios_found": "{count} stuudiot leitud", + "tags_found": "{count} silti leitud", + "scrape_results_missing": "Puudub" }, "dimensions": "Dimensioonid", "director": "Režissöör", @@ -963,7 +1011,8 @@ "list": "Nimekiri", "tagger": "Sildistaja", "unknown": "Teadmata", - "wall": "Sein" + "wall": "Sein", + "label_current": "Kuvarežiim: {current}" }, "donate": "Anneta", "dupe_check": { @@ -1047,7 +1096,7 @@ "filters": "Filtrid", "folder": "Kaust", "framerate": "Kaadrisagedus", - "frames_per_second": "{value} fps", + "frames_per_second": "{value} kaadrit sekundis", "front_page": { "types": { "premade_filter": "Eelsätestatud Filter", @@ -1076,8 +1125,8 @@ "syncing": "Serveriga sünkroniseerimine", "uploading": "Skripti üleslaadimine" }, - "hasChapters": "Sisaldab Episoode", - "hasMarkers": "On Markereid", + "hasChapters": "Peatükid", + "hasMarkers": "Markerid", "height": "Pikkus", "height_cm": "Pikkus (cm)", "help": "Abi", @@ -1110,7 +1159,7 @@ "interactive_speed": "Interaktiivne kiirus", "performer_card": { "age": "{age} {years_old}", - "age_context": "{age} selles stseenis {years_old}" + "age_context": "{age} {years_old} filmimisel" }, "phash": "PHash", "play_count": "Esituste Arv", @@ -1123,8 +1172,7 @@ "metadata": "Metaandmed", "name": "Nimi", "new": "Uus", - "none": "Puudub", - "o_counter": "O-Loendur", + "none": "Mitte ükski", "operations": "Operatsioonid", "organized": "Organiseeritud", "pagination": { @@ -1166,7 +1214,6 @@ "no_results_found": "Tulemusi ei leitud.", "number_of_performers_will_be_processed": "{performer_count} näitlejat töödeldakse", "performer_already_tagged": "Näitleja juba märgitud", - "performer_names_separated_by_comma": "Esinejate nimed on eraldatud komaga", "performer_selection": "Näitlejate valik", "performer_successfully_tagged": "Näitleja edukalt märgitud:", "query_all_performers_in_the_database": "Kõik andmebaasis olevad näitlejad", @@ -1179,7 +1226,8 @@ "untagged_performers": "Märkimata näitlejad", "update_performer": "Uuenda Näitlejat", "update_performers": "Uuenda Näitlejaid", - "updating_untagged_performers_description": "Märgistamata esinejate värskendamisel püütakse leida vasteid esinejatele, kellel puudub stashid, ja värskendatakse metaandmeid." + "updating_untagged_performers_description": "Märgistamata esinejate värskendamisel püütakse leida vasteid esinejatele, kellel puudub stashid, ja värskendatakse metaandmeid.", + "performer_names_or_stashids_separated_by_comma": "Näitlejate nimed või StashID-d eraldatud komadega" }, "performer_tags": "Näitleja Sildid", "performers": "Näitlejad", @@ -1210,7 +1258,9 @@ "edit_filter": "Muuda Filtrit", "name": "Filter", "saved_filters": "Salvestatud filtrid", - "update_filter": "Uuenda Filtrit" + "update_filter": "Uuenda Filtrit", + "more_filter_criteria": "+{count} rohkem", + "search_term": "Otsi sõnet" }, "second": "Sekund", "seconds": "Sekundit", @@ -1257,7 +1307,7 @@ }, "paths": { "database_filename_empty_for_default": "andmebaasi failinimi (vaikimisi tühi)", - "description": "Järgmisena peame kindlaks määrama, kust leida su pornokogu ja kuhu salvestada stashi andmebaas, genereeritud failid ja cache. Neid sätteid saab hiljem vajadusel muuta.", + "description": "Järgmisena peame kindlaks määrama, kust leida su sisu ja kuhu salvestada stashi andmebaas, genereeritud failid ja cache. Neid sätteid saab hiljem vajadusel muuta.", "path_to_cache_directory_empty_for_default": "tee cache kaustani (tühi vaikeseadeks)", "path_to_generated_directory_empty_for_default": "genereeritud kataloogi tee (vaikimisi tühi)", "set_up_your_paths": "Seadista oma failiteed", @@ -1268,14 +1318,17 @@ "where_can_stash_store_cache_files": "Kus saab Stash hoida cache faile?", "where_can_stash_store_cache_files_description": "Mõne funktsionaalsuse, nagu HLS/DASH reaalas transkodeerimine, töötamiseks vajab Stash cache kausta ajutiste failide jaoks. Vaikimisi loob Stash cache kausta mis asub konfiguratsioonifailiga samas kaustas. Kui tahad seda muuta, palun sisesta absoluutne või relatiivne (töökaustaga) tee. Stash loob selle kausta kui seda juba ei eksisteeri.", "where_can_stash_store_its_database": "Kuhu saab Stash oma andmebaasi salvestada?", - "where_can_stash_store_its_database_description": "Stash kasutab su porno metaandmete salvestamiseks SQLite'i andmebaasi. Vaikimisi luuakse see konfiguratsioonifaili sisaldavasse kataloogi kui stash-go.sqlite. Kui soovid seda muuta, sisesta absoluutne või suhteline failinimi (praeguse töökataloogi suhtes).", + "where_can_stash_store_its_database_description": "Stash kasutab su sisu metaandmete salvestamiseks SQLite'i andmebaasi. Vaikimisi luuakse see konfiguratsioonifaili sisaldavasse kataloogi kui stash-go.sqlite. Kui soovid seda muuta, sisesta absoluutne või suhteline failinimi (praeguse töökataloogi suhtes).", "where_can_stash_store_its_database_warning": "HOIATUS: hoides andmebaasi erineval süsteemil kui millel Stash jookseb (nt hoides andmebaasi NASil kui Stash jookseb teisel arvutil) on mitte toetatud! SQLite ei ole mõeldud kasutamiseks üle võrgu ja selle proovimine võib väga kergesti viia andmebaasi korrupeerumiseni.", "where_can_stash_store_its_generated_content": "Kus saab Stash oma genereeritud sisu salvestada?", "where_can_stash_store_its_generated_content_description": "Pisipiltide, eelvaadete ja spraitide pakkumiseks loob Stash pilte ja videoid. See hõlmab ka toetamata failivormingute ümbertöötlemist. Vaikimisi loob Stash konfiguratsioonifaili sisaldavas kaustas genereeritud kausta. Kui soovid muuta seda, kus see loodud meedium salvestatakse, sisesta absoluutne või suhteline failitee (praeguse töökataloogi suhtes). Stash loob selle kausta, kui seda veel pole.", - "where_is_your_porn_located": "Kus su porno asub?", - "where_is_your_porn_located_description": "Lisage oma pornovideoid ja pilte sisaldavad kataloogid. Stash kasutab neid katalooge skanimise ajal videote ja piltide otsimiseks.", + "where_is_your_porn_located": "Kus su sisu asub?", + "where_is_your_porn_located_description": "Lisage oma videoid ja pilte sisaldavad kataloogid. Stash kasutab neid katalooge skanimise ajal videote ja piltide otsimiseks.", "path_to_blobs_directory_empty_for_default": "blobsi kataloogi tee (vaikimisi tühi)", - "store_blobs_in_database": "Salvesta blobid andmebaasi" + "store_blobs_in_database": "Salvesta blobid andmebaasi", + "sfw_content_settings": "Kasutad stashi SFW sisu jaoks?", + "sfw_content_settings_description": "stashi saab kasutada SFW-sisu, näiteks fotograafia, kunsti, koomiksite ja muu haldamiseks. Selle valiku lubamine muudab kasutajaliidese käitumist SFW-sisu jaoks sobivamaks.", + "use_sfw_content_mode": "Kasuta SFW sisu režiimi" }, "stash_setup_wizard": "Stashi Ülessättimise Viisard", "success": { @@ -1329,7 +1382,8 @@ "scenes_played": "Mängitud Stseenid", "total_o_count": "Kokku O-Arv", "total_play_count": "Mängimiste Koguarv", - "total_play_duration": "Mängimiste Kogukestus" + "total_play_duration": "Mängimiste Kogukestus", + "total_o_count_sfw": "Meeldimisi kokku" }, "status": "Staatus: {statusText}", "studio": "Stuudio", @@ -1371,7 +1425,7 @@ "updated_at": "Viimati Uuendatud", "url": "URL", "validation": { - "date_invalid_form": "${path} peab olema AAAA-KK-PP vormis", + "date_invalid_form": "${path} peab olema AAAA, AAAA-KK või AAAA-KK-PP vormis", "required": "${path} on nõutud väli", "blank": "${path} ei tohi olla tühi", "unique": "${path} peab olema kordumatu", @@ -1451,7 +1505,6 @@ "no_results_found": "Tulemusi ei leitud.", "number_of_studios_will_be_processed": "{studio_count} stuudiot töödeldakse", "studio_already_tagged": "Stuudio on juba märgistatud", - "studio_names_separated_by_comma": "Komaga eraldatud stuudionimed", "studio_selection": "Stuudio valik", "studio_successfully_tagged": "Stuudio märgistamine õnnestus", "query_all_studios_in_the_database": "Kõik stuudiod andmebaasis", @@ -1466,7 +1519,8 @@ "refreshing_will_update_the_data": "Värskendamisel värskendatakse kõigi stash-boxi eksemplari märgistatud stuudiote andmeid.", "status_tagging_studios": "Staatus: Stuudiote märgistamine", "tag_status": "Märke Staatus", - "updating_untagged_studios_description": "Märgistamata stuudiote värskendamisel püütakse sobitada kõik stuudiod, millel puudub stashid, ja värskendatakse metaandmeid." + "updating_untagged_studios_description": "Märgistamata stuudiote värskendamisel püütakse sobitada kõik stuudiod, millel puudub stashid, ja värskendatakse metaandmeid.", + "studio_names_or_stashids_separated_by_comma": "Stuudio nimes või StashID-d eraldatud komadega" }, "urls": "URLid", "video_codec": "Video Koodek", @@ -1515,7 +1569,29 @@ "custom_fields": { "field": "Väli", "title": "Kohandatud Väli", - "value": "Väärtus" + "value": "Väärtus", + "criteria_format_string_others": "{criterion} (kohandatud väli) {modifierString} {valueString} (+{others} teist)", + "criteria_format_string": "{criterion} (kohandatud väli) {modifierString} {valueString}" }, - "eta": "ETA" + "eta": "ETA", + "login": { + "username": "Kasutajanimi", + "password": "Parool", + "internal_error": "Ootamatu sisemine viga. Vaata logisid rohkemate detailide jaoks", + "invalid_credentials": "Vale kasutajanimi või parool", + "login": "Logi Sisse" + }, + "age_on_date": "{age} filmimisel", + "sort_name": "Sorteeritud Nimi", + "scenes_duration": "Stseeni Pikkus", + "last_o_at_sfw": "Viimane Meeldimine", + "o_count_sfw": "Meeldimisi", + "o_history_sfw": "Meeldimiste Ajalugu", + "odate_recorded_no_sfw": "Meeldimise Kuupäeva Pole Salvestatud", + "stashbox_search": { + "header": "Otsi {entityType} StashBoxist", + "no_results": "Vasteid ei leitud.", + "placeholder_name_or_id": "{entityType} nimi või StashID...", + "select_stashbox": "Vali StashBox..." + } } diff --git a/ui/v2.5/src/locales/fa-IR.json b/ui/v2.5/src/locales/fa-IR.json index ed9914f98..25a57a44c 100644 --- a/ui/v2.5/src/locales/fa-IR.json +++ b/ui/v2.5/src/locales/fa-IR.json @@ -5,6 +5,31 @@ "add_entity": "افزودن {entityType}", "add_to_entity": "اضافه‌کردن به {entityType}", "allow": "اجازه دادن", - "allow_temporarily": "به طور موقت اجازه دهید" + "allow_temporarily": "موقتا اجازه دهید", + "add_sub_groups": "افزودن زیرگروه", + "browse_for_image": "پیدا کردن عکس…", + "cancel": "لغو", + "choose_date": "انتخاب تاریخ", + "clean": "پاکسازی", + "create_entity": "ایجاد {entityType}", + "add_manual_date": "افزودن دستی تاریخ", + "add_play": "افزودن نمایش", + "apply": "اعمال", + "backup": "پشتیبان گیری", + "auto_tag": "تگ زدن خودکار", + "clear_front_image": "پاک کردن عکس جلویی", + "clean_generated": "پاکسازی فایل های تولید شده", + "clear": "پاک کردن", + "clear_back_image": "پاک کردن عکس پشتی", + "clear_date_data": "پاک کردن دیتای تاریخ", + "clear_image": "پاک کردن عکس", + "close": "بستن", + "confirm": "تایید", + "continue": "ادامه", + "copy_to_clipboard": "کپی به کلیپبورد", + "create": "ایجاد", + "create_parent_studio": "ایجاد استادیو والد", + "anonymise": "بی نام کردن", + "create_chapters": "ایجاد فصل" } } diff --git a/ui/v2.5/src/locales/fi-FI.json b/ui/v2.5/src/locales/fi-FI.json index f90e06076..015d133ed 100644 --- a/ui/v2.5/src/locales/fi-FI.json +++ b/ui/v2.5/src/locales/fi-FI.json @@ -141,7 +141,17 @@ "add_sub_groups": "Lisää aliryhmiä", "migrate_blobs": "Siirrä blobit", "migrate_scene_screenshots": "Siirrä kohtauksen kuvakaappaukset", - "reset_play_duration": "Nollaa toiston kesto" + "reset_play_duration": "Nollaa toiston kesto", + "load": "Ladataan", + "load_filter": "Lataa filtteri", + "play": "Toista", + "show_results": "Näytä tulokset", + "show_count_results": "Näytä {count} tulosta", + "sidebar": { + "close": "Sulje sivupalkki", + "open": "Avaa sivupalkki", + "toggle": "näytä/piilota sivupalkki" + } }, "actions_name": "Toiminnot", "age": "Ikä", @@ -187,8 +197,6 @@ "set_cover_label": "Aseta kohtauksen kansikuva", "set_tag_desc": "Liitä tunnisteet kohtaukseen, joko ylikirjoittamalla nykyiset tai yhdistämällä ne jo olemassa oleviin tunnisteihin.", "set_tag_label": "Aseta tunnisteet", - "show_male_desc": "Valitse käytetäänkö miesesiintyjiä automaattisessa tunnisteiden asettamisessa.", - "show_male_label": "Näytä miesesiintyjät", "source": "Lähde", "mark_organized_label": "Merkitse järjestetyksi kun tallennetaan", "mark_organized_desc": "Merkitsee kohtauksen järjestellyksi hetki kun Tallenna -painiketta on painettu.", @@ -367,10 +375,12 @@ }, "transcode": { "output_args": { - "desc": "Edistynyt: Lisäargumentit, jotka välitetään ffmpegille ennen tuloskenttää videota luotaessa." + "desc": "Edistynyt: Lisäargumentit, jotka välitetään ffmpegille ennen tuloskenttää videota luotaessa.", + "heading": "FFmpeg:n muunnoksen ulostuloparametrit" }, "input_args": { - "desc": "Edistynyt: Lisäargumentit, jotka välitetään ffmpegille ennen syöttökenttää videota luotaessa." + "desc": "Edistynyt: Lisäargumentit, jotka välitetään ffmpegille ennen syöttökenttää videota luotaessa.", + "heading": "FFmpeg:n muunnoksen syöteparametrit" } }, "ffmpeg_path": { @@ -383,10 +393,12 @@ }, "live_transcode": { "input_args": { - "desc": "Edistynyt: Lisäargumentit, jotka välitetään ffmpegille ennen syöttökenttää, kun videota muutetaan livenä." + "desc": "Edistynyt: Lisäargumentit, jotka välitetään ffmpegille ennen syöttökenttää, kun videota muutetaan livenä.", + "heading": "FFmpeg:n live-muunnoksen syöteparametrit" }, "output_args": { - "desc": "Edistynyt: Lisäargumentit, jotka siirretään ffmpegille ennen lähtökenttää, kun videota muutetaan livenä." + "desc": "Edistynyt: Lisäargumentit, jotka siirretään ffmpegille ennen lähtökenttää, kun videota muutetaan livenä.", + "heading": "FFmpeg:n live-muunnoksen ulostuloparametrit" } } }, @@ -405,7 +417,8 @@ "funscript_heatmap_draw_range_desc": "Piirrä liikealue generoidun lämpökartan y-akselille. Olemassa olevat lämpökartat on luotava uudelleen vaihtamisen jälkeen.", "funscript_heatmap_draw_range": "Sisällytä alue luotuihin lämpökarttoihin", "gallery_cover_regex_desc": "Regexp käytetään kuvaamaan gallerian kansikuvaa", - "gallery_cover_regex_label": "Gallerian kansikuvio" + "gallery_cover_regex_label": "Gallerian kansikuvio", + "heatmap_generation": "Funscript-lämpökarttageneraattori" }, "library": { "exclusions": "Poisjättäminen", @@ -435,7 +448,9 @@ "endpoint": "Päätepiste", "graphql_endpoint": "GraphQL päätepiste", "name": "Nimi", - "title": "Stash-box päätepisteet" + "title": "Stash-box päätepisteet", + "max_requests_per_minute": "Enimmäispyynnöt minuutissa", + "max_requests_per_minute_description": "Käyttää oletusarvoa {defaultValue}, jos se on asetettu 0:ksi" }, "system": { "transcoding": "Transkoodaus" @@ -491,14 +506,21 @@ "sources": "Lähteet", "strategy": "Strategia", "skip_multiple_matches": "Ohita vastaavat, joilla on useampi kuin yksi tulos", - "skip_multiple_matches_tooltip": "Jos tämä ei ole otettu käyttöön ja palautetaan useampi kuin yksi tulos, yksi valitaan satunnaisesti vastaamaan" + "skip_multiple_matches_tooltip": "Jos tämä ei ole otettu käyttöön ja palautetaan useampi kuin yksi tulos, yksi valitaan satunnaisesti vastaamaan", + "skip_single_name_performers": "Ohita yksinimiset esiintyjät ilman tarkennusta", + "skip_single_name_performers_tooltip": "Jos tämä ei ole käytössä, usein geneeriset esiintyjät kuten Samantha tai Olga yhdistetään", + "tag_skipped_matches": "Merkitse ohitetut osumat seuraavalla", + "tag_skipped_matches_tooltip": "Luo tunniste, kuten 'Tunnista: Useita osumia', jota voit suodattaa Scene Tagger -näkymässä ja valita oikean osuman käsin", + "tag_skipped_performer_tooltip": "Luo tunniste, kuten 'Identify: Single Name Performer', jota voit suodattaa Scene Tagger -näkymässä ja valita, miten haluat käsitellä näitä esiintyjiä", + "tag_skipped_performers": "Merkitse ohitetut esiintyjät seuraavalla" }, "import_from_exported_json": "Tuo viedystä JSON -tiedoista, jotka ovat samassa kansiossa kuin metadata. Pyyhkii olemassaolevan tietokannan.", "incremental_import": "Lisäävä tuonti valitusta zip -tiedostosta.", "job_queue": "Tehtäväjono", "maintenance": "Ylläpito", "migrate_blobs": { - "delete_old": "Poista vanhat tiedot" + "delete_old": "Poista vanhat tiedot", + "description": "Siirrä blobit nykyiseen blob-tallennusjärjestelmään. Tämä siirto tulisi suorittaa blob-tallennusjärjestelmän vaihdon jälkeen. Vanhojen tietojen poistaminen siirron jälkeen on valinnainen." }, "migrate_hash_files": "Käytetään kun muutetaan generoitua tiedoston nimeämistiivistettä jo generoitujen tiedostojen uudelleennimeämiseen uuteen muotoon.", "migrations": "Migraatiot", @@ -516,12 +538,26 @@ "previews_desc": "Kohtauksien esikatselut ja pienoiskuvat", "blob_files": "Blob-tiedostot", "description": "Poistaa luodut tiedostot ilman vastaavaa tietokantatietuetta.", - "image_thumbnails": "Kuvien pikkukuvat" + "image_thumbnails": "Kuvien pikkukuvat", + "image_thumbnails_desc": "Kuvien pikkukuvat ja pätkät", + "sprites": "Kohtauksien sprite-kuvat", + "transcodes": "Kohtauksien muunnokset" }, "anonymising_database": "Anonymisoidaan tietokantaa", "anonymise_database": "Tekee kopion tietokannasta varmuuskopioiden hakemistoon anonymisoimalla kaikki arkaluontoiset tiedot. Tämä voidaan sitten tarjota muille vianmääritys- ja viankorjaustarkoituksiin. Alkuperäistä tietokantaa ei ole muokattu. Anonymisoitu tietokanta käyttää tiedostonimimuotoa {filename_format}.", "generate_sprites_during_scan_tooltip": "Videosoittimen alla näkyvät kuvat navigoinnin helpottamiseksi.", - "generate_video_covers_during_scan": "Luo kohtausten kannet" + "generate_video_covers_during_scan": "Luo kohtausten kannet", + "generate_clip_previews_during_scan": "Luo esikatselukuvat kuvaklippejä varten", + "generate_sprites_during_scan": "Luo pyyhkijän kuvasarjat", + "migrate_scene_screenshots": { + "delete_files": "Poista näyttötallenteiden tiedostot", + "description": "Siirrä kohtauksen näyttökuvat uuteen blob-tallennusjärjestelmään. Tämä siirto tulisi suorittaa olemassa olevan järjestelmän päivittämisen jälkeen versioon 0.20. Vanhojen näyttökuvien poistaminen siirron jälkeen on valinnainen.", + "overwrite_existing": "Korvaa olemassa olevat blobit näyttötallennetiedoilla" + }, + "optimise_database": "Yritä parantaa suorituskykyä analysoimalla ja koko tietokantatiedoston uudelleen rakentamalla.", + "optimise_database_warning": "Varoitus: tämän tehtävän ollessa käynnissä kaikki tietokantaa muokkaavat toiminnot epäonnistuvat, ja tietokannan koosta riippuen suoritus voi kestää useita minuutteja. Tarvitset lisäksi vähintään yhtä paljon vapaata levytilaa kuin tietokantasi koko on, mutta 1,5-kertainen määrä on suositeltavaa.", + "rescan": "Uudelleenskannaa tiedostot", + "rescan_tooltip": "Uudelleenskannaa kaikki tiedostot polussa. Käytetään tiedoston metatietojen pakolliseen päivitykseen ja zip-tiedostojen uudelleenskannaukseen." }, "tools": { "scene_duplicate_checker": "Kohtauksien kaksoiskappaleiden tarkistus", @@ -533,9 +569,16 @@ "ignore_organized": "Jätä järjestellyt kohtaukset huomiotta", "ignored_words": "Huomiotta jätetyt sanat", "matches_with": "Täsmää seuraavan kanssa {i}", - "whitespace_chars_desc": "Nämä merkit korvataan välilyönnillä otsikossa" + "whitespace_chars_desc": "Nämä merkit korvataan välilyönnillä otsikossa", + "escape_chars": "Käytä \\ merkin edessä, kun haluat käsitellä merkin kirjaimellisena merkkinä", + "filename_pattern": "Tiedostonimen malli", + "select_parser_recipe": "Valitse jäsentämisen ohjeistus, joka määrittää tiedon purkamisen ja käsittelyn", + "title": "Kohteen tiedostonimen jäsentäjä", + "whitespace_chars": "välilyöntimerkit" }, - "scene_tools": "Kohtauksen työkalut" + "scene_tools": "Kohtauksen työkalut", + "graphql_playground": "GraphQL-kokeiluympäristö", + "heading": "Työkalut" }, "ui": { "basic_settings": "Perusasetukset", @@ -551,7 +594,8 @@ }, "custom_locales": { "heading": "Mukautettu lokalisointi", - "option_label": "Mukautettu lokalisointi käytössä" + "option_label": "Mukautettu lokalisointi käytössä", + "description": "Ylikirjoita yksittäisiä paikallisia merkkijonoja. Katso https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/locales/en-GB.json master-lista. Sivun lataus on tehtävä uudelleen, jotta muutokset tulevat voimaan." }, "delete_options": { "description": "Oletusasetukset kun poistetaan kuvia, gallerioita ja kohtauksia.", @@ -580,7 +624,8 @@ "options": { "full": "Kokonainen", "half": "Puolikas", - "quarter": "Neljäsosa" + "quarter": "Neljäsosa", + "tenth": "Kymmenes" } }, "type": { @@ -590,6 +635,9 @@ "stars": "Tähdet" } } + }, + "max_options_shown": { + "label": "Valintavalikoissa näytettävien kohteiden enimmäismäärä" } }, "funscript_offset": { @@ -687,13 +735,33 @@ } } }, - "title": "Käyttöliittymä" + "title": "Käyttöliittymä", + "abbreviate_counters": { + "description": "Lyhennä lukujen esitystapaa korteissa ja yksityiskohtien näkymissä, esimerkiksi luku \"1831\" esitetään muodossa \"1,8K\".", + "heading": "Lukujen esitysmuodon lyhentäminen" + }, + "detail": { + "compact_expanded_details": { + "description": "Kun tämä asetus on otettu käyttöön, se näyttää laajennetut tiedot säilyttäen samalla kompaktin esityksen.", + "heading": "Tiivistetyt laajennetut tiedot" + }, + "enable_background_image": { + "description": "Näytä taustakuva yksityiskohtasivulla.", + "heading": "Ota taustakuva käyttöön" + }, + "heading": "Lisätietosivu", + "show_all_details": { + "description": "Kun tämä on otettu käyttöön, kaikki sisällön tiedot näytetään oletuksena ja jokainen tietoelementti mahtuu yhden sarakkeen alle.", + "heading": "Näytä kaikki tiedot" + } + } }, "advanced_mode": "Edistynyt tila", "plugins": { "installed_plugins": "Asennetut lisäosat", "available_plugins": "Saatavilla olevat liitännäiset", - "hooks": "Koukut" + "hooks": "Koukut", + "triggers_on": "Aktivoituu kun" } }, "configuration": "Konfiguraatio", @@ -784,7 +852,6 @@ "destination": "Kohde", "source": "Lähde" }, - "overwrite_filter_confirm": "Haluatko varmasti ylikirjoittaa jo olemassaolevan {entityName}?", "scene_gen": { "force_transcodes": "Pakota transkoodaus", "force_transcodes_tooltip": "Oletuksena transkoodaus tehdään vain, mikäli selain ei tue videotiedostoa. Jos tämä valinta on päällä, transkoodaus tehdään vaikka selain näyttäisi tukevan videotiedostoa.", @@ -793,7 +860,7 @@ "marker_image_previews": "Animoidut merkkien esikatselukuvat", "marker_image_previews_tooltip": "Animoidut merkkien WebP esikatselut, vaaditaan vain jos esikatselun tyypiksi on valittu Animoitu kuva.", "marker_screenshots": "Merkkien esikatselukuvat", - "marker_screenshots_tooltip": "Merkkien staattinen JPG -kuva, vaadittu vain jos esikatselutyypiksi on asetettu staattinen kuva.", + "marker_screenshots_tooltip": "Staattiset JPG-kuvat merkintöihin", "markers": "Merkkien esikatselut", "markers_tooltip": "20 sekunnin video jokaisen aikakoodin alusta.", "override_preview_generation_options": "Ohita Esikatselun generoinnin asetukset", @@ -942,7 +1009,7 @@ "interactive_speed": "Interaktiivinen nopeus", "performer_card": { "age": "{age} {years_old}", - "age_context": "{age} {years_old} tässä kohtauksessa" + "age_context": "{age} {years_old} tuotantovaiheessa" }, "phash": "PHash", "play_count": "Toistokerrat", @@ -955,7 +1022,6 @@ "name": "Nimi", "new": "Uusi", "none": "Ei mitään", - "o_counter": "O-Laskuri", "operations": "Operaatiot", "organized": "Järjestelty", "pagination": { @@ -994,7 +1060,6 @@ "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", @@ -1169,7 +1234,6 @@ "current_page": "Nykyinen sivu", "query_all_studios_in_the_database": "Kaikki studiot tietokannassa", "no_results_found": "Ei tuloksia.", - "studio_names_separated_by_comma": "Studion nimet eriteltynä pilkuilla", "studio_successfully_tagged": "Studion tunnisteiden asettaminen onnistui", "studio_selection": "Studion valinta", "update_studios": "Päivitä studiot", diff --git a/ui/v2.5/src/locales/fr-FR.json b/ui/v2.5/src/locales/fr-FR.json index 3e9e74648..a72361f22 100644 --- a/ui/v2.5/src/locales/fr-FR.json +++ b/ui/v2.5/src/locales/fr-FR.json @@ -141,7 +141,19 @@ "reset_resume_time": "Réinitialiser le temps de reprise", "set_cover": "Définir comme vignette", "remove_from_containing_group": "Supprimer du groupe", - "add_sub_groups": "Ajouter des groupes affiliés" + "add_sub_groups": "Ajouter des groupes affiliés", + "sidebar": { + "close": "Fermer la barre latérale", + "open": "Ouvrir la barre latérale", + "toggle": "Barre latérale" + }, + "show_count_results": "Afficher {count} résultats", + "show_results": "Afficher les résultats", + "play": "Lecture", + "load": "Charger", + "load_filter": "Charger un filtre", + "add_stash_id": "Ajouter un identifiant Stash", + "create_new": "Créer un nouveau" }, "actions_name": "Actions", "age": "Âge", @@ -190,11 +202,13 @@ "set_cover_label": "Définir la vignette 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", "errors": { "blacklist_duplicate": "Élément de liste noire en double" + }, + "performer_genders": { + "heading": "Genres des performeurs", + "description": "Les performeurs de ces genres seront affichés lors du marquage des scènes." } }, "noun_query": "Requête", @@ -216,7 +230,10 @@ "verb_scrape_all": "Extraire tout", "verb_submit_fp": "Soumettre {fpCount, plural, one{# Empreinte} other{# Empreintes}}", "verb_toggle_config": "{toggle} {configuration}", - "verb_toggle_unmatched": "{toggle} scènes incomparables" + "verb_toggle_unmatched": "{toggle} scènes incomparables", + "verb_add_as_alias": "Ajouter le nom récupéré comme alias", + "verb_link_existing": "Lien vers existant", + "verb_match_tag": "Étiquette correspondante" }, "config": { "about": { @@ -295,18 +312,20 @@ "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. Laisser vide pour désactiver l'authentification utilisateur" + "username_desc": "Nom d'utilisateur pour accéder à Stash. Laisser vide pour désactiver l'authentification utilisateur", + "log_file_max_size": "Taille maximale du journal", + "log_file_max_size_desc": "Taille maximale en mégaoctets du fichier journal avant compression. 0 Mo est désactivé. Nécessite un redémarrage." }, "backup_directory_path": { "description": "Emplacement de sauvegarde des bases de données SQLite", "heading": "Chemin du répertoire de sauvegarde" }, "blobs_path": { - "description": "Emplacement dans le système de fichiers pour le stockage des données binaires. Uniquement applicable lors de l'utilisation du type de stockage blob du système de fichiers. AVERTISSEMENT : La modification de ce paramètre nécessite le déplacement manuel des données existantes.", + "description": "Emplacement de stockage des données binaires dans le système de fichiers. Uniquement applicable lors de l'utilisation du type de stockage blob du système de fichiers. AVERTISSEMENT : La modification de ce paramètre nécessite le déplacement manuel des données existantes.", "heading": "Chemin du système de fichiers des données binaires" }, "blobs_storage": { - "description": "Emplacement où stocker les données binaires telles que les vignettes de scènes, les images de performeurs, de studios et d'étiquettes. Après avoir modifié cette valeur, les données existantes doivent être migrées à l'aide de la tâche Migrer les blobs. Voir la page Tâches de migration.", + "description": "Emplacement de stockage des données binaires telles que les vignettes de scènes, images de performeurs, studios et étiquettes. Après avoir modifié cette valeur, les données existantes doivent être migrées à l'aide de la tâche Migrer les blobs. Voir la page Tâches de migration.", "heading": "Type d'enregistrement de données binaires" }, "cache_location": "Emplacement du cache. Requis si le flux est diffusé à l'aide de HLS (comme sur les appareils Apple) ou de DASH.", @@ -411,6 +430,10 @@ "plugins_path": { "description": "Emplacement du répertoire des fichiers de configuration des plugins", "heading": "Chemin des plugins" + }, + "delete_trash_path": { + "description": "Chemin où les fichiers supprimés seront déplacés au lieu d'être définitivement supprimés. Laissez ce champ vide pour supprimer définitivement les fichiers.", + "heading": "Chemin de la corbeille" } }, "library": { @@ -447,7 +470,9 @@ "endpoint": "Point de terminaison", "graphql_endpoint": "Point de terminaison GraphQL", "name": "Nom", - "title": "Points de terminaison Stash-Box" + "title": "Points de terminaison Stash-Box", + "max_requests_per_minute": "Requêtes maximales par minute", + "max_requests_per_minute_description": "Utiliser la valeur par défaut de {defaultValue} si définie à 0" }, "system": { "transcoding": "Transcodage" @@ -573,7 +598,9 @@ "whitespace_chars": "Caractères d'espacement", "whitespace_chars_desc": "Ces caractères seront remplacés par un espace dans le titre" }, - "scene_tools": "Outils de scène" + "scene_tools": "Outils de scène", + "heading": "Outils", + "graphql_playground": "Implémentation GraphQL" }, "ui": { "abbreviate_counters": { @@ -798,6 +825,18 @@ "use_stash_hosted_funscript": { "description": "Activée, les scripts interactifs sont transmis directement de Stash à votre dispositif Handy sans recourir au serveur Handy de tierce partie. Nécessite que Stash soit accessible depuis votre dispositif Handy, et qu'une clé API soit générée si Stash a des informations d'identification configurées.", "heading": "Transmettre directement les funscripts" + }, + "performer_list": { + "heading": "Liste de performeurs", + "options": { + "show_links_on_grid_card": { + "heading": "Afficher les liens sur les fiches des performeurs" + } + } + }, + "sfw_mode": { + "description": "Activez cette option si vous utilisez Stash pour stocker du contenu SFW. Masque ou modifie certains aspects de l'interface utilisateur liés au contenu pour adultes.", + "heading": "Mode contenu SFW" } }, "advanced_mode": "Mode avancé" @@ -896,7 +935,8 @@ "label": "Mode de défilement", "pan_y": "Panoramique Y", "zoom": "Zoom" - } + }, + "disable_animation": "Désactiver l'animation de transition entre les images" }, "merge": { "destination": "Destination", @@ -907,7 +947,6 @@ "destination": "Destination", "source": "Source" }, - "overwrite_filter_confirm": "Êtes-vous sûr de vouloir remplacer la requête sauvegardée existante {entityName} ?", "performers_found": "{count} performeurs trouvés", "reassign_entity_title": "{count, plural, one {Réaffecté {singularEntity}} other {Réaffectés {pluralEntity}}}", "reassign_files": { @@ -960,7 +999,15 @@ "set_image_url_title": "URL de l'image", "unsaved_changes": "Modifications non sauvegardées. Vous êtes sûr de vouloir quitter ?", "clear_o_history_confirm": "Êtes-vous sûr de vouloir effacer l'historique des O ?", - "clear_play_history_confirm": "Êtes-vous sûr de vouloir effacer l'historique de lecture ?" + "clear_play_history_confirm": "Êtes-vous sûr de vouloir effacer l'historique de lecture ?", + "overwrite_filter_warning": "Le filtre enregistré \"{entityName}\" sera remplacé.", + "set_default_filter_confirm": "Êtes-vous sûr de vouloir définir ce filtre par défaut ?", + "clear_o_history_confirm_sfw": "Êtes-vous sûr de vouloir effacer l'historique des \"J'aime\" ?", + "delete_alert_to_trash": "Les éléments suivants {count, plural, one {{singularEntity}} other {{pluralEntity}}} seront déplacés vers la corbeille :", + "stashid_exists_warning": "L'identifiant Stash existant pour cette Stash-Box sera remplacé.", + "studios_found": "{count} studios trouvés", + "tags_found": "{count} étiquettes trouvées", + "scrape_results_missing": "Manquant" }, "dimensions": "Dimensions", "director": "Réalisateur", @@ -970,7 +1017,8 @@ "list": "Liste", "tagger": "Étiqueteuse", "unknown": "Inconnu", - "wall": "Mur" + "wall": "Mur", + "label_current": "Mode d'affichage : {current}" }, "donate": "Faire un don", "dupe_check": { @@ -1083,8 +1131,8 @@ "syncing": "Synchronisation avec le serveur", "uploading": "Script de chargement" }, - "hasChapters": "A des chapitres", - "hasMarkers": "Dispose de marqueurs", + "hasChapters": "Chapitres", + "hasMarkers": "Marqueurs", "height": "Taille", "height_cm": "Taille (cm)", "help": "Aide", @@ -1114,7 +1162,7 @@ "audio_codec": "Codec audio", "checksum": "Somme de contrôle", "downloaded_from": "Téléchargé depuis", - "hash": "Hachage", + "hash": "Empreinte", "interactive_speed": "Vitesse interactive", "performer_card": { "age": "{age} {years_old}", @@ -1132,7 +1180,6 @@ "name": "Nom", "new": "Nouveau", "none": "Aucun", - "o_counter": "O-Compteur", "operations": "Opérations", "organized": "Organisé", "pagination": { @@ -1178,7 +1225,6 @@ "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", @@ -1191,7 +1237,8 @@ "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 d'identifiant Stash et actualisera les métadonnées." + "updating_untagged_performers_description": "Une mise à jour des performeurs non étiquetés essaiera de faire correspondre tous les performeurs qui n'ont pas d'identifiant Stash et actualisera les métadonnées.", + "performer_names_or_stashids_separated_by_comma": "Noms des performeurs ou identifiants Stash séparés par des virgules" }, "performer_tags": "Étiquettes de performeur", "performers": "Performeurs", @@ -1222,7 +1269,9 @@ "edit_filter": "Modifier le filtre", "name": "Filtre", "saved_filters": "Filtres sauvegardés", - "update_filter": "Filtre actualisé" + "update_filter": "Filtre actualisé", + "more_filter_criteria": "+{count} de plus", + "search_term": "Terme recherché" }, "second": "Deuxième", "seconds": "Secondes", @@ -1269,7 +1318,7 @@ }, "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 pornographique, et où stocker la base de données Stash, les fichiers générés et les fichiers cache. Ces paramètres peuvent être modifiés ultérieurement si nécessaire.", + "description": "Ensuite, nous devons déterminer où trouver votre contenu, et où stocker la base de données Stash, les fichiers générés et les fichiers cache. Ces paramètres peuvent être modifiés ultérieurement si nécessaire.", "path_to_blobs_directory_empty_for_default": "chemin vers le répertoire des blobs (vide par défaut)", "path_to_cache_directory_empty_for_default": "chemin du répertoire du cache (vide par défaut)", "path_to_generated_directory_empty_for_default": "Chemin vers le répertoire généré (vide par défaut)", @@ -1282,12 +1331,15 @@ "where_can_stash_store_cache_files": "Où Stash peut-il stocker les fichiers cache ?", "where_can_stash_store_cache_files_description": "Pour que certaines fonctionnalités telles que le transcodage en temps réel HLS/DASH puissent fonctionner, Stash a besoin d'un répertoire de cache pour les fichiers temporaires. Par défaut, Stash créera un sous-répertoire cache dans le répertoire contenant votre fichier de configuration. Si vous souhaitez le modifier, merci de saisir un chemin absolu ou relatif (par rapport au répertoire de travail actuel). Stash créera ce sous-répertoire s'il n'existe pas déjà.", "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 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_database_description": "Stash utilise une base de données SQLite pour stocker vos métadonnées de contenu. 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_database_warning": "AVERTISSEMENT : Le stockage de la base de données sur un système différent de celui à partir duquel Stash est exécuté (par exemple, le stockage de la base de données sur un NAS tout en exécutant le serveur Stash sur un autre ordinateur) est non pris en charge ! SQLite n'est pas conçu pour être utilisé sur un réseau, et toute tentative de le faire peut très facilement entraîner la corruption de l'ensemble de votre base de données.", "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, aperçus et sprites, 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 pornographiques. Stash utilisera ces répertoires pour rechercher les vidéos et les images lors de l'analyse." + "where_is_your_porn_located": "Où se trouve votre contenu ?", + "where_is_your_porn_located_description": "Ajoutez des répertoires contenant vos vidéos et images. Stash utilisera ces répertoires pour rechercher les vidéos et les images lors de l'analyse.", + "sfw_content_settings": "Utiliser Stash pour du contenu SFW ?", + "sfw_content_settings_description": "Stash peut être utilisé pour gérer du contenu SFW tel que des photographies, des illustrations, des bandes dessinées, et plus. L'activation de cette option modifiera certains comportements de l'interface utilisateur pour les rendre plus appropriés au contenu SFW.", + "use_sfw_content_mode": "Utiliser le mode de contenu SFW" }, "stash_setup_wizard": "Assistant de configuration de Stash", "success": { @@ -1341,7 +1393,8 @@ "scenes_size": "Poids des scènes", "total_o_count": "Nombre total de O-", "total_play_count": "Nombre de visionnage total", - "total_play_duration": "Temps de visionnage total" + "total_play_duration": "Temps de visionnage total", + "total_o_count_sfw": "Total de J'aime" }, "status": "Statut : {statusText}", "studio": "Studio", @@ -1375,7 +1428,6 @@ "status_tagging_job_queued": "Statut : Étiquetage en file d'attente", "status_tagging_studios": "Statut : Étiquetage des studios", "studio_already_tagged": "Studio déjà étiqueté", - "studio_names_separated_by_comma": "Noms de studio séparés par une virgule", "studio_selection": "Sélection de studio", "studio_successfully_tagged": "Studio étiqueté avec succès", "tag_status": "Statut de l'étiquette", @@ -1383,7 +1435,8 @@ "untagged_studios": "Studios non étiquetés", "update_studio": "Actualiser le studio", "update_studios": "Actualiser les studios", - "updating_untagged_studios_description": "Une mise à jour des studios non étiquetés essaiera de faire correspondre les studios qui n'ont pas d'identifiant Stash et actualisera les métadonnées." + "updating_untagged_studios_description": "Une mise à jour des studios non étiquetés essaiera de faire correspondre les studios qui n'ont pas d'identifiant Stash et actualisera les métadonnées.", + "studio_names_or_stashids_separated_by_comma": "Noms de studios ou identifiants Stash séparés par des virgules" }, "studios": "Studios", "sub_tag_count": "Nombre d'étiquettes affiliées", @@ -1423,7 +1476,7 @@ "url": "URL", "urls": "URLs", "validation": { - "date_invalid_form": "${path} doit être au format AAAA-MM-JJ", + "date_invalid_form": "${path} doit être au format AAAA, AAAA-MM, ou AAAA-MM-JJ", "required": "${path} est un champ requis", "blank": "${path} ne doit pas être vide", "unique": "${path} doit être unique", @@ -1478,13 +1531,13 @@ "last_o_at": "Dernier O le", "o_count": "Nombre d'O", "o_history": "Historique d'O", - "odate_recorded_no": "Aucune date d'O enregistrée", + "odate_recorded_no": "Aucun O daté enregistré", "subsidiary_studio_count": "Nombre de studios affiliés", "tag_sub_tag_tooltip": "A des étiquettes affiliées", "time": "Temps", "photographer": "Photographe", "play_history": "Historique de Lecture", - "playdate_recorded_no": "Aucune date de lecture enregistrée", + "playdate_recorded_no": "Aucune lecture datée enregistrée", "plays": "{value} lectures", "unknown_date": "Date inconnue", "history": "Historique", @@ -1522,5 +1575,23 @@ }, "eta": "TAE", "sort_name": "Nom de tri", - "age_on_date": "{age} à la production" + "age_on_date": "{age} à la production", + "login": { + "password": "Mot de passe", + "invalid_credentials": "Nom d'utilisateur ou mot de passe incorrect", + "internal_error": "Erreur interne inattendue. Consulter le journal pour plus de détails", + "login": "Identification", + "username": "Nom d'utilisateur" + }, + "scenes_duration": "Durée de la scène", + "last_o_at_sfw": "Dernier J'aime", + "o_count_sfw": "J'aime", + "odate_recorded_no_sfw": "Aucun “J’aime” daté enregistré", + "o_history_sfw": "Historique des \"J’aime\"", + "stashbox_search": { + "header": "Rechercher {entityType} à partir de Stash-Box", + "no_results": "Aucun résultat trouvé.", + "placeholder_name_or_id": "{entityType} nom ou identifiant Stash...", + "select_stashbox": "Sélectionner une Stash-Box..." + } } diff --git a/ui/v2.5/src/locales/hi-IN.json b/ui/v2.5/src/locales/hi-IN.json index 4f18b8fb7..daa1968b3 100644 --- a/ui/v2.5/src/locales/hi-IN.json +++ b/ui/v2.5/src/locales/hi-IN.json @@ -66,5 +66,13 @@ "generate": "उत्पन्न करें", "preview": "पूर्वदर्शन", "refresh": "ताजा करें" + }, + "config": { + "general": { + "blobs_storage": { + "heading": "rgfe" + }, + "database": "डेटाबेस" + } } } diff --git a/ui/v2.5/src/locales/hr-HR.json b/ui/v2.5/src/locales/hr-HR.json index dbd8deab3..2102fc165 100644 --- a/ui/v2.5/src/locales/hr-HR.json +++ b/ui/v2.5/src/locales/hr-HR.json @@ -91,11 +91,196 @@ "swap": "Zamijeni", "tasks": { "clean_confirm_message": "Jeste li sigurni da želite započeti čišćenje? Ovaj će postupak obrisati podatke iz baze podataka i sav generirani sadržaj za sve scene i galerije čije su izvorne datoteke obrisane.", - "dry_mode_selected": "Odabran je probni način rada. Ništa neće biti obrisano, datoteke koje više ne postoje će se samo zapisati u konzolu." + "dry_mode_selected": "Odabran je probni način rada. Ništa neće biti obrisano, datoteke koje više ne postoje će se samo zapisati u konzolu.", + "import_warning": "Da li ste sigurni da želite uvesti? Ovo će izbrisati bazu podataka i ponovno je uvesti iz vaših izvezenih metapodataka." }, "temp_disable": "Privremeno isključi…", "temp_enable": "Privremeno uključi…", "use_default": "Koristi zadane vrijednosti", - "view_random": "Vidi nasumično" + "view_random": "Vidi nasumično", + "add_manual_date": "Dodaj ručni datum", + "add_sub_groups": "Dodaj podgrupu", + "add_o": "Dodaj O", + "add_play": "Dodaj reproduciranje", + "anonymise": "Anonimiziraj", + "assign_stashid_to_parent_studio": "Dodijeli Stash ID na postojeći matični studio i ažuriraj metapodatke", + "choose_date": "Odaberi datum", + "clean_generated": "Očisti generirane datoteke", + "clear_back_image": "Obriši stražnju sliku", + "clear_date_data": "Očisti podatke o datumu", + "clear_front_image": "Očisti prednju sliku", + "copy_to_clipboard": "Kopiraj u međuspremnik", + "create_chapters": "Stvori poglavlje", + "create_parent_studio": "Napravi matični studio", + "customise": "Prilagodi", + "delete_file_and_funscript": "Izbriši datoteku (i funscript)", + "disable": "Onemogući", + "download_anonymised": "Preuzmi anonimno", + "enable": "Omogući", + "encoding_image": "Kodiranje slike…", + "hash_migration": "migracija hash-a", + "load": "Učitaj", + "load_filter": "Učitaj filter", + "make_primary": "Učini Primarnim", + "migrate_blobs": "Spoji Blobs", + "migrate_scene_screenshots": "Migriraj Snimke Zaslona Scene", + "optimise_database": "Optimiziraj Bazu Podataka", + "overwrite": "Prebriši", + "play": "Pokreni", + "reassign": "Preraspodjeli", + "reload": "Ponovno Učitaj", + "reload_scrapers": "Ponovno učitaj scraper-e", + "remove_date": "Ukloni datum", + "remove_from_containing_group": "Ukloni iz Grupe", + "reset_play_duration": "Resetiraj duljinu reprodukcije", + "reset_resume_time": "Resetiraj vrijeme nastavka", + "reset_cover": "Vrati Zadanu Naslovnicu", + "reshuffle": "Promješaj", + "set_back_image": "Stražnja slika…", + "set_cover": "Postavi kao Naslovnicu", + "set_front_image": "Prednja slika…", + "show_results": "Prikaži rezultate", + "show_count_results": "Prikaži {count} rezultata", + "sidebar": { + "close": "Zatvori bočnu traku", + "open": "Otvori bočnu traku", + "toggle": "Uključi/Isključi bočnu traku" + }, + "view_history": "Vidi povijest" + }, + "actions_name": "Radnje", + "age": "Dob", + "age_on_date": "{age} u produkciji", + "aliases": "Pseudonimi", + "all": "sve", + "also_known_as": "Također poznat kao", + "appears_with": "Pojavljuje se sa", + "ascending": "Uzlazno", + "audio_codec": "Audio Kodek", + "average_resolution": "Prosječna Rezolucija", + "between_and": "i", + "birth_year": "Godina Rođenja", + "birthdate": "Datum rođenja", + "bitrate": "Brzina Prijenosa Podataka", + "blobs_storage_type": { + "database": "Baza podataka", + "filesystem": "Datotečni sustav" + }, + "captions": "Natpisi", + "career_length": "Duljina Karijere", + "chapters": "Poglavlja", + "circumcised": "Obrezan", + "circumcised_types": { + "CUT": "Izrezan", + "UNCUT": "Neizrezan" + }, + "component_tagger": { + "config": { + "active_instance": "Aktivna stash-box instanca:", + "blacklist_desc": "Stavke crne liste isključene su iz upita. Imajte na umu da su to regularni izrazi i da nisu osjetljivi na velika i mala slova. Određeni znakovi moraju se izbjeći obrnutom kosom crtom: {chars_require_escape}", + "blacklist_label": "Crna Lista", + "errors": { + "blacklist_duplicate": "Duplikat stavke crne liste" + }, + "mark_organized_desc": "Odmah označi scenu kao Organiziranu nakon što je tipka Spremi stisnuta.", + "mark_organized_label": "Označi kao organizirano pri spremanju", + "query_mode_auto": "Automatski", + "query_mode_auto_desc": "Koristi metapodatke ako postoje, ili ime datoteke", + "query_mode_dir": "Dir", + "query_mode_dir_desc": "Koristi samo nadređeni direktorij video datoteke", + "query_mode_filename": "Ime datoteke", + "query_mode_filename_desc": "Koristi samo ime datoteke", + "query_mode_label": "Query Mod", + "query_mode_metadata": "Metapodaci", + "query_mode_metadata_desc": "Koristi samo metapodatke", + "query_mode_path": "Putanja", + "query_mode_path_desc": "Koristi cijelu putanju datoteke", + "set_cover_desc": "Zamijeni sliku naslovnice scene ako je pronađena.", + "set_cover_label": "Postavi sliku naslovnice scene", + "set_tag_desc": "Priložite oznake sceni, bilo prepisivanjem ili spajanjem s postojećim oznakama na sceni.", + "set_tag_label": "Postavi oznake", + "source": "Izvor" + }, + "noun_query": "Upit", + "results": { + "duration_unknown": "Trajanje nepoznato", + "fp_matches": "Trajanje se podudara", + "fp_matches_multi": "Trajanje odgovara otiscima {matchCount}/{durationsLength}", + "hash_matches": "{hash_type} se podudara", + "match_failed_already_tagged": "Scena već označena", + "match_failed_no_result": "Nisu pronađeni rezultati", + "match_success": "Scena uspješno označena", + "unnamed": "Neimenovano" + }, + "verb_matched": "Podudara se", + "verb_toggle_unmatched": "{toggle} neusklađene scene" + }, + "config": { + "about": { + "build_hash": "Izradi hash:", + "build_time": "Vrijeme izrade:", + "check_for_new_version": "Provjeri za novu verziju", + "latest_version": "Zadnja Verzija", + "latest_version_build_hash": "Izrađen Hash Zadnje Verzije:", + "new_version_notice": "[NOVO]", + "release_date": "Datum izdanja:", + "stash_discord": "Pridruži se našem {url} kanalu", + "stash_home": "Stash početna stranica na {url}", + "stash_open_collective": "Podrži nas kroz {url}", + "stash_wiki": "Stash {url} stranica", + "version": "Verzija" + }, + "advanced_mode": "Napredan Način", + "application_paths": { + "heading": "Putanje Aplikacije" + }, + "categories": { + "about": "O nama", + "changelog": "Zapisnik promjena", + "interface": "Sučelje", + "logs": "Zapisnici", + "metadata_providers": "Pružatelji Metapodataka", + "plugins": "Dodaci", + "security": "Sigurnost", + "services": "Usluge", + "system": "Sistem", + "tasks": "Zadaci", + "tools": "Alati" + }, + "dlna": { + "allow_temp_ip": "Dopusti {tempIP}", + "allowed_ip_addresses": "Dopuštene IP adrese", + "allowed_ip_temporarily": "Dopušten IP privremeno", + "default_ip_whitelist": "Zadana bijela lista IP adresa", + "default_ip_whitelist_desc": "Zadane IP adrese omogućuju pristup DLNA. Koristi {wildcard} za dopuštanje svih IP adresa.", + "disabled_dlna_temporarily": "Onemogućen DLNA privremeno", + "disallowed_ip": "Nedopušen IP", + "enabled_by_default": "Omogućeno prema zadanim postavkama", + "enabled_dlna_temporarily": "Omogućen DLNA privremeno", + "network_interfaces": "Sučelja", + "network_interfaces_desc": "Sučelja na kojima će se izložiti DLNA poslužitelj. Prazan popis rezultira pokretanjem na svim sučeljima. Zahtijeva ponovno pokretanje DLNA nakon promjene.", + "recent_ip_addresses": "Nedavne IP adrese", + "server_display_name": "Prikazni Naziv Poslužitelja", + "server_display_name_desc": "Prikazni naziv za DLNA poslužitelj. Zadano je {server_name} ako je prazno.", + "server_port": "Port Poslužitelja", + "server_port_desc": "Port na kojem će se pokretati DLNA poslužitelj. Zahtijeva ponovno pokretanje DLNA nakon promjene.", + "successfully_cancelled_temporary_behaviour": "Uspješno otkazano privremeno ponašanje", + "until_restart": "do ponovnog pokretanja", + "video_sort_order": "Zadani Redoslijed Sortiranja Videozapisa" + }, + "general": { + "auth": { + "api_key": "API Ključ", + "api_key_desc": "API ključ za vanjske sustave. Potreban je samo kada je konfigurirano korisničko ime/lozinka. Korisničko ime mora biti spremljeno prije generiranja API ključa.", + "authentication": "Autentifikacija", + "clear_api_key": "Obriši API ključ", + "credentials": { + "description": "Vjerodajnice za ograničavanje pristupa zalihama.", + "heading": "Vjerodajnice" + }, + "generate_api_key": "Generiraj API ključ", + "log_file": "Datoteka zapisnika" + } + } } } diff --git a/ui/v2.5/src/locales/hu-HU.json b/ui/v2.5/src/locales/hu-HU.json index be47f77a9..e7b5c15f8 100644 --- a/ui/v2.5/src/locales/hu-HU.json +++ b/ui/v2.5/src/locales/hu-HU.json @@ -153,7 +153,6 @@ "query_mode_metadata": "Metaadat", "query_mode_path": "Elérési út", "source": "Forrás", - "show_male_label": "Férfi előadók mutatása", "active_instance": "Aktív stash-box példány:", "blacklist_desc": "A tiltólistás elemek nem látszódnak a lekérésekben. Fontos tudni, hogy a regex kifejezések érzékenyek a kis-és nagybetűkre. Bizonyos karakterek után kötelező a \\ : {chars_require_escape}", "mark_organized_desc": "Azonnal megjelöli a jelenetet \"Rendezettnek\", amint a \"Mentés\" gomb lenyomásra került.", @@ -503,7 +502,6 @@ "name": "Név", "new": "Új", "none": "Nincs", - "o_counter": "O-Számláló", "operations": "Műveletek", "organized": "Rendezve", "pagination": { diff --git a/ui/v2.5/src/locales/id-ID.json b/ui/v2.5/src/locales/id-ID.json index a7327508a..b4edec6d5 100644 --- a/ui/v2.5/src/locales/id-ID.json +++ b/ui/v2.5/src/locales/id-ID.json @@ -3,7 +3,7 @@ "welcome_to_stash": "Selamat datang di Stash", "paths": { "where_is_your_porn_located": "Di mana lokasi porno Anda?", - "description": "Selanjutnya, kita perlu menentukan di mana lokasi koleksi porno Anda, dan di mana menyimpan pangkalan data Stash, berkas yang dihasilkan, dan berkas tembolok. Pengaturan ini dapat diubah nanti jika diperlukan." + "description": "Selanjutnya, kita perlu menentukan di mana lokasi koleksi bokep Anda, dan di mana menyimpan pangkalan data Stash, berkas yang dihasilkan, dan berkas tembolok. Pengaturan ini dapat diubah nanti jika diperlukan." }, "success": { "thanks_for_trying_stash": "Terima kasih sudah mencoba Stash!", @@ -29,7 +29,7 @@ "continue": "Lanjutkan", "copy_to_clipboard": "Salin ke papan klip", "create": "Buat", - "create_chapters": "Buat Bab", + "create_chapters": "Buat Bagian", "create_entity": "Buat {entityType}", "create_marker": "Buat Tanda", "create_parent_studio": "Buat studio induk", @@ -147,7 +147,25 @@ "use_default": "Gunakan bawaan", "view_random": "Lihat Acak", "unset": "Tidak disetel", - "view_history": "Lihat histori" + "view_history": "Lihat histori", + "reset_cover": "Pulihkan Sampul Bawaan", + "set_cover": "Atur sebagai Sampul", + "reset_resume_time": "Atur ulang waktu lanjut", + "reset_play_duration": "Atur ulang durasi putar", + "add_sub_groups": "Tambah Subgrup", + "remove_from_containing_group": "Hapus dari Grup", + "load": "Muat", + "load_filter": "Muat filter", + "play": "Mainkan", + "show_results": "Lihat hasil", + "show_count_results": "Lihat {count} hasil", + "sidebar": { + "close": "Tutup bilah samping", + "open": "Buka bilah samping", + "toggle": "Alihkan bilah samping" + }, + "add_stash_id": "Tambah ID Stash", + "create_new": "Buat Baru" }, "circumcised_types": { "CUT": "Disunat", @@ -216,15 +234,20 @@ "query_mode_filename_desc": "Hanya menggunakan nama berkas", "query_mode_label": "Mode Kueri", "query_mode_metadata_desc": "Hanya menggunakan metadata", - "query_mode_path": "Jalur", + "query_mode_path": "Lokasi", "query_mode_path_desc": "Menggunakan seluruh jalur berkas", "set_cover_desc": "Ganti gambar adegan jika cover ditemukan.", "set_cover_label": "Tetapkan gambar cover adegan", "set_tag_label": "Tetapkan tag", "set_tag_desc": "Lampirkan tag ke adegan, baik dengan menimpa atau menggabungkan dengan tag yang sudah ada.", - "show_male_desc": "Baeralih apakah pemain pria dapat diberi tag.", - "show_male_label": "Tampilkan pemain pria", - "source": "Sumber" + "source": "Sumber", + "errors": { + "blacklist_duplicate": "Duplikat item daftar hitam" + }, + "performer_genders": { + "heading": "Kelamin pemain", + "description": "Pemain dengan kelamin ini akan ditampilkan ketika memberi tanda pada adegan." + } }, "results": { "duration_unknown": "Durasi tidak diketahui", @@ -280,7 +303,9 @@ "enabled_dlna_temporarily": "DLNA yang diaktifkan sementara", "network_interfaces_desc": "Antarmuka untuk mengekspos server DLNA. Daftar kosong menghasilkan berjalannya di semua antarmuka. Dibutuhkan mulai ulang DLNA setelah perubahan.", "recent_ip_addresses": "Alamat IP terbaru", - "server_display_name": "Nama Tampilan Server" + "server_display_name": "Nama Tampilan Server", + "server_port_desc": "Port untuk menjalankan server DLNA. Harus memulai ulang DLNA setelah diganti.", + "server_port": "Port Server" }, "general": { "logging": "Pencatatan", @@ -322,7 +347,8 @@ "gallery_ext_desc": "Daftar ekstensi berkas yang dibatasi koma yang akan diidentifikasi sebagai berkas zip galeri.", "gallery_ext_head": "Ekstensi zip galeri", "python_path": { - "heading": "Jalur Eksekusi Python" + "heading": "Jalur Eksekusi Python", + "description": "Lokasi executable Python (bukan hanya folder). Digunakan untuk skrip penggali data dan plugin. Jika kosong, Python akan diambil dari environment" }, "scraper_user_agent": "Agen Pengguna Penggali", "db_path_head": "Jalur Pangkalan Data", @@ -359,10 +385,72 @@ "blobs_path": { "description": "Dimana pada sistem berkas untuk menyimpan data biner. Hanya berlaku saat menggunakan jenis penyimpanan blob Sistem Berkas. PERINGATAN: mengubah ini memerlukan pemindahan data yang ada secara manual.", "heading": "Jalur sistem berkas data biner" - } + }, + "check_for_insecure_certificates": "Cek untuk sertifikat tidak aman", + "cache_location": "Direktori lokasi cache. Harus diisi jika streaming menggunakan HLS (seperti pada perangkat Apple) atau DASH.", + "calculate_md5_and_ohash_label": "Kalkulasi MD5 untuk video", + "cache_path_head": "Lokasi Cache", + "calculate_md5_and_ohash_desc": "Kalkulasi checksum MD5 untuk tambahan oshash. Jika diaktifkan akan memperlambat proses scan awal. Hash penamaan file harus diatur menjadi oshash untuk menonaktifkan kalkulasi MD5.", + "check_for_insecure_certificates_desc": "Beberapa situs menggunakan sertifikat ssl yang tidak aman. Jika dinonaktifkan, scraper akan mengabaikan sertifikat yang tidak aman dan akan melanjutkan scraping. Jika anda mendapatkan error saat scraping, nonaktifkan ini.", + "chrome_cdp_path": "Lokasi Chrome CDP", + "chrome_cdp_path_desc": "Lokasi file eksekutabel Chrome, atau alamat remote (dimulai dengan http:// atau https://, sebagai contoh http://localhost:9222/json/version) sebuah instansi Chrome.", + "ffmpeg": { + "live_transcode": { + "output_args": { + "desc": "Lanjutan: Argumen tambahan untuk ditambahkan pada ffmpeg sebelum isian output saat mentranscode video secara langsung.", + "heading": "Argumen untuk Output Transcode Langsung pada FFmpeg" + }, + "input_args": { + "desc": "Lanjutan: Argumen tambahan untuk diteruskan ke ffmpeg sebelum field input saat melakukan transcoding video langsung.", + "heading": "FFmpeg Live Transcode Input Args" + } + }, + "transcode": { + "input_args": { + "desc": "Lanjutan: Argumen tambahan untuk ditambahkann pada ffmpeg sebelum isian saat menghasilkan video.", + "heading": "Argumen Input Transcode FFmpeg" + }, + "output_args": { + "desc": "Lanjutan: Argumen tambahan untuk ditambahkan pada ffmpeg sebelum output saat menghasilkan video.", + "heading": "Argumen Output Transcode FFmpeg" + } + }, + "download_ffmpeg": { + "description": "Mengunduh FFmpeg ke dalam direktori konfigurasi dan mengosongkan lokasi ffmpeg serta ffprobe untuk diambil dari direktori konfigurasi.", + "heading": "Unduh FFmpeg" + }, + "ffmpeg_path": { + "description": "Lokasi ke executable ffmpeg (bukan hanya folder). Jika kosong, ffmpeg akan diambil dari environment melalui $PATH, direktori konfigurasi, atau dari $HOME/.stash", + "heading": "Lokasi Executable FFmpeg" + }, + "ffprobe_path": { + "description": "Lokasi executable ffprobe (bukan hanya folder). Jika kosong, ffprobe akan diambil dari environment melalui $PATH, direktori konfigurasi, atau dari $HOME/.stash", + "heading": "Lokasi Executable FFprobe" + }, + "hardware_acceleration": { + "desc": "Menggunakan perangkat keras yang tersedia untuk melakukan encoding video dalam transcoding langsung.", + "heading": "Encoding perangkat keras FFmpeg" + } + }, + "create_galleries_from_folders_label": "Buat galeri dari folder yang berisi gambar", + "directory_locations_to_your_content": "Lokasi direktori konten Anda", + "create_galleries_from_folders_desc": "Jika true, secara bawaan membuat galeri dari folder yang berisi gambar. Buat sebuah file bernama .forcegallery atau .nogallery di dalam folder untuk memaksa/mencegah hal ini.", + "excluded_image_gallery_patterns_desc": "Regexp file gambar dan galeri/lokasi yang akan dikecualikan dari Pemindaian dan ditambahkan ke Pembersihan", + "excluded_image_gallery_patterns_head": "Pola Gambar/Galeri yang Dikecualikan", + "excluded_video_patterns_desc": "Regexp file video/lokasi yang akan dikecualikan dari Pemindaian dan ditambahkan ke Pembersihan", + "excluded_video_patterns_head": "Pola Video yang Dikecualikan", + "generated_file_naming_hash_desc": "Gunakan MD5 atau oshash untuk penamaan file yang dihasilkan. Mengubah ini memerlukan semua adegan memiliki nilai MD5/oshash yang sesuai. Setelah mengubah nilai ini, file yang sudah dihasilkan perlu dimigrasikan atau dibuat ulang. Lihat halaman Tugas untuk migrasi.", + "image_ext_desc": "Daftar ekstensi file yang dipisahkan koma dan akan dikenali sebagai gambar.", + "plugins_path": { + "description": "Lokasi direktori file konfigurasi plugin", + "heading": "Lokasi Plugin" + }, + "video_ext_desc": "Daftar ekstensi file yang dipisahkan koma dan akan dikenali sebagai video." }, "library": { - "exclusions": "Pengecualian" + "exclusions": "Pengecualian", + "gallery_and_image_options": "Opsi Galeri dan Gambar", + "media_content_extensions": "Ekstensi konten media" }, "tasks": { "identify": { @@ -387,9 +475,11 @@ "markers": "Pratinjau Penanda", "image_thumbnails": "Keluku Gambar", "image_thumbnails_desc": "Keluku dan klip gambar", - "previews": "Pratinjau Adegan" + "previews": "Pratinjau Adegan", + "previews_desc": "Pratinjau adegan dan keluku" }, - "added_job_to_queue": "{operation_name} ditambahkan ke antrean tugas" + "added_job_to_queue": "{operation_name} ditambahkan ke antrean tugas", + "data_management": "Manajemen data" }, "ui": { "editing": { @@ -456,14 +546,16 @@ "supported_types": "Tipe yang didukung", "search_by_name": "Cari berdasarkan nama", "available_scrapers": "Penggali Tersedia", - "entity_scrapers": "Penggali {entityType}" + "entity_scrapers": "Penggali {entityType}", + "excluded_tag_patterns_head": "Pola Tag Terkecualikan" }, "stashbox": { "endpoint": "Titik akhir", "name": "Nama", "graphql_endpoint": "Titik akhir GraphQL", "title": "Titik Akhir Stash-box", - "api_key": "Kunci API" + "api_key": "Kunci API", + "max_requests_per_minute": "Maks permintaan per menit" }, "system": { "transcoding": "Transkode" @@ -472,6 +564,13 @@ "scene_filename_parser": { "filename": "Nama berkas" } + }, + "logs": { + "log_level": "Level Log" + }, + "plugins": { + "available_plugins": "Plugin Tersedia", + "installed_plugins": "Plugin Terinstal" } }, "criterion": { @@ -499,7 +598,8 @@ "options": "Opsi", "scroll_mode": { "zoom": "Perbesar" - } + }, + "page_header": "Halaman {page} / {total}" }, "merge": { "destination": "Destinasi", @@ -514,7 +614,9 @@ "video_previews": "Pratinjau" }, "scrape_results_existing": "Yang sudah ada", - "scrape_results_scraped": "Digali" + "scrape_results_scraped": "Digali", + "performers_found": "{count} pemain ditemukan", + "dont_show_until_updated": "Sembunyikan hingga pembaruan selanjutnya" }, "effect_filters": { "brightness": "Kecerahan", @@ -548,13 +650,16 @@ "uninstall": "Copot pemasangan", "update": "Perbarui", "version": "Versi", - "unknown": "" + "unknown": "", + "installed_version": "Versi Terpasang", + "latest_version": "Versi Terkini" }, "pagination": { "next": "Berikutnya", "previous": "Sebelumnya", "last": "Terakhir", - "first": "Pertama" + "first": "Pertama", + "current_total": "{current} dari {total}" }, "performer": "Pemain", "performers": "Pemain", @@ -650,15 +755,44 @@ }, "instagram": "Instagram", "interactive": "Interaktif", - "library": "Pustaka", + "library": "Koleksi", "loading": { "generic": "Memuat…" }, "tag": "Tag", - "o_counter": "Penghitung Crot", "operations": "Operasi", "organized": "Terorganisir", "orientation": "Orientasi", "all": "semua", - "ascending": "Urut naik" + "ascending": "Urut naik", + "age_on_date": "{age} saat produksi", + "performer_age": "Umur Pemain", + "performer_count": "Jumlah Pemain", + "performer_favorite": "Pemain Difavorit", + "performer_image": "Foto Pemain", + "performer_tagger": { + "add_new_performers": "Tambah Pemain Baru" + }, + "part_of": "Bagian dari {parent}", + "cover_image": "Foto Sampul", + "death_date": "Tanggal Kematian", + "death_year": "Tahun Kematian", + "last_o_at": "Terakhir Crot Pada", + "last_played_at": "Terakhir Dimainkan Pada", + "login": { + "username": "Nama Pengguna", + "password": "Kata Sandi", + "login": "Masuk", + "invalid_credentials": "Nama pengguna atau kata sandi salah" + }, + "marker_count": "Jumlah Penanda", + "media_info": { + "o_count": "Jumlah Crot", + "performer_card": { + "age": "{age} {years_old}", + "age_context": "{age} {years_old} saat produksi" + }, + "phash": "PHash" + }, + "o_count": "Jumlah Crot" } diff --git a/ui/v2.5/src/locales/index.ts b/ui/v2.5/src/locales/index.ts index 86c1a607e..0e699b8f7 100644 --- a/ui/v2.5/src/locales/index.ts +++ b/ui/v2.5/src/locales/index.ts @@ -2,6 +2,7 @@ import Countries from "i18n-iso-countries"; export const localeCountries = { af: () => import("i18n-iso-countries/langs/af.json"), + bg: () => import("i18n-iso-countries/langs/bg.json"), bn: () => import("i18n-iso-countries/langs/bn.json"), ca: () => import("i18n-iso-countries/langs/ca.json"), cs: () => import("i18n-iso-countries/langs/cs.json"), @@ -20,6 +21,7 @@ export const localeCountries = { it: () => import("i18n-iso-countries/langs/it.json"), ja: () => import("i18n-iso-countries/langs/ja.json"), ko: () => import("i18n-iso-countries/langs/ko.json"), + lt: () => import("i18n-iso-countries/langs/lt.json"), lv: () => import("i18n-iso-countries/langs/lv.json"), nb: () => import("i18n-iso-countries/langs/nb.json"), nl: () => import("i18n-iso-countries/langs/nl.json"), @@ -32,6 +34,7 @@ export const localeCountries = { sv: () => import("i18n-iso-countries/langs/sv.json"), th: () => import("i18n-iso-countries/langs/th.json"), tr: () => import("i18n-iso-countries/langs/tr.json"), + ur: () => import("i18n-iso-countries/langs/ur.json"), uk: () => import("i18n-iso-countries/langs/uk.json"), vi: () => import("i18n-iso-countries/langs/vi.json"), zh: () => import("i18n-iso-countries/langs/zh.json"), @@ -53,6 +56,7 @@ export async function registerCountry(locale: string) { export const localeLoader = { afZA: () => import("./af-ZA.json"), + bgBG: () => import("./bg-BG.json"), bnBD: () => import("./bn-BD.json"), caES: () => import("./ca-ES.json"), csCZ: () => import("./cs-CZ.json"), @@ -72,6 +76,7 @@ export const localeLoader = { itIT: () => import("./it-IT.json"), jaJP: () => import("./ja-JP.json"), koKR: () => import("./ko-KR.json"), + ltLT: () => import("./lt-LT.json"), lvLV: () => import("./lv-LV.json"), nbNO: () => import("./nb-NO.json"), // neNP: () => import("./ne-NP.json"), @@ -85,6 +90,7 @@ export const localeLoader = { svSE: () => import("./sv-SE.json"), thTH: () => import("./th-TH.json"), trTR: () => import("./tr-TR.json"), + urPK: () => import("./ur-PK.json"), ukUA: () => import("./uk-UA.json"), viVN: () => import("./vi-VN.json"), zhCN: () => import("./zh-CN.json"), diff --git a/ui/v2.5/src/locales/it-IT.json b/ui/v2.5/src/locales/it-IT.json index 98269f6d8..51347e993 100644 --- a/ui/v2.5/src/locales/it-IT.json +++ b/ui/v2.5/src/locales/it-IT.json @@ -135,7 +135,8 @@ "optimise_database": "Ottimizza il Database", "reload": "Ricarica", "remove_date": "Rimuovi la data", - "view_history": "Visualizza cronologia" + "view_history": "Visualizza cronologia", + "add_sub_groups": "Aggiungi Sottogruppi" }, "actions_name": "Azioni", "age": "Età", @@ -176,8 +177,6 @@ "set_cover_label": "Imposta la copertina della scena", "set_tag_desc": "Attacca tag alla scena, sovrascrivendoli o unendoli a quelli esistenti sulla scena.", "set_tag_label": "Imposta i tags", - "show_male_desc": "Attiva/Disattiva l'opzione di taggare attori maschi.", - "show_male_label": "Mostra attori maschi", "source": "Sorgente", "mark_organized_desc": "Contrassegna immediatamente la scena come \"ordinata\" quando si clicca sul pulsante Save.", "mark_organized_label": "Contrassegna come \"ordinato\" al salvataggio" @@ -744,7 +743,6 @@ "destination": "Destinazione", "source": "Origine" }, - "overwrite_filter_confirm": "Sei sicuro di voler sovrascrivere le esistenti query salvate {entityName}?", "reassign_entity_title": "{count, plural, one {Riassegna {singularEntity}} other {Riassegna {pluralEntity}}}", "reassign_files": { "destination": "Riassegna a" @@ -937,7 +935,6 @@ "name": "Nome", "new": "Nuovo", "none": "Nessuno/a", - "o_counter": "Contatore-O", "operations": "Operazioni", "organized": "Ordinato", "pagination": { @@ -978,7 +975,6 @@ "no_results_found": "Nessun risultato trovato.", "number_of_performers_will_be_processed": "{performer_count} attori saranno processati", "performer_already_tagged": "Attore/Attrice già taggato/a", - "performer_names_separated_by_comma": "Nome attore/attrice separato da virgola", "performer_selection": "Selezione attore/attrice", "performer_successfully_tagged": "Attore/Attrice taggato/a con successo:", "query_all_performers_in_the_database": "Tutti gli attori nel database", diff --git a/ui/v2.5/src/locales/ja-JP.json b/ui/v2.5/src/locales/ja-JP.json index c8e95e595..3ee3597eb 100644 --- a/ui/v2.5/src/locales/ja-JP.json +++ b/ui/v2.5/src/locales/ja-JP.json @@ -141,7 +141,16 @@ "set_cover": "カバーをセット", "view_history": "履歴を表示する", "reset_resume_time": "再開時間をリセットする", - "reset_cover": "標準カバーに復元" + "reset_cover": "標準カバーに復元", + "sidebar": { + "close": "サイドバーを閉じる", + "open": "サイドバーを開く", + "toggle": "サイドバーを切り替え" + }, + "play": "再生", + "show_results": "結果を表示", + "show_count_results": "{count}件の結果を表示", + "load": "読み込み" }, "actions_name": "操作", "age": "年齢", @@ -176,8 +185,6 @@ "set_cover_label": "シーンのカバー画像を設定", "set_tag_desc": "シーン上の既存のタグを上書きまたはマージすることで、シーンにタグを付与します。", "set_tag_label": "タグを設定", - "show_male_desc": "男優をタグ付けできるかを切り替えます。", - "show_male_label": "男優を表示", "source": "ソース", "mark_organized_desc": "保存ボタンをクリック後に、すぐにシーンが「分類済み」になります。", "mark_organized_label": "分類済みにして保存", @@ -435,7 +442,8 @@ "endpoint": "エンドポイント", "graphql_endpoint": "GraphQL エンドポイント", "name": "名前", - "title": "Stash-box エンドポイント" + "title": "Stash-box エンドポイント", + "max_requests_per_minute": "1分あたりの最大リクエスト数" }, "system": { "transcoding": "トランスコード" @@ -718,8 +726,11 @@ "disable_mobile_media_auto_rotate": "モバイル機器でフル画面再生時の画面回転を無効化", "enable_chromecast": "クロームキャスト機能の有効化", "vr_tag": { - "heading": "VRタッグ" - } + "heading": "VRタッグ", + "description": "VRボタンはこのタグがついたシーンにのみ表示されます。" + }, + "show_ab_loop_controls": "ABループプラグインのコントロールを表示", + "show_range_markers": "範囲マーカーを表示" } }, "scene_wall": { @@ -779,6 +790,9 @@ "direction": "方向", "heading": "画像ウォール", "margin": "マージン (px単位)" + }, + "performer_list": { + "heading": "AV女優・男優リスト" } }, "advanced_mode": "高度なモード" @@ -796,7 +810,7 @@ }, "country": "国", "cover_image": "カバー画像", - "created_at": "作成者:", + "created_at": "作成日", "criterion": { "greater_than": "より大きい", "less_than": "より小さい", @@ -862,7 +876,8 @@ "label": "スクロールモード", "pan_y": "Yにパン", "zoom": "拡大" - } + }, + "page_header": "ページ {page} / {total}" }, "merge": { "destination": "宛先", @@ -873,7 +888,6 @@ "destination": "場所", "source": "ソース" }, - "overwrite_filter_confirm": "本当に保存されているクエリ「{entityName}」を上書きしてもよろしいですか?", "reassign_entity_title": "{count, plural, one {{singularEntity}を再割り当て} other {{pluralEntity}を再割り当て}}", "reassign_files": { "destination": "次に再割り当て:" @@ -911,7 +925,10 @@ "transcodes": "トランスコード", "transcodes_tooltip": "サポートされていない動画フォーマットをMP4に変換します", "video_previews": "プレビュー", - "video_previews_tooltip": "シーンにマウスカーソルを置いた時に再生されるビデオプレビュー" + "video_previews_tooltip": "シーンにマウスカーソルを置いた時に再生されるビデオプレビュー", + "clip_previews": "画像のプレビュー", + "covers": "シーンカバー", + "image_thumbnails": "サムネ画像" }, "scenes_found": "{count}シーンが見つかりました", "scrape_entity_query": "{entity_type}スクレイプクエリ", @@ -919,7 +936,9 @@ "scrape_results_existing": "存在します", "scrape_results_scraped": "スクレイプ済み", "set_image_url_title": "画像URL", - "unsaved_changes": "変更が保存されていません。本当に移動してよろしいですか?" + "unsaved_changes": "変更が保存されていません。本当に移動してよろしいですか?", + "clear_play_history_confirm": "再生履歴を本当に削除しますか?", + "performers_found": "{count}人の出演者が見つかりました" }, "dimensions": "寸法", "director": "監督", @@ -1059,7 +1078,6 @@ "name": "名前", "new": "新規作成", "none": "なし", - "o_counter": "発射カウンター", "operations": "オペレーション", "organized": "分類済み", "pagination": { @@ -1100,7 +1118,6 @@ "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": "データベース内の全出演者", @@ -1299,6 +1316,26 @@ "date_format": "YYYY -MM-DD", "datetime_format": "YYYY-MM-DD HH:MM", "criterion_modifier_values": { - "none": "ない" - } + "none": "ない", + "only": "のみ" + }, + "connection_monitor": { + "websocket_connection_failed": "WebSocket接続ができません:詳細はブラウザのコンソールを確認してください" + }, + "custom_fields": { + "field": "フィールド", + "title": "カスタムフィールド", + "value": "値" + }, + "distance": "距離", + "age_on_date": "撮影時の年齢 {age}歳", + "containing_group": "含まれるグループ", + "penis_length": "ペニスの長さ", + "parent_studio": "親スタジオ", + "sort_name": "ソート用の名称", + "groups": "グループ", + "include_sub_groups": "サブグループを含める", + "sub_groups": "サブグループ", + "history": "履歴", + "group_scene_number": "シーン番号" } diff --git a/ui/v2.5/src/locales/ko-KR.json b/ui/v2.5/src/locales/ko-KR.json index 60f942e82..566e4806d 100644 --- a/ui/v2.5/src/locales/ko-KR.json +++ b/ui/v2.5/src/locales/ko-KR.json @@ -45,13 +45,13 @@ "from_url": "URL로 불러오기…", "full_export": "전부 내보내기", "full_import": "전부 불러오기", - "generate": "만들기", - "generate_thumb_default": "기본 썸네일 만들기", - "generate_thumb_from_current": "현재 화면으로 썸네일 만들기", + "generate": "생성", + "generate_thumb_default": "기본 썸네일 생성", + "generate_thumb_from_current": "현재 화면으로 썸네일 생성", "hash_migration": "해쉬 값 마이그레이션", "hide": "숨기기", "hide_configuration": "설정 숨기기", - "identify": "인증", + "identify": "식별", "ignore": "무시", "import": "불러오기…", "import_from_file": "파일 불러오기", @@ -65,7 +65,7 @@ "next_action": "다음", "not_running": "실행 중이 아님", "open_in_external_player": "외부 플레이어에서 열기", - "open_random": "랜덤 배우 정보 열기", + "open_random": "랜덤 열기", "overwrite": "덮어쓰기", "play_random": "랜덤 영상 재생", "play_selected": "선택된 영상 재생", @@ -104,7 +104,7 @@ "show": "보여주기", "show_configuration": "설정 보여주기", "skip": "건너뛰기", - "split": "나누기", + "split": "분할", "stop": "정지", "submit": "제출", "submit_stash_box": "Stash-Box에 제출하기", @@ -141,7 +141,18 @@ "reset_play_duration": "재생 시간 초기화", "reset_resume_time": "마지막 재생 위치 초기화", "add_sub_groups": "서브그룹 추가", - "remove_from_containing_group": "그룹에서 제거" + "remove_from_containing_group": "그룹에서 제거", + "sidebar": { + "close": "사이드바 닫기", + "open": "사이드바 열기", + "toggle": "사이드바 토글" + }, + "play": "재생", + "show_results": "결과 표시", + "show_count_results": "{count}개 결과 표시", + "load": "불러오기", + "load_filter": "필터 불러오기", + "add_stash_id": "Stash ID 추가" }, "actions_name": "액션", "age": "나이", @@ -187,11 +198,16 @@ "set_cover_label": "영상 커버 이미지 설정", "set_tag_desc": "영상에 이미 존재하는 태그들을 덮어쓰거나 병합함으로써 태그를 영상에 추가합니다.", "set_tag_label": "태그 설정", - "show_male_desc": "남성 배우들의 태그 가능 여부 설정을 켜거나 끕니다.", - "show_male_label": "남성 배우 보여주기", "source": "출처", "mark_organized_desc": "저장 버튼을 클릭하면 곧바로 영상을 '정리됨' 상태로 만듭니다.", - "mark_organized_label": "저장 시 '정리됨' 상태로 만들기" + "mark_organized_label": "저장 시 '정리됨' 상태로 만들기", + "errors": { + "blacklist_duplicate": "블랙리스트 항목이 중복되었습니다" + }, + "performer_genders": { + "heading": "배우 성별", + "description": "영상을 태그할 때, 이 성별에 해당하는 배우들이 표시됩니다." + } }, "noun_query": "쿼리", "results": { @@ -201,7 +217,7 @@ "fp_matches": "영상 길이가 일치함", "fp_matches_multi": "영상 길이가 {durationsLength}개 중 {matchCount}개의 식별값과 일치합니다", "hash_matches": "{hash_type}이 일치함", - "match_failed_already_tagged": "이미 태깅된 영상", + "match_failed_already_tagged": "이미 태그된 영상", "match_failed_no_result": "결과 없음", "match_success": "영상 태깅 성공", "phash_matches": "{count}개의 PHash가 일치함", @@ -291,7 +307,9 @@ "password_desc": "Stash에 접속하기 위한 비밀번호입니다. 로그인 과정을 생략하려면 빈 칸으로 두십시오", "stash-box_integration": "Stash-box 통합", "username": "아이디", - "username_desc": "Stash에 접속하기 위한 아이디입니다. 로그인을 생략하려면 빈 칸으로 두십시오" + "username_desc": "Stash에 접속하기 위한 아이디입니다. 로그인을 생략하려면 빈 칸으로 두십시오", + "log_file_max_size": "최대 로그 크기", + "log_file_max_size_desc": "압축 전 로그 파일의 최대 크기(MB)입니다. 0MB를 입력하면 비활성화됩니다. 설정 변경 후 재시작해야 합니다." }, "backup_directory_path": { "description": "SQLite 데이터베이스 백업 파일을 위한 폴더 경로", @@ -306,7 +324,7 @@ "heading": "바이너리 데이터 저장 타입" }, "cache_location": "캐시 폴더 경로입니다. HLS(애플 기기 등)이나 DASH로 스트리밍할 때 필요합니다.", - "cache_path_head": "캐쉬 경로", + "cache_path_head": "캐시 경로", "calculate_md5_and_ohash_desc": "oshash 외에 MD5 체크섬도 계산합니다. 활성화하면 초기 스캔을 더 느리게 만들 것입니다. MD5 계산을 사용하지 않으려면 파일 이름 해쉬를 oshash로 설정해야 합니다.", "calculate_md5_and_ohash_label": "비디오 MD5 계산하기", "check_for_insecure_certificates": "안전하지 않은 자격증명 검사", @@ -407,6 +425,10 @@ "plugins_path": { "description": "플러그인 설정 파일의 폴더 위치", "heading": "플러그인 경로" + }, + "delete_trash_path": { + "description": "삭제된 파일들이 영구 삭제되는 대신에 옮겨지게 될 경로입니다. 파일들을 영구 삭제하려면 빈 칸으로 두십시오.", + "heading": "휴지통 경로" } }, "library": { @@ -419,7 +441,7 @@ }, "plugins": { "hooks": "후크", - "triggers_on": "트리거 켜기", + "triggers_on": "작동 조건", "available_plugins": "사용 가능한 플러그인", "installed_plugins": "설치된 플러그인" }, @@ -443,7 +465,9 @@ "endpoint": "엔드포인트", "graphql_endpoint": "GraphQL 엔드포인트", "name": "이름", - "title": "Stash-box 엔드포인트" + "title": "Stash-box 엔드포인트", + "max_requests_per_minute": "분당 최대 요청 수", + "max_requests_per_minute_description": "0으로 설정할 경우 기본값({defaultValue})을 사용합니다" }, "system": { "transcoding": "트랜스코딩" @@ -472,9 +496,9 @@ "generating_from_paths": "다음 경로에서 영상 생성 중", "generating_scenes": "{num}개의 {scene} 생성 중" }, - "generate_clip_previews_during_scan": "이미지 클립 미리보기 생성하기", + "generate_clip_previews_during_scan": "이미지 클립 미리보기 생성", "generate_desc": "이미지, 스프라이트, 비디오, vtt 등 파일을 생성합니다.", - "generate_phashes_during_scan": "컨텐츠 해시 값 생성", + "generate_phashes_during_scan": "해쉬 값 생성", "generate_phashes_during_scan_tooltip": "중복된 파일 확인과 영상 식별에 사용됩니다.", "generate_previews_during_scan": "움직이는 이미지 미리보기 생성", "generate_previews_during_scan_tooltip": "애니메이션(webp) 미리보기 또한 생성합니다. 영상/마커 월의 미리보기 유형이 '애니메이션 이미지'로 설정된 경우에만 필요합니다. '애니메이션 미리보기'는 '비디오 미리보기'보다 CPU를 덜 사용하지만, '비디오 미리보기'에 추가적으로 '애니메이션 미리보기'가 생성되기 때문에 파일 크기가 커집니다.", @@ -569,7 +593,9 @@ "whitespace_chars": "공백 문자", "whitespace_chars_desc": "이 문자들은 제목에서 공백으로 대체됩니다" }, - "scene_tools": "영상 도구" + "scene_tools": "영상 도구", + "heading": "도구", + "graphql_playground": "GraphQL 플레이그라운드" }, "ui": { "abbreviate_counters": { @@ -609,12 +635,12 @@ }, "editing": { "disable_dropdown_create": { - "description": "드랍다운 메뉴에서 새로운 오브젝트를 추가할 수 없도록 합니다", - "heading": "드랍다운 메뉴 비활성화" + "description": "선택창에서 새로운 오브젝트를 추가할 수 없도록 합니다", + "heading": "선택창 비활성화" }, "heading": "수정하기", "max_options_shown": { - "label": "Dropdown에 표시되는 최대 개수" + "label": "선택창에 표시되는 최대 개수" }, "rating_system": { "star_precision": { @@ -726,7 +752,7 @@ "description": "비디오가 끝나면 대기열에 있는 다음 영상을 재생합니다", "heading": "플레이리스트 이어보기" }, - "show_scrubber": "스크러버 보이기", + "show_scrubber": "스크러버 표시", "track_activity": "영상 재생 기록 활성화", "vr_tag": { "description": "VR 버튼은 이 태그를 가진 영상에서만 보여질 것입니다.", @@ -734,7 +760,8 @@ }, "enable_chromecast": "크롬캐스트 활성화", "disable_mobile_media_auto_rotate": "모바일 환경에서 전체화면 시 자동 방향 회전 비활성화", - "show_ab_loop_controls": "구간반복 기능 활성화" + "show_ab_loop_controls": "구간반복 기능 활성화", + "show_range_markers": "범위 마커 표시" } }, "scene_wall": { @@ -793,6 +820,18 @@ "description": "활성화되면, 모든 컨텐츠 세부사항이 기본값으로 보여지게 되고, 각각의 세부사항들이 하나의 열에 위아래로 정렬됩니다", "heading": "모든 세부사항 보여주기" } + }, + "performer_list": { + "heading": "배우 목록", + "options": { + "show_links_on_grid_card": { + "heading": "배우 그리드 카드에 링크 표시" + } + } + }, + "sfw_mode": { + "description": "건전한 컨텐츠를 저장하기 위해 Stash를 사용한다면 활성화하세요. 성인 컨텐츠와 관련된 UI 요소들을 숨기거나 변경시킵니다.", + "heading": "건전 컨텐츠 모드" } }, "advanced_mode": "고급 설정 모드" @@ -820,7 +859,7 @@ "criterion_modifier": { "between": "구간", "equals": "=", - "excludes": "포함하지 않음", + "excludes": "제외", "format_string": "{criterion} {modifierString} {valueString}", "greater_than": ">", "includes": "포함", @@ -834,7 +873,7 @@ "not_null": "값 존재함", "format_string_excludes": "{criterion} {modifierString} {valueString} ({excludedString} 제외)", "format_string_excludes_depth": "{criterion} {modifierString} {valueString} ({excludedString} 제외) (+{depth, plural, =-1 {all} other {{depth}}})", - "format_string_depth": "{criterion} {modifierString} {valueString} (+{depth, plural, =-1 {all} other {{depth}}})" + "format_string_depth": "{criterion} {modifierString} {valueString} (+{depth, plural, =-1 {all} other {{depth}}}수준)" }, "custom": "커스텀", "date": "날짜", @@ -861,7 +900,7 @@ "delete_object_title": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} 삭제", "dont_show_until_updated": "다음 업데이트까지 보지 않기", "edit_entity_title": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} 수정", - "export_include_related_objects": "내보내기 할 때 관련된 개체를 포합합니다", + "export_include_related_objects": "내보내기 할 때 관련된 개체를 포함합니다", "export_title": "내보내기", "imagewall": { "direction": { @@ -891,13 +930,13 @@ "label": "스크롤 모드", "pan_y": "수직 스크롤 모드", "zoom": "확대" - } + }, + "disable_animation": "이미지 간 전환 애니메이션 비활성화하기" }, "merge_tags": { "destination": "다른 태그와 합쳐질 태그", "source": "다른 태그로 합쳐질 태그" }, - "overwrite_filter_confirm": "정말 원래 저장되어 있었던 쿼리 {entityName}을 덮어쓰시겠습니까?", "reassign_entity_title": "{count, plural, one {{singularEntity} 재할당} other {{pluralEntity} 재할당}}", "scene_gen": { "clip_previews": "이미지 클립 미리보기", @@ -910,13 +949,13 @@ "marker_image_previews": "마커 움직이는 이미지 미리보기", "marker_image_previews_tooltip": "애니메이션(webp) 미리보기도 생성합니다. 영상/마커 월 미리보기 유형이 '애니메이션 이미지'로 설정된 경우에만 필요합니다. '비디오 미리보기'보다 CPU를 덜 사용하지만, '비디오 미리보기'에 추가적으로 생성되기 때문에 파일 크기가 커집니다.", "marker_screenshots": "마커 스크린샷", - "marker_screenshots_tooltip": "마커 JPG 이미지. 미리보기 유형이 이미지로 설정된 경우에만 필요합니다.", + "marker_screenshots_tooltip": "마커 고정 JPG 이미지", "markers": "마커 미리보기", "markers_tooltip": "주어진 시간 코드에서 시작하는 20초 짜리 비디오입니다.", "override_preview_generation_options": "미리보기 생성 옵션 재정의", "override_preview_generation_options_desc": "이 작업에 대한 미리보기 생성 옵션을 재정의합니다. 기본값은 '시스템' -> '미리보기 생성'에서 설정됩니다.", "overwrite": "기존 파일들 덮어쓰기", - "phash": "해시", + "phash": "해쉬", "preview_exclude_end_time_desc": "영상 미리보기에서 마지막 x 초를 제외합니다. 초 단위, 혹은 전체 영상 재생 길이 중 비율(예: 2%)로 나타낼 수 있습니다.", "preview_exclude_end_time_head": "마지막 영상 부분 제외", "preview_exclude_start_time_desc": "영상 미리보기에서 처음 x 초를 제외합니다. 초 단위, 혹은 전체 영상 재생 길이 중 비율(예: 2%)로 나타낼 수 있습니다.", @@ -938,7 +977,7 @@ "image_thumbnails": "이미지 썸네일", "phash_tooltip": "중복 방지와 영상 식별에 사용됩니다" }, - "scenes_found": "{count}개의 영상을 찾았습니다", + "scenes_found": "{count}개 영상 발견됨", "scrape_entity_query": "{entity_type} 스크레이핑 쿼리", "scrape_entity_title": "{entity_type} 스크레이핑 결과", "scrape_results_existing": "존재", @@ -955,7 +994,14 @@ }, "reassign_files": { "destination": "~으로 재지정" - } + }, + "overwrite_filter_warning": "저장된 필터 \"{entityName}\"은 덮어쓰기될 것입니다.", + "set_default_filter_confirm": "정말로 이 필터를 기본값으로 설정하시겠습니까?", + "clear_o_history_confirm_sfw": "정말 좋아요 기록을 삭제하시겠습니까?", + "delete_alert_to_trash": "다음 {count, plural, one {{singularEntity}} other {{pluralEntity}}}가 휴지통으로 옮겨질 것입니다:", + "stashid_exists_warning": "이 Stash-Box의 기존 Stash ID가 교체될 것입니다.", + "studios_found": "{count}개 스튜디오 발견됨", + "tags_found": "{count}개 태그 발견됨" }, "dimensions": "해상도", "director": "감독", @@ -965,12 +1011,13 @@ "list": "목록", "tagger": "태거", "unknown": "알 수 없음", - "wall": "월 모드" + "wall": "월 모드", + "label_current": "디스플레이 모드: {current}" }, "donate": "후원", "dupe_check": { "description": "'정확' 이하의 수준에서는 계산이 오래 걸릴 수 있습니다. 낮은 정밀도 수준에서는 부정확한 결과가 함께 나올 수 있습니다.", - "duration_diff": "최대 영상 재생 시간 차이", + "duration_diff": "최대 영상 길이 차이", "duration_options": { "any": "상관 없음", "equal": "같음" @@ -1017,13 +1064,19 @@ }, "empty_server": "이 페이지에서 추천 영상들을 확인하려면 영상을 추가하세요.", "errors": { - "image_index_greater_than_zero": "이미지 인덱스는 0보다 커야 합니다", + "image_index_greater_than_zero": "이미지 번호는 0보다 커야 합니다", "lazy_component_error_help": "만약 최근에 Stash를 업그레이드했다면, 웹페이지를 새로고침하거나 브라우저 캐시를 삭제해주세요.", "something_went_wrong": "오류가 발생했습니다.", "header": "오류", "loading_type": "{type}을(를) 로딩하는 중 오류가 발생했습니다", "invalid_javascript_string": "유효하지 않은 자바스크립트 코드입니다: {error}", - "invalid_json_string": "유효하지 않은 JSON 문자열입니다: {error}" + "invalid_json_string": "유효하지 않은 JSON 문자열입니다: {error}", + "custom_fields": { + "field_name_required": "항목 이름이 필요합니다", + "field_name_whitespace": "항목 이름의 전후에 공백이 없어야 합니다", + "duplicate_field": "항목 이름은 중복될 수 없습니다", + "field_name_length": "항목 이름의 글자 수는 65글자보다 작아야 합니다" + } }, "ethnicity": "인종", "existing_value": "존재하는 값", @@ -1072,8 +1125,8 @@ "syncing": "서버와 동기화 중", "uploading": "스크립트 업로드 중" }, - "hasChapters": "챕터 유무", - "hasMarkers": "마커 유무", + "hasChapters": "챕터", + "hasMarkers": "마커", "height": "키", "height_cm": "키 (cm)", "help": "도움말", @@ -1092,7 +1145,8 @@ "last_played_at": "마지막 재생 날짜", "library": "라이브러리", "loading": { - "generic": "로드 중…" + "generic": "로드 중…", + "plugins": "플러그인 로딩 중…" }, "marker_count": "마커 개수", "markers": "마커", @@ -1105,11 +1159,11 @@ "interactive_speed": "인터랙티브 속도", "performer_card": { "age": "{age} {years_old}", - "age_context": "작품에서 {age} {years_old}" + "age_context": "제작 당시 {age} {years_old}" }, "phash": "PHash", - "play_count": "재생 횟수", - "play_duration": "재생 길이", + "play_count": "재생된 횟수", + "play_duration": "재생된 길이", "stream": "스트림", "video_codec": "비디오 코덱", "o_count": "싼 횟수" @@ -1119,7 +1173,6 @@ "name": "이름", "new": "새로 만들기", "none": "없음", - "o_counter": "싼 횟수", "operations": "작업", "organized": "정리됨", "pagination": { @@ -1163,21 +1216,21 @@ "network_error": "네트워크 오류", "no_results_found": "결과가 없습니다.", "number_of_performers_will_be_processed": "{performer_count}명의 배우들이 처리됩니다", - "performer_already_tagged": "이 배우에 이미 존재하는 태그입니다", - "performer_names_separated_by_comma": "배우 이름 (,으로 구분)", + "performer_already_tagged": "배우가 이미 태그되어 있음", "performer_selection": "배우 선택", "performer_successfully_tagged": "배우 태깅에 성공했습니다:", "query_all_performers_in_the_database": "데이터베이스의 모든 배우", "refresh_tagged_performers": "태그된 배우 새로고침", - "refreshing_will_update_the_data": "새로고침하면 Stash-box 인스턴스에 있는 태그된 배우들의 데이터가 업데이트될 것입니다.", + "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": "배우 수정", - "updating_untagged_performers_description": "태그가 지정되지 않은 배우를 업데이트하면, Stash ID가 없는 배우와 비교해본 뒤 메타데이터를 업데이트할 것입니다." + "update_performer": "배우 업데이트", + "update_performers": "배우 업데이트", + "updating_untagged_performers_description": "'태그되지 않은 배우 업데이트'를 통해, Stash ID가 없는 배우들에 대한 데이터를 찾아보고, 가능하다면 이를 이용해 배우를 업데이트합니다.", + "performer_names_or_stashids_separated_by_comma": "배우 이름 또는 Stash ID (쉼표(,)로 구분)" }, "performer_tags": "배우 태그", "performers": "배우", @@ -1208,14 +1261,16 @@ "edit_filter": "필터 수정", "name": "필터", "saved_filters": "저장된 필터", - "update_filter": "필터 업데이트" + "update_filter": "필터 업데이트", + "more_filter_criteria": "외 {count} 개", + "search_term": "검색어" }, "second": "초", "seconds": "초", "settings": "설정", "setup": { "confirm": { - "almost_ready": "거의 설정을 완료했습니다. 아래 설정들을 확인해주세요. 틀린 내용이 있다면 이전으로 돌아가 변경할 수 있습니다. 내용이 모두 맞다면, '확인'을 눌러 시스템을 만드세요.", + "almost_ready": "거의 설정을 완료했습니다. 아래 설정들을 확인해주세요. 틀린 내용이 있다면 이전으로 돌아가 변경할 수 있습니다. 내용이 모두 맞다면, '확인'을 눌러 시스템을 생성하세요.", "blobs_directory": "바이너리 데이터 경로", "cache_directory": "캐시 파일 경로", "configuration_file_location": "설정 파일 위치:", @@ -1255,7 +1310,7 @@ }, "paths": { "database_filename_empty_for_default": "데이터베이스 파일 이름 (빈 칸으로 두면 기본값을 사용합니다)", - "description": "다음으로, 야동 위치와, Stash 데이터베이스, 생성 파일, 캐시 파일의 위치를 정해야 합니다. 이 설정은 나중에 필요할 때 언제든 바꿀 수 있습니다.", + "description": "다음으로, 컨텐츠를 찾을 위치를 정하고, Stash 데이터베이스, 생성 파일, 캐시 파일이 저장될 위치를 정해야 합니다. 이 설정은 나중에 필요할 때 언제든 바꿀 수 있습니다.", "path_to_cache_directory_empty_for_default": "캐시 폴더 경로 (빈 칸으로 두면 기본값을 사용합니다)", "path_to_generated_directory_empty_for_default": "생성되는 파일들을 저장할 폴더 경로 (빈 칸으로 두면 기본값을 사용합니다)", "set_up_your_paths": "경로를 설정하세요", @@ -1266,14 +1321,17 @@ "where_can_stash_store_cache_files": "어디에 캐시 파일을 저장할까요?", "where_can_stash_store_cache_files_description": "HLS/DASH 실시간 스트리밍과 같은 기능들이 동작하기 위해서는, 임시 파일을 저장할 캐시 폴더가 필요합니다. 기본값으로는, 설정 파일이 저장된 폴더 안에 cache라는 폴더가 만들어질 것입니다. 만약 이것을 바꾸고 싶다면, 절대 경로 혹은 (현재 경로의) 상대 경로를 입력해주세요. 입력된 경로에 해당 폴더가 없다면 자동으로 생성됩니다.", "where_can_stash_store_its_database": "어디에 Stash 데이터베이스를 저장할까요?", - "where_can_stash_store_its_database_description": "Stash는 야동 메타데이터를 저장할 때 SQLite 데이터베이스를 사용합니다. 기본값으로, 데이터베이스 파일은 stash-go.sqlite라는 이름으로 설정 파일이 포함된 폴더 안에 생성될 것입니다. 데이터베이스 파일 이름을 바꾸고 싶다면, 절대 경로 또는 (현재 경로의 )상대 경로를 입력하세요.", + "where_can_stash_store_its_database_description": "Stash는 SQLite 데이터베이스를 사용하여 컨텐츠의 메타데이터를 저장합니다. 기본값으로, 데이터베이스 파일은 설정 파일이 포함된 폴더 안에 stash-go.sqlite라는 이름으로 생성될 것입니다. 데이터베이스 파일 이름을 바꾸고 싶다면, 절대 경로 파일 이름, 혹은 (현재 경로의) 상대 경로 파일 이름을 입력하세요.", "where_can_stash_store_its_database_warning": "경고: Stash가 동작하는 곳이 아닌 다른 시스템에 데이터베이스를 저장하는 것은 지원되지 않습니다! (예시: 데이터베이스를 NAS에 저장하면서 Stash 서버를 다른 컴퓨터에서 돌리는 행위) 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": "야동 폴더를 추가하세요. 비디오와 이미지를 스캐닝할 때 사용됩니다.", + "where_is_your_porn_located": "컨텐츠가 있는 위치가 어딘가요?", + "where_is_your_porn_located_description": "영상과 이미지가 들어있는 폴더를 추가하세요. 이 폴더들을 스캔하여 비디오와 이미지를 찾을 것입니다.", "path_to_blobs_directory_empty_for_default": "Blob 폴더 경로 (빈 칸으로 두면 기본값을 사용합니다)", - "store_blobs_in_database": "데이터베이스에 Blob 저장" + "store_blobs_in_database": "데이터베이스에 Blob 저장", + "sfw_content_settings": "Stash를 성인물 등이 아닌 건전한 컨텐츠 저장 용도로 사용하시나요?", + "sfw_content_settings_description": "Stash는 사진, 그림, 만화 등등의 건전한 컨텐츠를 관리하기 위해 사용될 수 있습니다. 이 옵션을 활성화하면 일부 UI 동작이 건전한 컨텐츠에 더 적합하도록 조정됩니다.", + "use_sfw_content_mode": "건전 컨텐츠 모드 사용" }, "stash_setup_wizard": "Stash 설정 마법사", "success": { @@ -1345,20 +1403,20 @@ "title": "제목", "toast": { "added_entity": "{singularEntity}을(를) 추가했습니다", - "added_generation_job_to_queue": "생성 작업을 대기열에 추가했습니다", + "added_generation_job_to_queue": "컨텐츠 생성 작업을 대기열에 추가했습니다", "created_entity": "{entity}를 생성했습니다", "default_filter_set": "기본 필터가 설정되었습니다", "delete_past_tense": "{count, plural, one {{singularEntity}} other {{pluralEntity}}}이(가) 삭제되었습니다", "generating_screenshot": "스크린샷을 생성하는 중…", "image_index_too_large": "오류: 이미지 번호가 갤러리의 이미지 개수보다 큽니다", - "merged_scenes": "영상들을 합쳤습니다", - "merged_tags": "병합된 태그", - "reassign_past_tense": "재할당된 파일", + "merged_scenes": "영상이 병합되었습니다", + "merged_tags": "태그가 병합되었습니다", + "reassign_past_tense": "파일이 재할당되었습니다", "removed_entity": "{singularEntity}을(를) 제거했습니다", "rescanning_entity": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} 다시 스캔하는 중…", "saved_entity": "{entity}를 저장했습니다", "started_auto_tagging": "자동 태깅을 시작했습니다", - "started_generating": "생성을 시작했습니다", + "started_generating": "컨텐츠 생성을 시작했습니다", "started_importing": "불러오기를 시작했습니다", "updated_entity": "{entity}를 수정했습니다" }, @@ -1369,11 +1427,11 @@ "updated_at": "수정 날짜", "url": "URL", "validation": { - "date_invalid_form": "${path}는 YYYY-MM-DD 형태여야 합니다", + "date_invalid_form": "${path}는 YYYY, YYYY-MM, YYYY-MM-DD 형식 중 하나여야 합니다", "required": "${path}는 필수 항목입니다", "unique": "${path}은(는) 유일해야 합니다", "blank": "${path}를 빈 칸으로 둘 수 없습니다", - "end_time_before_start_time": "종료 시간은 시작 시간보다 크거나 같아야 합니다" + "end_time_before_start_time": "종료 시간은 시작 시간 이후여야 합니다" }, "videos": "비디오", "view_all": "모두 보기", @@ -1396,7 +1454,6 @@ "name_already_exists": "이름이 이미 존재합니다", "studio_already_tagged": "스튜디오가 이미 태그되었음", "updating_untagged_studios_description": "태그되지 않은 스튜디오를 업데이트하면, stashid가 없는 스튜디오들을 확인하고 메타데이터를 업데이트할 것입니다.", - "studio_names_separated_by_comma": "스튜디오 이름들 (쉼표(,)로 구분)", "current_page": "현재 페이지", "failed_to_save_studio": "\"{studio}\" 스튜디오를 저장하는 데에 실패했습니다", "refresh_tagged_studios": "태그된 스튜디오 새로고침", @@ -1417,7 +1474,8 @@ "update_studios": "스튜디오 업데이트", "untagged_studios": "태그되지 않은 스튜디오", "update_studio": "스튜디오 업데이트", - "studio_selection": "스튜디오 선택" + "studio_selection": "스튜디오 선택", + "studio_names_or_stashids_separated_by_comma": "스튜디오 이름 또는 StashID (쉼표로 구분)" }, "audio_codec": "오디오 코덱", "connection_monitor": { @@ -1459,7 +1517,7 @@ "show_all": "모두 보여주기", "update": "업데이트", "selected_only": "선택된 것만", - "required_by": "{packages}로 인해 요구됨" + "required_by": "{packages}가 정상 동작하기 위해 설치되어야 함" }, "o_count": "싼 횟수", "orientation": "방향", @@ -1495,5 +1553,39 @@ "include_sub_studio_content": "서브스튜디오 컨텐츠 포함", "include_sub_tag_content": "서브태그 컨텐츠 포함", "time_end": "종료 시간", - "include_sub_groups": "서브그룹 포함" + "include_sub_groups": "서브그룹 포함", + "custom_fields": { + "value": "값", + "field": "항목", + "title": "커스텀 항목", + "criteria_format_string": "{criterion} (커스텀 항목) {modifierString} {valueString}", + "criteria_format_string_others": "{criterion} (커스텀 항목) {modifierString} {valueString} (+{others} 기타)" + }, + "login": { + "password": "비밀번호", + "invalid_credentials": "유효하지 않은 사용자 이름 또는 비밀번호입니다", + "login": "로그인", + "internal_error": "예상치 못한 내부 에러입니다. 로그에서 세부 사항을 확인하세요", + "username": "사용자 이름" + }, + "age_on_date": "제작 당시 {age}살", + "sort_name": "이름 (sort name)", + "criterion_modifier_values": { + "none": "값 없음", + "only": "해당 값만 존재", + "any": "값 존재", + "any_of": "해당 값 중 일부 포함" + }, + "eta": "예상 소요 시간", + "scenes_duration": "영상 길이", + "last_o_at_sfw": "마지막 좋아요 날짜", + "o_count_sfw": "좋아요", + "o_history_sfw": "좋아요 기록", + "odate_recorded_no_sfw": "좋아요 날짜 기록 없음", + "stashbox_search": { + "header": "StashBox로부터 {entityType} 탐색", + "no_results": "탐색 결과가 없습니다.", + "placeholder_name_or_id": "({entityType} 이름 또는 StashID를 입력하세요)", + "select_stashbox": "StashBox 선택..." + } } diff --git a/ui/v2.5/src/locales/lt-LT.json b/ui/v2.5/src/locales/lt-LT.json new file mode 100644 index 000000000..0007adb26 --- /dev/null +++ b/ui/v2.5/src/locales/lt-LT.json @@ -0,0 +1,112 @@ +{ + "actions": { + "add": "Pridėti", + "add_directory": "Pridėti katalogą", + "add_entity": "Pridėti {entityType}", + "add_manual_date": "Įvesti datą rankini būdu", + "add_sub_groups": "Pridėti pogrupius", + "add_o": "Pridėti O", + "add_to_entity": "Pridėti prie {entityType}", + "allow": "Leisti", + "allow_temporarily": "Leisti laikinai", + "anonymise": "Anonimizuoti", + "apply": "Taikyti", + "assign_stashid_to_parent_studio": "Priskirti Stash ID esamai pagrindinei studijai ir atnaujinti metaduomenis", + "auto_tag": "Auto žymė", + "browse_for_image": "Ieškoti vaizdo…", + "cancel": "Atšaukti", + "choose_date": "Pasirinkti datą", + "clean": "Valyti", + "clean_generated": "Išvalyti sugeneruotus failus", + "clear": "Valyti", + "clear_date_data": "Valyti datos duomenis", + "clear_front_image": "Valyti priekinį vaizdą", + "clear_back_image": "Valyti galinį vaizdą", + "clear_image": "Valyti vaizdą", + "close": "Uždaryti", + "confirm": "Patvirtinti", + "continue": "Tęsti", + "copy_to_clipboard": "Kopijuoti į iškarpinę", + "create": "Sukurti", + "create_chapters": "Sukurti skyrių", + "create_entity": "Sukurti {entityType}", + "create_marker": "Sukurti žymeklį", + "create_parent_studio": "Sukurti pagrindinę studiją", + "created_entity": "Sukurtas {entity_type}: {entity_name}", + "customise": "Tinkinti", + "delete": "Ištrinti", + "delete_entity": "Ištrinti {entityType}", + "delete_file": "Ištrinti failą", + "delete_file_and_funscript": "Ištrinti failą (ir funscript)", + "delete_generated_supporting_files": "Ištrinti sugeneruotus pagalbinius failus", + "disable": "Išjungti", + "disallow": "Neleisti", + "download": "Atsisiųsti", + "download_anonymised": "Atsisiųsti anonimizuotą", + "download_backup": "Atsisiųsti atsarginę kopiją", + "edit": "Redaguoti", + "edit_entity": "Redaguoti {entityType}", + "enable": "Įjungti", + "encoding_image": "Koduojamas vaizdas…", + "export": "Eksportuoti", + "export_all": "Eksportuoti visus…", + "find": "Rasti", + "finish": "Baigti", + "from_file": "Iš failo…", + "from_url": "Iš URL…", + "full_export": "Pilnas eksportas", + "full_import": "Pilnas importas", + "generate": "Generuoti", + "generate_thumb_default": "Sugeneruoti numatytąją miniatiūrą", + "generate_thumb_from_current": "Sugeneruoti miniatiūrą iš dabartinio", + "hide": "Paslėpti", + "hide_configuration": "Paslėpti konfiguraciją", + "identify": "Atpažinti", + "ignore": "Ignoruoti", + "import": "Importuoti…", + "import_from_file": "Importuoti iš failo", + "load": "Krauti", + "load_filter": "Užkrauti filtrą", + "logout": "Atsijungti", + "make_primary": "Padaryti pirminiu", + "merge": "Sulieti", + "merge_from": "Sulieti iš", + "merge_into": "Sulieti į", + "next_action": "Kitas", + "not_running": "Nevykdomas", + "open_in_external_player": "Atidaryti išoriniame grotuve", + "open_random": "Atidaryti atsitiktinį", + "optimise_database": "Optimizuoti duomenų bazę", + "overwrite": "Perrašyti", + "play": "Groti", + "play_random": "Groti atsitiktinį", + "play_selected": "Groti pasirinktą", + "preview": "Peržiūra", + "previous_action": "Atgal", + "reassign": "Priskirti iš naujo", + "refresh": "Atnaujinti", + "reload": "Perkrauti", + "reload_plugins": "Perkrauti papildinius", + "reload_scrapers": "Perkrauti skreperius", + "remove": "Šalinti", + "remove_date": "Šalinti datą", + "remove_from_containing_group": "Šalinti iš grupės", + "remove_from_gallery": "Šalinti iš galerijos", + "rename_gen_files": "Pervadinti sugeneruotus failus", + "rescan": "Pakartotinai nuskaityti", + "reset_play_duration": "Atstatyti grojimo trukmę", + "reset_resume_time": "Atstatyti tęsimo laiką", + "reset_cover": "Atkurti numatytąjį viršelį", + "reshuffle": "Permaišyti", + "running": "Vykdoma", + "save": "Išsaugoti", + "save_delete_settings": "Naudoti šias parinktis kaip numatytąsias trinant", + "save_filter": "Išsaugoti filtrą", + "scan": "Skenuoti", + "search": "Ieškoti", + "select_all": "Pasirinkti visus", + "select_entity": "Pasirinkti {entityType}", + "select_folders": "Pasirinkti katalogus", + "select_none": "Atžymėti viską" + } +} diff --git a/ui/v2.5/src/locales/lv-LV.json b/ui/v2.5/src/locales/lv-LV.json index b90c34df2..e61f8f9ab 100644 --- a/ui/v2.5/src/locales/lv-LV.json +++ b/ui/v2.5/src/locales/lv-LV.json @@ -64,13 +64,13 @@ "add_sub_groups": "Pievienot apakšgrupas", "from_file": "No Faila…", "from_url": "No URL…", - "disallow": "Neatļaut", + "disallow": "Aizliegt", "download": "Lejupielādēt", "download_anonymised": "Lejupielādēt anonīmi", "download_backup": "Lejupielādēt Dublējumu", "edit": "Rediģēt", "edit_entity": "Rediģēt {entityType}", - "enable": "Iepējot", + "enable": "Iespējot", "encoding_image": "Konstruē bildi…", "export_all": "Eksportēt visu…", "find": "Atrast", @@ -80,7 +80,7 @@ "hide_configuration": "Paslēpt Konfigurāciju", "identify": "Identificēt", "ignore": "Ignorēt", - "import": "Importēt…", + "import": "Ievietot…", "hide": "Paslēpt", "make_primary": "Padarīt primāro", "merge_from": "Apvienot no", @@ -100,7 +100,7 @@ "reload_plugins": "Pārlādēt spraudņus", "refresh": "Atsvaidzināt", "disable": "Atspējot", - "export": "Eksportēt", + "export": "Izgūt", "logout": "Izrakstīties", "full_export": "Pilns Eksports", "full_import": "Pilns Imports", @@ -108,7 +108,17 @@ "generate_thumb_from_current": "Ģenerēt sīktēlu no pašreizējā", "import_from_file": "Importēt no faila", "merge": "Apvienot", - "migrate_scene_screenshots": "Migrēt Video Ekrānšāviņus" + "migrate_scene_screenshots": "Migrēt Video Ekrānšāviņus", + "save": "Saglabāt", + "search": "Meklēt", + "skip": "Izlaist", + "split": "Sadalīt", + "stop": "Apturēt", + "submit": "Iesniegt", + "remove": "Noņemt", + "rescan": "Skenēt pa jaunu", + "scan": "Skenēt", + "show": "Rādīt" }, "unknown_date": "Nezināms datums", "twitter": "Twitter", @@ -117,5 +127,54 @@ "zip_file_count": "Zip Failu Skaits", "weight_kg": "Svars (kg)", "weight": "Svars", - "years_old": "Gadus vecs" + "years_old": "Gadus vecs", + "component_tagger": { + "config": { + "query_mode_filename": "Datnes nosaukums", + "blacklist_label": "Melnais saraksts", + "query_mode_dir": "Mape", + "query_mode_metadata": "Metadati" + } + }, + "actions_name": "Darbības", + "age": "Vecums", + "aliases": "Aizstājvārdi", + "all": "visi", + "ascending": "Augošā secībā", + "between_and": "un", + "birthdate": "Dzimšanas datums", + "blobs_storage_type": { + "filesystem": "Datņsistēma", + "database": "Datubāze" + }, + "captions": "Subtitri", + "chapters": "Nodaļas", + "config": { + "categories": { + "security": "Drošība", + "tools": "Rīki", + "changelog": "Izmaiņu žurnāls", + "plugins": "Spraudņi" + }, + "general": { + "plugins_path": { + "description": "Ceļš uz spraudņu konfigurācijas mapi", + "heading": "Spraudņu mape" + } + }, + "plugins": { + "available_plugins": "Pieejamie spraudņi", + "installed_plugins": "Uzstādītie spraudņi" + }, + "tasks": { + "cleanup_desc": "Meklēt trūkstošos failus un noņemt tos no datubāzes. Šī darbība ir neatgriezeniska." + }, + "about": { + "check_for_new_version": "Pārbaudīt, vai pieejama jauna versija" + } + }, + "donate": "Ziedot", + "package_manager": { + "check_for_updates": "Pārbaudīt, vai pieejami atjauninājumi" + } } diff --git a/ui/v2.5/src/locales/nb-NO.json b/ui/v2.5/src/locales/nb-NO.json index 883b33b34..6a97b5ca2 100644 --- a/ui/v2.5/src/locales/nb-NO.json +++ b/ui/v2.5/src/locales/nb-NO.json @@ -5,7 +5,7 @@ "confirm": "Bekreft", "continue": "Fortsett", "close": "Lukk", - "reset_cover": "Tilbakestill Standard Omslag", + "reset_cover": "Tilbakestill Standard Forsidebilde", "remove": "Fjern", "running": "kjører", "submit_stash_box": "Send til Stash-Box", @@ -29,7 +29,7 @@ "save_delete_settings": "Bruk disse alternativene som standard når du sletter", "save_filter": "Lagre filter", "scan": "Skann", - "scrape": "Skrape", + "scrape": "Skrap", "create": "Opprett", "create_chapters": "Opprett Kapittel", "create_marker": "Opprett Markør", @@ -61,22 +61,22 @@ "created_entity": "Opprettet {entity_type}: {entity_name}", "merge_from": "Slå sammen fra", "clean_generated": "Rydd opp i genererte filer", - "clear": "Tøm", + "clear": "Fjern", "clear_back_image": "Fjern bakbilde", "clear_date_data": "Fjern dato data", "clear_image": "Fjern Bilde", "create_parent_studio": "Opprett foreldre studio", "customise": "Tilpass", - "disallow": "Ikke tillat", + "disallow": "Forby", "download_anonymised": "Last ned anonymisert", "export_all": "Eksporter alle…", - "full_export": "Full eksport", - "full_import": "Full Import", + "full_export": "Eksporter alle", + "full_import": "Importer alle", "hash_migration": "hash migrering", "make_primary": "Gjør til Primær", "previous_action": "Tilbake", "refresh": "Oppdater", - "reload": "Last på nytt", + "reload": "Last inn på nytt", "not_running": "Kjører ikke", "open_in_external_player": "Åpne i ekstern spiller", "remove_date": "Fjern dato", @@ -105,9 +105,9 @@ "view_history": "Visningshistorikk", "view_random": "Vis Tilfeldig", "migrate_blobs": "Migrer Blobs", - "migrate_scene_screenshots": "Migrer Scene Skjermbilder", - "reassign": "Omplasser", - "reload_plugins": "Last inn plugins på nytt", + "migrate_scene_screenshots": "Flytt Scene Skjermbilder", + "reassign": "Tilordne på nytt", + "reload_plugins": "Last inn programtillegg på nytt", "reload_scrapers": "Last inn skrapere på nytt", "scrape_scene_fragment": "Skrap etter fragment", "set_back_image": "Baksidebilde…", @@ -119,7 +119,7 @@ "cancel": "Avbryt", "apply": "Bruk", "assign_stashid_to_parent_studio": "Tildel Stash ID til eksisterende foreldre studio og oppdater metadata", - "add_to_entity": "Legg til til {entityType}", + "add_to_entity": "Legg til {entityType}", "add_entity": "Legg til {entityType}", "add_manual_date": "Legg til manuell dato", "add_directory": "Legg til mappe", @@ -138,10 +138,18 @@ "play_selected": "Spill av valgte", "rescan": "Skann på nytt", "reshuffle": "Stokk om", - "rename_gen_files": "Gi nytt navn til genererte filer", - "selective_auto_tag": "Selektiv Automatisk Tagging", + "rename_gen_files": "Gi genererte filer nytt navn", + "selective_auto_tag": "Selektiv Auto Tag", "set_image": "Velg bilde…", - "selective_clean": "Selektiv Rens" + "selective_clean": "Selektiv Fjerning", + "sidebar": { + "close": "lukk sidebar", + "open": "åpne sidebar", + "toggle": "Endre sidepanelet" + }, + "show_results": "Vis resultater", + "show_count_results": "Vis {count} resultater", + "play": "Spill av" }, "component_tagger": { "config": { @@ -168,15 +176,28 @@ "errors": { "blacklist_duplicate": "Dupliser element fra svarteliste" }, - "set_tag_desc": "Tilknytt tagger til scenen, enten ved å overskrive eller flette med eksisterende tagger på scenen.", - "show_male_label": "Vis mannlige utøvere", - "show_male_desc": "Angi om mannlige utøvere skal være tilgjengelige for tagging." + "set_tag_desc": "Tilknytt tagger til scenen, enten ved å overskrive eller flette med eksisterende tagger på scenen." }, "noun_query": "Forespørsel", "results": { "duration_off": "Varighet avviker fra forventet verdi med minst {number}s", - "duration_unknown": "Ukjent varighet" - } + "duration_unknown": "Ukjent varighet", + "phash_matches": "{count} PHash-er stemmer overens", + "unnamed": "Uten navn", + "hash_matches": "{hash_type} stemmer overens", + "match_failed_already_tagged": "Scenen er allerede tagget", + "match_failed_no_result": "Ingen resultater funnet", + "fp_found": "{fpCount, plural, =0 {Ingen nye fingeravtrykksmatch funnet} other {# nye fingeravtrykksmatch funnet}}", + "fp_matches": "Varighet stemmer overens", + "fp_matches_multi": "Varighet samsvarer med {matchCount} av {durationsLength} fingeravtrykk", + "match_success": "Scenen ble tagget vellykket" + }, + "verb_match_fp": "Match Fingeravtrykk", + "verb_matched": "Matchet", + "verb_scrape_all": "Skrap alle", + "verb_submit_fp": "Send inn {fpCount, plural, one{# Fingeravtrykk} andre{# Fingerprints}}", + "verb_toggle_config": "{toggle} {configuration}", + "verb_toggle_unmatched": "{toggle} vis scener uten treff" }, "config": { "dlna": { @@ -192,7 +213,14 @@ "allow_temp_ip": "Tillatt {tempIP}", "allowed_ip_addresses": "Tillatt IP adresser", "server_port": "Serverport", - "server_display_name": "Server Visningsnavn" + "server_display_name": "Server Visningsnavn", + "server_display_name_desc": "Visningsnavn for DLNA-serveren. Standard til {server_navn} hvis tom.", + "server_port_desc": "Port for å kjøre DLNA-serveren på. Krever omstart av DLNA etter endring.", + "until_restart": "frem til omstart", + "network_interfaces_desc": "Grensesnitt for å eksponere DLNA-server på. En tom liste resulterer i å kjøre på alle grensesnitt. Krever omstart av DLNA etter endring.", + "successfully_cancelled_temporary_behaviour": "Vellykket kansellert midlertidig oppførsel", + "video_sort_order_desc": "Rekkefølgen som videoer sorteres etter som standard.", + "video_sort_order": "Standard videosortering" }, "about": { "stash_open_collective": "Støtt oss gjennom {url}", @@ -203,7 +231,10 @@ "check_for_new_version": "Sjekk for ny versjon", "latest_version": "Siste versjon", "latest_version_build_hash": "Siste Versjon Build Hash:", - "build_time": "Kompileringstid:" + "build_time": "Kompileringstid:", + "stash_wiki": "stash {url} side", + "build_hash": "bygg hash:", + "stash_home": "Stash-startside på {url}" }, "advanced_mode": "Avansert Modus", "categories": { @@ -227,18 +258,533 @@ "log_file": "Log-fil", "generate_api_key": "Generer API-nøkkel", "log_to_terminal": "Log til terminal", - "password": "Passord" + "password": "Passord", + "log_http_desc": "Logger HTTP-tilgang til terminalen. Krever omstart.", + "authentication": "Autentisering", + "clear_api_key": "Fjern API-nøkkel", + "credentials": { + "description": "Brukernavn og passord for å sikre tilgang til Stash.", + "heading": "Påloggingsinformasjon" + }, + "log_file_desc": "Sti til filen hvor logg skal lagres. La feltet stå tomt for å deaktivere fillogging. Krever omstart.", + "log_http": "Logg HTTP-tilgang", + "log_to_terminal_desc": "Logger til terminalen i tillegg til en fil. Er alltid aktivert hvis fillogging er deaktivert. Krever omstart.", + "maximum_session_age": "Maksimal levetid for økt", + "maximum_session_age_desc": "Maksimal inaktiv tid før en innloggingsøkt utløper, i sekunder. Krever omstart.", + "password_desc": "Passord for å få tilgang til Stash. La stå tomt for å deaktivere brukergodkjenning", + "stash-box_integration": "ntegrasjon med Stash-box", + "username_desc": "Brukernavn for å få tilgang til Stash. La stå tomt for å deaktivere brukergodkjenning", + "api_key_desc": "API-nøkkel for eksterne systemer. Kun nødvendig når brukernavn/passord er konfigurert. Brukernavn må lagres før API-nøkkel kan genereres." }, "db_path_head": "Database filbane", "ffmpeg": { "download_ffmpeg": { - "heading": "Last ned FFmpeg" + "heading": "Last ned FFmpeg", + "description": "Laster ned FFmpeg til konfigurasjonsmappen og tilbakestiller ffmpeg- og ffprobe-banene til å bruke konfigurasjonsmappen i stedet." }, "hardware_acceleration": { - "heading": "FFmpeg maskinvare encoding" + "heading": "Maskinvareakselerert koding med FFmpeg", + "desc": "Bruker tilgjengelig maskinvare til å kode video for sanntids-transkoding." + }, + "ffmpeg_path": { + "description": "Bane til ffmpeg-programmet (ikke bare mappen). Hvis den er tom, vil ffmpeg bli funnet fra miljøet via $PATH, konfigurasjonsmappen eller fra $HOME/.stash", + "heading": "FFmpeg kjørbar filbane" + }, + "ffprobe_path": { + "heading": "FFmpeg kjørbar filbane", + "description": "Bane til ffprobe-kjørbar fil (ikke bare mappen). Hvis feltet er tomt, vil ffprobe bli hentet fra miljøet via $PATH, konfigurasjonsmappen eller fra $HOME/.stash" + }, + "live_transcode": { + "input_args": { + "heading": "FFmpeg sanntids-transkoding inndata-argumenter", + "desc": "Avansert: Ekstra argumenter som sendes til ffmpeg før inndatafeltet ved sanntids-transkoding av video." + }, + "output_args": { + "heading": "FFmpeg sanntids-transkoding utdata-argumenter", + "desc": "Avansert: Ekstra argumenter som sendes til ffmpeg før utdatafeltet ved sanntids-transkoding av video." + } + }, + "transcode": { + "input_args": { + "desc": "Avansert: Ekstra argumenter som sendes til ffmpeg før inndatafeltet ved generering av video.", + "heading": "FFmpeg transkoding inndata-argumenter" + }, + "output_args": { + "desc": "Avansert: Ekstra argumenter som sendes til ffmpeg før utdatafeltet ved generering av video.", + "heading": "FFmpeg transkoding utdata-argumenter" + } } }, - "database": "Database" + "database": "Database", + "backup_directory_path": { + "heading": "Sti til sikkerhetskopimappe", + "description": "Mappeplassering for sikkerhetskopier av SQLite-databasefilen" + }, + "blobs_storage": { + "description": "Hvor binærdata som scenecovers, bilder av skuespillere, studioer og tagger skal lagres. Etter at denne verdien endres, må eksisterende data migreres ved hjelp av oppgaven “Migrer blobs”. Se oppgavesiden for migrering.", + "heading": "Lagringstype for binærdata" + }, + "blobs_path": { + "description": "Hvor i filsystemet binærdata skal lagres. Gjelder kun når filsystem er valgt som lagringstype for blob-data. ADVARSEL: Endring av dette krever at eksisterende data flyttes manuelt.", + "heading": "Sti for lagring av binærdata" + }, + "cache_path_head": "Sti til hurtigbuffer", + "chrome_cdp_path": "Sti til Chrome CDP", + "cache_location": "Mappeplassering for hurtigbufferen. Påkrevd ved strømming med HLS (f.eks. på Apple-enheter) eller DASH.", + "calculate_md5_and_ohash_desc": "Beregn MD5-sjekksum i tillegg til oshash. Å aktivere dette vil gjøre innledende skanninger tregere. Filnavn-hash må være satt til oshash for å deaktivere MD5-beregning.", + "calculate_md5_and_ohash_label": "Beregn MD5 for videoer", + "check_for_insecure_certificates": "Sjekk etter usikre sertifikater", + "check_for_insecure_certificates_desc": "Noen nettsteder bruker usikre SSL-sertifikater. Når dette er avhuket, hopper skraperen over kontrollen av sertifikater og tillater skraping av slike nettsteder. Hvis du får en sertifikatfeil under skraping, fjern avhukingen her.", + "chrome_cdp_path_desc": "Filsti til Chrome-kjørbar fil, eller en ekstern adresse (som begynner med http:// eller https://, for eksempel http://localhost:9222/json/version) til en Chrome-instans.", + "create_galleries_from_folders_desc": "Hvis aktivert, opprettes gallerier automatisk fra mapper som inneholder bilder. Opprett en fil kalt .forcegallery eller .nogallery i en mappe for å henholdsvis tvinge eller hindre dette.", + "create_galleries_from_folders_label": "Lag gallerier automatisk fra bildemapper", + "directory_locations_to_your_content": "Stier til innholdskataloger", + "excluded_image_gallery_patterns_desc": "Regexps av bilde og galleri filer/filbaner å utelukke fra Scan og legge til Clean", + "excluded_video_patterns_desc": "Regexps av video filer/filbaner å utelukke fra Scan og legge til Clean", + "excluded_image_gallery_patterns_head": "Utelukkede Bilde-/Gallerimønstre", + "excluded_video_patterns_head": "Utelukkede Videomønstre", + "image_ext_head": "Bilde-filendelser", + "plugins_path": { + "description": "Mappestedsplassering for plugin-konfigurasjonsfiler", + "heading": "Plugin-bane" + }, + "scrapers_path": { + "heading": "Scraper-bane", + "description": "Mappestedsplassering for scraper-konfigurasjonsfiler" + }, + "scraping": "Scraping", + "logging": "Logging", + "maximum_transcode_size_head": "Maksimal transkodingsstørrelse", + "funscript_heatmap_draw_range": "Inkluder område i genererte varmekart", + "funscript_heatmap_draw_range_desc": "Tegn bevegelsesområde på y-aksen i det genererte varmekartet. Eksisterende varmekart må genereres på nytt etter endring.", + "generated_file_naming_hash_head": "Hash for generert filnavngivning", + "generated_path_head": "Generert bane", + "maximum_streaming_transcode_size_head": "Maksimal størrelse for strømmingstranskoding", + "maximum_transcode_size_desc": "Maksimal størrelse for genererte transkodinger", + "number_of_parallel_task_for_scan_generation_head": "Antall parallelle oppgaver for skanning/generering", + "scraper_user_agent_desc": "User-Agent-streng som brukes under HTTP-forespørsler for scraping", + "gallery_cover_regex_desc": "Regexp som brukes for å identifisere et bilde som galleriomslag", + "include_audio_desc": "Inkluderer lydstrøm ved generering av forhåndsvisninger.", + "include_audio_head": "Inkluder lyd", + "maximum_streaming_transcode_size_desc": "Maksimal størrelse for transkodede strømmer", + "metadata_path": { + "heading": "Metadatabane", + "description": "Mappestedsplassering som brukes ved full eksport eller import" + }, + "python_path": { + "description": "Bane til Python-kjørbar fil (ikke bare mappen). Brukes for skript-skapere og plugins. Hvis tomt, vil Python bli hentet fra miljøet", + "heading": "Python kjørbar filbane" + }, + "generated_file_naming_hash_desc": "Bruk MD5 eller oshash for generert filnavngivning. Endring av dette krever at alle scener har den aktuelle MD5/oshash-verdien fylt ut. Etter å ha endret denne verdien, må eksisterende genererte filer migreres eller genereres på nytt. Se Oppgaver-siden for migrering.", + "heatmap_generation": "Funscript-varmekartgenerering", + "number_of_parallel_task_for_scan_generation_desc": "Sett til 0 for automatisk deteksjon. Advarsel: Å kjøre flere oppgaver enn nødvendig for å oppnå 100 % CPU-utnyttelse vil redusere ytelsen og kan potensielt forårsake andre problemer.", + "sqlite_location": "Filplassering for SQLite-databasen (krever omstart). ADVARSEL: Å lagre databasen på et annet system enn der Stash-serveren kjører (f.eks. over nettverket) støttes ikke!", + "image_ext_desc": "Kommaseparert liste over filendelser som vil bli identifisert som bilder.", + "parallel_scan_head": "Parallell skanning/generering", + "video_ext_desc": "Kommaseparert liste over filendelser som vil bli identifisert som videoer.", + "gallery_cover_regex_label": "Mønster for galleriomslag", + "gallery_ext_desc": "Kommaseparert liste over filendelser som vil bli identifisert som galleri-zip-filer.", + "gallery_ext_head": "Galleri-zip-filendelser", + "generated_files_location": "Mappestedsplassering for de genererte filene (scene-markører, scene-forhåndsvisninger, sprites, osv.)", + "hashing": "Hashing", + "preview_generation": "Forhåndsvisningsgenerering", + "scraper_user_agent": "Scraper-brukeragent", + "video_ext_head": "Video-filendelser", + "video_head": "Video" + }, + "application_paths": { + "heading": "applikasjon baner" + }, + "tasks": { + "plugin_tasks": "Plugin-oppgaver", + "generate_thumbnails_during_scan": "Generer miniatyrbilder for bilder", + "identify": { + "source": "Kilde", + "tag_skipped_matches": "Tagg hoppet over treff med", + "and_create_missing": "og skape mangler", + "create_missing": "Lag mangler", + "description": "Angi automatisk scenemetadata ved hjelp av stash-box- og scraper-kilder.", + "field": "Felt", + "identifying_scenes": "Identifisering av {num} {scene}", + "include_male_performers": "Inkluder mannlige utøvere", + "set_cover_images": "Sett forsidebilder", + "tag_skipped_performers": "Tagg hoppet over utøvere med", + "default_options": "Standardalternativer", + "set_organized": "Sett organisert flagg", + "identifying_from_paths": "Identifisere scener fra følgende stier", + "tag_skipped_performer_tooltip": "Opprett en tagg som «Identifiser: Enkeltnavnsutøver» som du kan filtrere etter i scenetaggervisningen, og velg hvordan du vil håndtere disse utøverne", + "explicit_set_description": "Følgende alternativer vil bli brukt der de ikke overstyres i de kildespesifikke alternativene.", + "field_behaviour": "{strategi} {felt}", + "field_options": "Feltalternativer", + "heading": "Identifisere", + "skip_multiple_matches": "Hopp over treff som har mer enn ett resultat", + "skip_multiple_matches_tooltip": "Hvis dette ikke er aktivert og mer enn ett resultat returneres, vil ett bli tilfeldig valgt for å matche", + "skip_single_name_performers": "Hopp over utøvere med ett enkelt navn uten entydighet", + "skip_single_name_performers_tooltip": "Hvis dette ikke er aktivert, vil utøvere som ofte er generiske, som Samantha eller Olga, bli matchet", + "source_options": "{kilde} Alternativer", + "sources": "Kilder", + "strategy": "Strategi", + "tag_skipped_matches_tooltip": "Opprett en tagg som «Identifiser: Flere treff» som du kan filtrere etter i Scene Tagger-visningen og velge riktig treff manuelt" + }, + "rescan": "Skann filer på nytt", + "auto_tag": { + "auto_tagging_all_paths": "Automatisk tagging av alle stier", + "auto_tagging_paths": "Automatisk tagging av følgende stier" + }, + "auto_tag_based_on_filenames": "Automatisk tagging av innhold basert på filstier.", + "clean_generated": { + "blob_files": "Blob-filer", + "description": "Fjerner genererte filer uten en tilhørende databaseoppføring.", + "image_thumbnails": "Bildeminiatyrer", + "markers": "Markørforhåndsvisninger", + "previews": "Sceneforhåndsvisninger", + "sprites": "Scenesprites", + "transcodes": "Scenetranskodinger", + "image_thumbnails_desc": "Bildeminiatyrer og klipp", + "previews_desc": "Sceneforhåndsvisninger og miniatyrbilder" + }, + "dont_include_file_extension_as_part_of_the_title": "Ikke inkluder filtypen som en del av tittelen", + "generate_video_previews_during_scan": "Generer forhåndsvisninger", + "generated_content": "Generert innhold", + "import_from_exported_json": "Importer fra eksportert JSON i metadatakatalogen. Sletter den eksisterende databasen.", + "incremental_import": "Trinnvis import fra en levert eksport-zip-fil.", + "job_queue": "Oppgavekø", + "maintenance": "Vedlikehold", + "anonymising_database": "Anonymisering av database", + "backing_up_database": "Sikkerhetskopiering av database", + "generate_video_previews_during_scan_tooltip": "Generer forhåndsvisninger av videoer som spilles av når du holder musepekeren over en scene", + "added_job_to_queue": "La til {operation_name} i jobbkøen", + "anonymise_and_download": "Lager en anonymisert kopi av databasen og laster ned den resulterende filen.", + "anonymise_database": "Lager en kopi av databasen i backup-mappen, og anonymiserer all sensitiv informasjon. Denne kan deretter deles med andre for feilsøking og debugging. Den opprinnelige databasen blir ikke endret. Den anonymiserte databasen bruker filnavnformatet {filename_format}.", + "backup_database": "Utfører en sikkerhetskopi av databasen til backup-mappen, med filnavnformatet {filename_format}", + "cleanup_desc": "Sjekk etter manglende filer og fjern dem fra databasen. Dette er en destruktiv handling.", + "defaults_set": "Standardinnstillinger er satt og vil bli brukt når du klikker på {action}-knappen på Oppgaver-siden.", + "migrate_scene_screenshots": { + "description": "Migrer skjermbilder av scener til det nye blob-lagringssystemet. Denne migreringen bør kjøres etter at et eksisterende system er migrert til 0.20. Kan eventuelt slette de gamle skjermbildene etter migreringen.", + "delete_files": "Slett skjermbildefiler", + "overwrite_existing": "Overskriv eksisterende blobs med skjermbildedata" + }, + "data_management": "Databehandling", + "generate_previews_during_scan_tooltip": "Generer også animerte (webp) forhåndsvisninger, som bare er nødvendig når Forhåndsvisningstype for scene/markørvegg er satt til Animert bilde. Når du surfer, bruker de mindre CPU enn videoforhåndsvisningene, men genereres i tillegg til dem og er større filer.", + "generate_video_covers_during_scan": "Generer sceneomslag", + "optimise_database_warning": "Advarsel: Mens denne oppgaven kjører, vil alle operasjoner som endrer databasen mislykkes, og avhengig av databasestørrelsen kan det ta flere minutter å fullføre. Den krever også minst like mye ledig diskplass som databasen er stor, men 1,5 ganger anbefales.", + "auto_tagging": "Automatisk tagging", + "backup_and_download": "Utfører en sikkerhetskopi av databasen og laster ned den resulterende filen.", + "empty_queue": "Ingen oppgaver kjører for øyeblikket.", + "export_to_json": "Eksporterer databaseinnholdet til JSON-format i metadata-mappen.", + "generate": { + "generating_from_paths": "Genererer for scener fra følgende stier", + "generating_scenes": "Genererer for {num} {scene}" + }, + "generate_clip_previews_during_scan": "Generer forhåndsvisninger for bildeklipp", + "generate_desc": "Generer støttefiler for bilde, sprite, video, vtt og andre filer.", + "generate_phashes_during_scan": "perseptuelle", + "generate_phashes_during_scan_tooltip": "For deduplisering og sceneidentifikasjon.", + "generate_previews_during_scan": "Generer animerte bildeforhåndsvisninger", + "generate_sprites_during_scan": "Generer scrubber-sprites", + "generate_sprites_during_scan_tooltip": "Bildesettet som vises under videospilleren for enkel navigering.", + "migrate_blobs": { + "delete_old": "Slett gamle data", + "description": "Migrer blober til gjeldende blob-lagringssystem. Denne migreringen bør kjøres etter at blob-lagringssystemet er endret. Kan eventuelt slette gamle data etter migrering." + }, + "migrate_hash_files": "Brukes etter endring av den genererte filnavngivningshashen for å gi eksisterende genererte filer nytt navn til det nye hashformatet.", + "migrations": "Migrasjoner", + "only_dry_run": "Utfør kun en prøvekjøring. Ikke fjern noe", + "optimise_database": "Forsøk å forbedre ytelsen ved å analysere og deretter gjenoppbygge hele databasefilen.", + "rescan_tooltip": "Skann alle filer i banen på nytt. Brukes til å tvinge frem oppdatering av filmetadata og skanne zip-filer på nytt.", + "scan": { + "scanning_all_paths": "Skanner alle stier", + "scanning_paths": "Skanner følgende stier" + }, + "scan_for_content_desc": "Skann etter nytt innhold og legg det til i databasen.", + "set_name_date_details_from_metadata_if_present": "Angi navn, dato og detaljer fra innebygde filmetadata" + }, + "ui": { + "scene_player": { + "options": { + "vr_tag": { + "heading": "VR Tag", + "description": "VR-knappen vises bare for scener med denne taggen." + }, + "always_start_from_beginning": "Start alltid videoen fra begynnelsen", + "auto_start_video": "Autostart video", + "auto_start_video_on_play_selected": { + "description": "Start scenevideoer automatisk når du spiller av fra køen, eller spiller av valgte eller tilfeldige scener fra scenesiden", + "heading": "Start video automatisk når den valgte videoen spilles av" + }, + "continue_playlist_default": { + "description": "Spill av neste scene i køen når videoen er ferdig", + "heading": "Fortsett spilleliste som standard" + }, + "disable_mobile_media_auto_rotate": "Deaktiver automatisk rotasjon av fullskjermsmedier på mobil", + "enable_chromecast": "Aktiver Chromecast", + "show_ab_loop_controls": "Vis AB Loop-plugin-kontroller", + "show_scrubber": "Vis Scrubber", + "show_range_markers": "Vis avstandsmarkører", + "track_activity": "Aktiver Scene Play-logg" + }, + "heading": "Scenespiller" + }, + "tag_panel": { + "options": { + "show_child_tagged_content": { + "heading": "Vis subtag-innhold", + "description": "I tag-visningen kan du også vise innhold fra undertaggene" + } + }, + "heading": "Tag-visning" + }, + "editing": { + "heading": "Redigering", + "disable_dropdown_create": { + "heading": "Deaktiver oppretting av rullegardinmenyen", + "description": "Fjern muligheten til å opprette nye objekter fra rullegardinmenyene" + }, + "max_options_shown": { + "label": "Maksimalt antall elementer som skal vises i utvalgte rullegardinmenyer" + }, + "rating_system": { + "star_precision": { + "label": "Rangeringsstjernepresisjon", + "options": { + "full": "Full", + "half": "Halv", + "quarter": "Fjerdedel", + "tenth": "Tiende" + } + }, + "type": { + "label": "Rangeringssystemtype", + "options": { + "decimal": "Desimal", + "stars": "Stjerner" + } + } + } + }, + "interactive_options": "Interaktive alternativer", + "studio_panel": { + "options": { + "show_child_studio_content": { + "heading": "Vis innhold fra understudioer", + "description": "I studiovisningen kan du også vise innhold fra understudioene" + } + }, + "heading": "Studio utsikt" + }, + "abbreviate_counters": { + "heading": "Forkort tellere", + "description": "Forkort tellere i kort- og detaljvisningssider, for eksempel vil «1831» bli formatert til «1,8K»." + }, + "basic_settings": "Grunnleggende innstillinger", + "custom_css": { + "heading": "Egendefinert CSS", + "option_label": "Egendefinert CSS aktivert", + "description": "Siden må lastes inn på nytt for at endringene skal tre i kraft. Det er ingen garanti for kompatibilitet mellom tilpasset CSS og fremtidige utgivelser av Stash." + }, + "custom_javascript": { + "description": "Siden må lastes inn på nytt for at endringene skal tre i kraft. Det er ingen garanti for kompatibilitet mellom tilpasset Javascript og fremtidige versjoner av Stash.", + "heading": "Egendefinert Javascript", + "option_label": "Egendefinert Javascript aktivert" + }, + "custom_locales": { + "heading": "Tilpasset lokalisering", + "option_label": "Egendefinert lokalisering aktivert", + "description": "Overstyr individuelle språkstrenger. Se https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/locales/en-GB.json for hovedlisten. Siden må lastes inn på nytt for at endringene skal tre i kraft." + }, + "delete_options": { + "description": "Standardinnstillinger ved sletting av bilder, gallerier og scener.", + "heading": "Slett alternativer", + "options": { + "delete_file": "Slett fil som standard", + "delete_generated_supporting_files": "Slett genererte støttefiler som standard" + } + }, + "desktop_integration": { + "desktop_integration": "Desktop-integrasjon", + "notifications_enabled": "Aktiver varsler", + "send_desktop_notifications_for_events": "Send skrivebordsvarsler for hendelser", + "skip_opening_browser": "Hopp over å åpne nettleseren", + "skip_opening_browser_on_startup": "Hopp over automatisk åpning av nettleser under oppstart" + }, + "detail": { + "compact_expanded_details": { + "heading": "Kompakte utvidede detaljer", + "description": "Når dette alternativet er aktivert, vil det presentere utvidede detaljer samtidig som presentasjonen blir kompakt" + }, + "enable_background_image": { + "description": "Vis bakgrunnsbilde på detaljsiden.", + "heading": "Aktiver bakgrunnsbilde" + }, + "heading": "Detaljside", + "show_all_details": { + "description": "Når den er aktivert, vises alle innholdsdetaljer som standard, og hvert detaljelement får plass under én kolonne", + "heading": "Vis alle detaljer" + } + }, + "funscript_offset": { + "heading": "Funksjonsskriptforskyvning (ms)", + "description": "Tidsforskyvning i millisekunder for avspilling av interaktive skript." + }, + "handy_connection": { + "connect": "Koble til", + "server_offset": { + "heading": "Serveroffset" + }, + "status": { + "heading": "Praktisk tilkoblingsstatus" + }, + "sync": "Synkroniser" + }, + "handy_connection_key": { + "heading": "Handy tilkoblingsnøkkel", + "description": "Praktisk tilkoblingsnøkkel for bruk for interaktive scener. Hvis du angir denne knappen, kan Stash dele informasjon om gjeldende scene med handyfeeling.com" + }, + "image_lightbox": { + "heading": "Bilde lysboks" + }, + "image_wall": { + "direction": "Retning", + "heading": "Bildevegg", + "margin": "Margin (piksler)" + }, + "images": { + "heading": "Bilder", + "options": { + "create_image_clips_from_videos": { + "description": "Når et bibliotek har deaktivert videoer, vil videofiler (filer som slutter på videoendelsen) skannes som bildeklipp.", + "heading": "Skann videoutvidelser som bildeklipp" + }, + "write_image_thumbnails": { + "heading": "Skriv miniatyrbilder", + "description": "Skriv miniatyrbilder av bilder til disk når de genereres på farten" + } + } + }, + "language": { + "heading": "Språk" + }, + "max_loop_duration": { + "description": "Maksimal scenevarighet der scenespilleren vil spille av videoen i loop - 0 for å deaktivere", + "heading": "Maksimal sløyfevarighet" + }, + "menu_items": { + "heading": "Menyelementer", + "description": "Vis eller skjul ulike typer innhold på navigasjonslinjen" + }, + "minimum_play_percent": { + "description": "Prosentandelen av tiden en scene må spilles av før avspillingstallene økes.", + "heading": "Minimum Spilleprosent" + }, + "performers": { + "options": { + "image_location": { + "heading": "Tilpasset utøverbildebane", + "description": "Tilpasset sti for standard utøverbilder. La stå tomt for å bruke innebygde standardinnstillinger" + } + } + }, + "preview_type": { + "heading": "Forhåndsvisningstype", + "options": { + "animated": "Animert bilde", + "static": "Statisk bilde", + "video": "Video" + }, + "description": "Standardalternativet er forhåndsvisninger av videoer (mp4). For mindre CPU-bruk når du surfer, kan du bruke forhåndsvisninger av animerte bilder (webp). Disse må imidlertid genereres i tillegg til forhåndsvisningene av videoer, og de er større filer." + }, + "scene_list": { + "heading": "Rutenettvisning", + "options": { + "show_studio_as_text": "Vis studiooverlegg som tekst" + } + }, + "scene_wall": { + "heading": "Scene-/markørvegg", + "options": { + "toggle_sound": "Aktiver lyd", + "display_title": "Vis tittel og tagger" + } + }, + "scroll_attempts_before_change": { + "heading": "Rullforsøk før overgang", + "description": "Antall forsøk på å bla før man går til neste/forrige element. Gjelder kun for Pan Y-rullemodus." + }, + "show_tag_card_on_hover": { + "description": "Vis tagkort når du holder musepekeren over tag-merkene", + "heading": "Verktøytips for tagkort" + }, + "slideshow_delay": { + "description": "Lysbildefremvisning er tilgjengelig i gallerier i veggvisningsmodus", + "heading": "Forsinkelse av lysbildefremvisning (sekunder)" + }, + "title": "Brukergrensesnitt", + "use_stash_hosted_funscript": { + "heading": "Server funscripts direkte", + "description": "Når dette er aktivert, vil funscripts bli servert direkte fra Stash til Handy-enheten din uten å bruke tredjeparts Handy-serveren. Krever at Stash er tilgjengelig fra Handy-enheten din, og at en API-nøkkel genereres hvis stash har konfigurert legitimasjon." + } + }, + "scraping": { + "available_scrapers": "Tilgjengelige scrapers", + "search_by_name": "Søk etter navn", + "supported_types": "Støttede typer", + "installed_scrapers": "Installerte scrapers", + "scraper": "Skraper", + "entity_scrapers": "{entityType} scrapers", + "excluded_tag_patterns_desc": "Regulære uttrykk for tag-navn som skal ekskluderes fra scraperesultater", + "scrapers": "Skrapere", + "entity_metadata": "{entityType} Metadata", + "excluded_tag_patterns_head": "Ekskluderte tag-mønstre", + "supported_urls": "URLs" + }, + "plugins": { + "installed_plugins": "Installerte plugins", + "hooks": "Hooks", + "triggers_on": "Utløsere aktivert", + "available_plugins": "Tilgjengelige plugins" + }, + "library": { + "media_content_extensions": "Filendelser for medieinnhold", + "gallery_and_image_options": "Galleri- og bildeinnstillinger", + "exclusions": "Eksklusjoner" + }, + "tools": { + "scene_duplicate_checker": "Sceneduplikatkontrollør", + "scene_filename_parser": { + "add_field": "Legg til felt", + "capitalize_title": "Bruk stor bokstav i tittelen", + "display_fields": "Vis felt", + "escape_chars": "Bruk \\ for å escape-tegn", + "filename": "Filnavn", + "ignore_organized": "Ignorer organiserte scener", + "ignored_words": "Ignorerte ord", + "matches_with": "Samsvarer med {i}", + "select_parser_recipe": "Velg parseroppskrift", + "title": "Scenefilnavnparser", + "whitespace_chars_desc": "Disse tegnene vil bli erstattet med mellomrom i tittelen", + "filename_pattern": "Filnavnmønster", + "whitespace_chars": "Mellomromstegn" + }, + "graphql_playground": "GraphQL lekeplass", + "heading": "Verktøy", + "scene_tools": "Sceneverktøy" + }, + "stashbox": { + "endpoint": "Endepunkt", + "description": "Stash-box muliggjør automatisk tagging av scener og utøvere basert på fingeravtrykk og filnavn.\nEndepunkt og API-nøkkel finnes på kontosiden din på Stash-box-instansen. Navn kreves når mer enn én instans legges til.", + "graphql_endpoint": "GraphQL-endepunkt", + "name": "Navn", + "add_instance": "Legg til Stash-box-instans", + "api_key": "API-nøkkel", + "max_requests_per_minute": "Maks forespørsler per minutt", + "max_requests_per_minute_description": "Bruker standardverdi på {defaultValue} hvis satt til 0", + "title": "Stash-box-endepunkter" + }, + "logs": { + "log_level": "Loggnivå" + }, + "system": { + "transcoding": "Transkoding" } }, "appears_with": "Opptrer med", @@ -265,5 +811,733 @@ "UNCUT": "Ikke omskåret" }, "birth_year": "Fødselsår", - "all": "alle" + "all": "alt", + "media_info": { + "performer_card": { + "age": "{alder} {år_gammel}", + "age_context": "{age} {years_old} ved produksjon" + }, + "video_codec": "Video Kodek", + "audio_codec": "Lydkodek", + "checksum": "Sjekksum", + "downloaded_from": "Lastet ned fra", + "hash": "Hash", + "interactive_speed": "Interaktiv hastighet", + "o_count": "0 Antall", + "phash": "PHash", + "play_count": "Avspilt", + "play_duration": "Spillevarighet", + "stream": "Strøm" + }, + "age_on_date": "{age} ved produksjon", + "search_filter": { + "update_filter": "Oppdater Filtre", + "saved_filters": "Lagrede filter", + "edit_filter": "Rediger filter", + "name": "Filter", + "more_filter_criteria": "+{tell} flere" + }, + "history": "Historie", + "play_count": "Antall Avspillinger", + "play_history": "Avspillinger", + "release_notes": "Utgivelsesnotater", + "scene": "Scene", + "path": "Sti", + "package_manager": { + "installed_version": "Installert versjon", + "required_by": "Kreves av {pakker}", + "add_source": "Legg til kilde", + "check_for_updates": "Se etter oppdateringer", + "confirm_delete_source": "Er du sikker på at du vil slette kilden {name} ({url})?", + "description": "Beskrivelse", + "edit_source": "Rediger Kilde", + "hide_unselected": "Skjul uvalgt", + "latest_version": "Siste versjon", + "no_packages": "Ingen pakker funnet", + "no_sources": "Ingen kilder er konfigurert", + "no_upgradable": "Ingen oppgraderbare pakker funnet", + "package": "Pakke", + "selected_only": "Kun valgte", + "show_all": "Vis alle", + "source": { + "local_path": { + "heading": "Lokal Sti", + "description": "Relativ sti til lagringspakker for denne kilden. Merk at endring av dette krever at pakkene flyttes manuelt." + }, + "name": "Navn", + "url": "Kilde URL" + }, + "uninstall": "Avinstallere", + "unknown": "", + "update": "Oppdatere", + "version": "Versjon", + "install": "Installere", + "confirm_uninstall": "Er du sikker på at du vil avinstallere {number} pakker?" + }, + "rating": "Vurdering", + "queue": "Kø", + "studio_tagger": { + "status_tagging_job_queued": "Status: Taggejobb i kø", + "batch_update_studios": "Batch Oppdater Studio", + "failed_to_save_studio": "Kunne ikke lagre studioet «{studio}»", + "add_new_studios": "Legg til nye studioer", + "batch_add_studios": "Batch Legg Til Studio", + "config": { + "edit_excluded_fields": "Rediger ekskluderte felt", + "excluded_fields": "Ekskluderte felt:", + "no_fields_are_excluded": "Ingen felt er ekskludert", + "no_instances_found": "Ingen forekomster funnet", + "these_fields_will_not_be_changed_when_updating_studios": "Disse feltene vil ikke bli endret når du oppdaterer studioer.", + "active_stash-box_instance": "Aktiv stash-box-instans:", + "create_parent_label": "Lag foreldrestudioer", + "create_parent_desc": "Opprett manglende foreldrestudioer, eller tagg og oppdater data/bilde for eksisterende foreldrestudioer med nøyaktige navnesamsvar" + }, + "create_or_tag_parent_studios": "Opprett manglende eller tagg eksisterende foreldrestudioer", + "current_page": "Gjeldende side", + "name_already_exists": "Navnet finnes allerede", + "network_error": "Nettverksfeil", + "no_results_found": "Ingen resultater funnet.", + "number_of_studios_will_be_processed": "{studio_count} studioer vil bli behandlet", + "query_all_studios_in_the_database": "Alle studioer i databasen", + "refreshing_will_update_the_data": "Oppdatering vil oppdatere dataene til alle taggede studioer fra stash-box-instansen.", + "studio_already_tagged": "Studio allerede merket", + "studio_selection": "Studio utvalg", + "studio_successfully_tagged": "Studio er tagget", + "any_names_entered_will_be_queried": "Alle navn som legges inn vil bli spørt fra den eksterne Stash-Box-instansen og lagt til hvis de blir funnet. Bare eksakte treff vil bli ansett som treff.", + "refresh_tagged_studios": "Oppdater taggede studioer", + "status_tagging_studios": "Status: Merking av studioer", + "tag_status": "Tag status", + "to_use_the_studio_tagger": "For å bruke studio-taggeren må en stash-box-instans konfigureres.", + "untagged_studios": "Umerkede studioer", + "update_studio": "Oppdater Studio", + "update_studios": "Oppdater Studio", + "updating_untagged_studios_description": "Oppdatering av utaggede studioer vil prøve å matche alle studioer som mangler en stashid og oppdatere metadataene." + }, + "effect_filters": { + "blue": "Blå", + "name_transforms": "Forvandles", + "red": "Rød", + "aspect": "Aspekt", + "blur": "Uskarphet", + "brightness": "Lysstyrke", + "contrast": "Kontrast", + "gamma": "Gamma", + "green": "Grønn", + "hue": "Hue", + "rotate": "Rotere", + "rotate_left_and_scale": "Roter til venstre og skaler", + "rotate_right_and_scale": "Roter til høyre og skaler", + "saturation": "Metning", + "scale": "Skala", + "warmth": "Varme", + "reset_filters": "Tilbakestill filtre", + "reset_transforms": "Tilbakestill Transforms", + "name": "Filtre" + }, + "megabits_per_second": "{verdi} mbps", + "stats": { + "scenes_duration": "Varighet av scener", + "image_size": "Bildestørrelse", + "total_o_count": "Total O-telling", + "scenes_played": "Scener spilt", + "scenes_size": "Scenestørrelse", + "total_play_count": "Totale Avspillinger", + "total_play_duration": "Total Spilletid" + }, + "studio_depth": "Nivåer (tomt for alle)", + "custom_fields": { + "title": "Egendefinerte felt", + "criteria_format_string": "{kriterium} (egendefinert felt) {modifierString} {valueString}", + "value": "Verdi", + "criteria_format_string_others": "{kriterium} (egendefinert felt) {modifierString} {valueString} (+{andre} andre)", + "field": "Felt" + }, + "dialogs": { + "scene_gen": { + "marker_screenshots": "Marker skjermbilder", + "video_previews": "Forhåndsvisninger", + "video_previews_tooltip": "Videoforhåndsvisninger som spilles av når du holder musepekeren over en scene", + "covers": "Scenebilder", + "image_previews": "Forhåndsvisninger av animerte bilder", + "image_previews_tooltip": "Generer også animerte (webp) forhåndsvisninger, som bare er nødvendig når Forhåndsvisningstype for scene/markørvegg er satt til Animert bilde. Når du surfer, bruker de mindre CPU enn videoforhåndsvisningene, men genereres i tillegg til dem og er større filer.", + "image_thumbnails": "Miniatyrbilder", + "interactive_heatmap_speed": "Generer varmekart og hastigheter for interaktive scener", + "marker_image_previews": "Forhåndsvisning av animerte bilder for markører", + "marker_screenshots_tooltip": "Marker statiske JPG-bilder", + "markers": "Marker forhåndsvisning", + "markers_tooltip": "20 sekunders videoer som starter på den gitte tidskoden.", + "override_preview_generation_options": "Overstyr forhåndsvisningsgenereringsalternativer", + "override_preview_generation_options_desc": "Overstyr forhåndsvisningsgenereringsalternativer for denne operasjonen. Standardverdier angis i System -> Forhåndsvisningsgenerering.", + "overwrite": "Overskriv eksisterende filer", + "phash": "Perseptuelle hasher", + "phash_tooltip": "For deduplisering og sceneidentifikasjon", + "preview_exclude_end_time_head": "Ekskluder sluttid", + "preview_exclude_start_time_desc": "Ekskluder de første x sekundene fra sceneforhåndsvisninger. Dette kan være en verdi i sekunder, eller en prosentandel (f.eks. 2 %) av den totale scenevarigheten.", + "preview_exclude_start_time_head": "Ekskluder starttidspunkt", + "preview_generation_options": "Forhåndsvisningsgenereringsalternativer", + "preview_options": "Forhåndsvisningsalternativer", + "preview_preset_head": "Forhåndsinnstilling av koding", + "preview_seg_count_desc": "Antall segmenter i forhåndsvisningsfiler.", + "preview_seg_count_head": "Antall segmenter i forhåndsvisning", + "preview_seg_duration_desc": "Varigheten av hvert forhåndsvisningssegment, i sekunder.", + "preview_seg_duration_head": "Forhåndsvis segmentets varighet", + "sprites": "Scene Scrubber Sprites", + "sprites_tooltip": "Bildesettet som vises under videospilleren for enkel navigering.", + "transcodes": "Transkoder", + "force_transcodes": "Tving generering av transkode", + "marker_image_previews_tooltip": "Generer også animerte (webp) forhåndsvisninger, som bare er nødvendig når Forhåndsvisningstype for scene/markørvegg er satt til Animert bilde. Når du surfer, bruker de mindre CPU enn videoforhåndsvisningene, men genereres i tillegg til dem og er større filer.", + "force_transcodes_tooltip": "Som standard genereres transkoder bare når videofilen ikke støttes i nettleseren. Når dette er aktivert, genereres transkoder selv om videofilen ser ut til å være støttet i nettleseren.", + "preview_preset_desc": "Forhåndsinnstillingen regulerer størrelse, kvalitet og kodingstid for forhåndsvisningsgenerering. Forhåndsinnstillinger utover «treg» har avtagende avkastning og anbefales ikke.", + "preview_exclude_end_time_desc": "Ekskluder de siste x sekundene fra sceneforhåndsvisninger. Dette kan være en verdi i sekunder, eller en prosentandel (f.eks. 2 %) av den totale scenevarigheten.", + "clip_previews": "Forhåndsvisninger av bildeklipp", + "transcodes_tooltip": "MP4-transkoder vil bli forhåndsgenerert for alt innhold; nyttig for trege CPU-er, men krever mye mer diskplass" + }, + "scenes_found": "{count} scener funnet", + "scrape_results_scraped": "Skrapet", + "set_image_url_title": "Bilde-URL", + "lightbox": { + "scroll_mode": { + "label": "Rullemodus", + "pan_y": "Pan Y", + "zoom": "Zoom", + "description": "Hold Shift-tasten nede for å midlertidig bruke en annen modus." + }, + "delay": "Forsinkelse (sek)", + "display_mode": { + "fit_to_screen": "Tilpass til skjermen", + "label": "Visningsmodus", + "original": "Orginalt", + "fit_horizontally": "Passer horisontalt" + }, + "options": "Alternativer", + "reset_zoom_on_nav": "Tilbakestill zoomnivå når du bytter bilde", + "scale_up": { + "description": "Skaler mindre bilder opp for å fylle skjermen", + "label": "Skaler opp for å passe" + }, + "page_header": "Side {side} / {totalt}" + }, + "merge": { + "empty_results": "Verdiene i destinasjonsfeltet vil forbli uendret.", + "destination": "Destinasjon", + "source": "Kilde" + }, + "imagewall": { + "margin_desc": "Antall margpiksler rundt hvert bilde.", + "direction": { + "column": "Kolonne", + "description": "Kolonne- eller radbasert oppsett.", + "row": "Rad" + } + }, + "delete_confirm": "Er du sikker på at du vil slette {entityName}?", + "clear_o_history_confirm": "Er du sikker på at du vil slette O-historikken?", + "clear_play_history_confirm": "Er du sikker på at du vil slette avspillingshistorikken?", + "create_new_entity": "Opprett ny {entity}", + "delete_entity_simple_desc": "{count, plural, one {Er du sikker på at du vil slette denne {singularEntity}?} other {Er du sikker på at du vil slette disse {pluralEntity}?}}", + "delete_entity_title": "{antall, flertall, én {Slett {entallEntitet}} annet {Slett {flertallEntitet}}}", + "delete_galleries_extra": "... pluss eventuelle bildefiler som ikke er knyttet til noe annet galleri.", + "delete_object_desc": "Er du sikker på at du vil slette {count, plural, one {this {singularEntity}} other {these {pluralEntity}}}?", + "delete_object_overflow": "…og {count} other {count, flertall, one {{singularEntity}} other {{pluralEntity}}}.", + "delete_object_title": "Slett {count, flertall, én {{singularEntity}} annen {{pluralEntity}}}", + "dont_show_until_updated": "Ikke vis før neste oppdatering", + "export_include_related_objects": "Inkluder relaterte objekter i eksporten", + "export_title": "Eksport", + "merge_tags": { + "destination": "Destinasjon", + "source": "Kilde" + }, + "performers_found": "{count} utøvere funnet", + "reassign_entity_title": "{antall, flertall, én {Reassign {singularEntity}} annen {Reassign {pluralEntity}}}", + "reassign_files": { + "destination": "Tilordne på nytt til" + }, + "scrape_entity_query": "Skrapeforespørsel fra {entity_type}", + "scrape_entity_title": "{entity_type} Skrape Resultater", + "scrape_results_existing": "Eksisterende", + "delete_entity_desc": "{count, plural, one {Er du sikker på at du vil slette denne {singularEntity}? Med mindre filen også slettes, vil denne {singularEntity} bli lagt til på nytt når skanningen utføres.} other {Er du sikker på at du vil slette disse {pluralEntity}? Med mindre filene også slettes, vil disse {pluralEntity} bli lagt til på nytt når skanningen utføres.}}", + "delete_gallery_files": "Slett gallerimappen/zip-filen og alle bilder som ikke er knyttet til noe annet galleri.", + "edit_entity_title": "Rediger {count, flertall, én {{singularEntity}} annen {{pluralEntity}}}", + "delete_alert": "Følgende {count, plural, one {{singularEntity}} other {{pluralEntity}}} vil bli slettet permanent:", + "overwrite_filter_warning": "Det lagrede filteret «{entityName}» vil bli overskrevet.", + "unsaved_changes": "Ulagrede endringer. Er du sikker på at du vil avslutte?", + "set_default_filter_confirm": "Er du sikker på at du vil angi dette filteret som standard?" + }, + "director": "Regissør", + "display_mode": { + "unknown": "Ukjent", + "wall": "Vegg", + "tagger": "Tagger", + "list": "Liste", + "label_current": "Visningsmodus: {gjeldende}", + "grid": "Rutenett" + }, + "distance": "Lengde", + "dupe_check": { + "only_select_matching_codecs": "Velg bare hvis alle kodeker samsvarer i duplikatgruppen", + "options": { + "exact": "Nøyaktig", + "high": "Høy", + "low": "Lav", + "medium": "Medium" + }, + "search_accuracy_label": "Søkenøyaktighet", + "duration_diff": "Maksimal varighetsforskjell", + "duration_options": { + "equal": "Lik", + "any": "Noen" + }, + "select_none": "Velg Ingen", + "select_oldest": "Velg den eldste filen i duplikatgruppen", + "select_options": "Velg Alternativer…", + "select_youngest": "Velg den yngste filen i duplikatgruppen", + "title": "Dupliserte scener", + "description": "Nivåer under «Nøyaktig» kan ta lengre tid å beregne. Falske positive resultater kan også returneres ved lavere nøyaktighetsnivåer.", + "found_sets": "{setCount, flertall, one{# sett med duplikater funnet.} other {# sett med duplikater funnet.}}", + "select_all_but_largest_file": "Velg alle filer i hver dupliserte gruppe, unntatt den største filen", + "select_all_but_largest_resolution": "Velg alle filer i hver dupliserte gruppe, unntatt filen med høyest oppløsning" + }, + "duplicated_phash": "Duplisert (pHash)", + "errors": { + "invalid_javascript_string": "Ugyldig javascript-kode: {error}", + "custom_fields": { + "duplicate_field": "Feltnavnet må være unikt", + "field_name_length": "Feltnavnet må inneholde færre enn 65 tegn", + "field_name_required": "Feltnavn er obligatorisk", + "field_name_whitespace": "Feltnavnet kan ikke ha innledende eller etterfølgende mellomrom" + }, + "header": "Feil", + "invalid_json_string": "Ugyldig JSON-streng: {error}", + "loading_type": "Feil ved lasting av {type}", + "something_went_wrong": "Noe gikk galt.", + "image_index_greater_than_zero": "Bildeindeksen må være større enn 0", + "lazy_component_error_help": "Hvis du nylig har oppgradert Stash, må du laste inn siden på nytt eller tømme nettleserens hurtigbuffer." + }, + "file_info": "Filinformasjon", + "group_scene_number": "Scenenummer", + "groups": "Grupper", + "image_index": "Bilde #", + "last_o_at": "Siste O Kl", + "library": "Bibliotek", + "penis": "Penis", + "part_of": "En del av {forelder}", + "performer_tagger": { + "batch_update_performers": "Batch Oppdater Skuespillere", + "config": { + "active_stash-box_instance": "Aktiv stash-box-instans:", + "edit_excluded_fields": "Rediger Ekskluderte Felt", + "no_fields_are_excluded": "Ingen felt er ekskludert", + "no_instances_found": "Ingen forekomster funnet", + "excluded_fields": "Ekskluderte felt:", + "these_fields_will_not_be_changed_when_updating_performers": "Disse feltene vil ikke bli endret når skuespillerne oppdateres." + }, + "network_error": "Nettverksfeil", + "performer_already_tagged": "Skuespilleren er allerede tagget", + "number_of_performers_will_be_processed": "{skuespiller_antall} skuespillere vil bli behandlet", + "batch_add_performers": "Batch Legg Til Skuespillere", + "current_page": "Gjeldende side", + "failed_to_save_performer": "Kunne ikke lagre skuespillere «{skuespiller}»", + "name_already_exists": "Navnet finnes allerede", + "no_results_found": "Ingen resultater funnet.", + "performer_selection": "Utvalg av skuespillere", + "performer_successfully_tagged": "Skuespiller er tagget:", + "query_all_performers_in_the_database": "Alle skuespillere i databasen", + "refresh_tagged_performers": "Oppdater taggede skuespillere", + "status_tagging_job_queued": "Status: Taggejobb i kø", + "status_tagging_performers": "Status: Tagger skuespillere", + "tag_status": "Tagging Status", + "to_use_the_performer_tagger": "For å bruke skuespiller-taggeren må en stash-box-instans konfigureres.", + "untagged_performers": "Utaggede skuespillere", + "update_performer": "Oppdater Skuespiller", + "update_performers": "Oppdater Skuespillere", + "add_new_performers": "Legg til ny skuespiller", + "updating_untagged_performers_description": "Oppdatering av utaggede skuespillere vil forsøke å matche skuespillere som mangler en stashid og oppdatere metadataene.", + "refreshing_will_update_the_data": "Oppdatering vil oppdatere dataene til alle taggede skuespillere fra stash-box-instansen.", + "any_names_entered_will_be_queried": "Alle navn som legges inn vil bli spørt fra den eksterne Stash-Box-instansen og lagt til hvis de blir funnet. Bare eksakte treff vil bli ansett som treff." + }, + "setup": { + "welcome_specific_config": { + "next_step": "Når du er klar til å fortsette med å sette opp et nytt system, klikker du på Neste.", + "config_path": "Stash vil bruke følgende konfigurasjonsfilsti: {path}", + "unable_to_locate_specified_config": "Hvis du leser dette, fant ikke Stash konfigurasjonsfilen som er angitt på kommandolinjen eller i miljøet. Denne veiviseren vil veilede deg gjennom prosessen med å sette opp en ny konfigurasjon." + }, + "paths": { + "set_up_your_paths": "Sett opp dine veier", + "database_filename_empty_for_default": "databasefilnavn (tomt for standard)", + "description": "Deretter må vi finne ut hvor du finner pornosamlingen din, og hvor du skal lagre Stash-databasen, genererte filer og hurtigbufferfiler. Disse innstillingene kan endres senere om nødvendig.", + "path_to_generated_directory_empty_for_default": "sti til generert katalog (tom som standard)", + "stash_alert": "Ingen bibliotekstier er valgt. Ingen medier kan skannes inn i Stash. Er du sikker?", + "store_blobs_in_database": "Lagre blober i databasen", + "where_can_stash_store_blobs": "Hvor kan Stash lagre binære databasedata?", + "where_can_stash_store_blobs_description_addendum": "Alternativt kan du lagre disse dataene i databasen. Merk: Dette vil øke størrelsen på databasefilen din, og det vil øke migreringstiden for databasen.", + "where_can_stash_store_cache_files": "Hvor kan Stash lagre hurtigbufferfiler?", + "where_can_stash_store_its_database_description": "Stash bruker en SQLite-database for å lagre pornometadataene dine. Som standard opprettes dette som stash-go.sqlite i katalogen som inneholder konfigurasjonsfilen din. Hvis du vil endre dette, må du skrive inn et absolutt eller relativt (til gjeldende arbeidskatalog) filnavn.", + "where_can_stash_store_its_database_warning": "ADVARSEL: lagring av databasen på et annet system enn der Stash kjøres fra (f.eks. lagring av databasen på en NAS mens Stash-serveren kjøres på en annen datamaskin) er ikke støttet! SQLite er ikke ment for bruk på tvers av et nettverk, og forsøk på å gjøre det kan lett føre til at hele databasen blir ødelagt.", + "where_can_stash_store_its_generated_content": "Hvor kan Stash lagre det genererte innholdet?", + "where_is_your_porn_located": "Hvor ligger pornoen din?", + "where_is_your_porn_located_description": "Legg til kataloger som inneholder pornovideoene og bildene dine. Stash vil bruke disse katalogene til å finne videoer og bilder under skanning.", + "path_to_blobs_directory_empty_for_default": "sti til blobs-katalogen (tom som standard)", + "where_can_stash_store_its_database": "Hvor kan Stash lagre databasen sin?", + "where_can_stash_store_cache_files_description": "For at funksjonalitet som HLS/DASH live-transkoding skal fungere, krever Stash en hurtigbufferkatalog for midlertidige filer. Som standard oppretter Stash en cache-katalog i katalogen som inneholder konfigurasjonsfilen din. Hvis du vil endre dette, må du angi en absolutt eller relativ (til gjeldende arbeidskatalog) sti. Stash oppretter denne katalogen hvis den ikke allerede finnes.", + "where_can_stash_store_its_generated_content_description": "For å kunne tilby miniatyrbilder, forhåndsvisninger og sprites genererer Stash bilder og videoer. Dette inkluderer også transkoder for filformater som ikke støttes. Som standard vil Stash opprette en generert katalog i katalogen som inneholder konfigurasjonsfilen din. Hvis du vil endre hvor dette genererte mediet skal lagres, må du angi en absolutt eller relativ (til gjeldende arbeidskatalog) sti. Stash vil opprette denne katalogen hvis den ikke allerede finnes.", + "path_to_cache_directory_empty_for_default": "sti til hurtigbufferkatalog (tom som standard)", + "where_can_stash_store_blobs_description": "Stash kan lagre binære data som scenecovere, utøver, studio og tag-bilder enten i databasen eller i filsystemet. Som standard lagrer den disse dataene i filsystemet i underkatalogen blobs i katalogen som inneholder konfigurasjonsfilen din. Hvis du vil endre dette, må du angi en absolutt eller relativ (til gjeldende arbeidskatalog) sti. Stash vil opprette denne katalogen hvis den ikke allerede finnes." + }, + "creating": { + "creating_your_system": "Oppretter systemet ditt" + }, + "errors": { + "something_went_wrong_while_setting_up_your_system": "Noe gikk galt under oppsettet av systemet ditt. Her er feilen vi mottok: {feil}", + "unable_to_retrieve_system_status": "Kan ikke hente systemstatus: {feil}", + "something_went_wrong": "Å nei! Noe gikk galt!", + "unexpected_error": "Det oppsto en uventet feil: {feil}", + "something_went_wrong_description": "Hvis dette ser ut som et problem med inndataene dine, kan du klikke tilbake for å fikse dem. Ellers kan du rapportere en feil på {githubLink} eller søke hjelp på {discordLink}." + }, + "folder": { + "file_path": "Filbane", + "up_dir": "Opp en mappe" + }, + "github_repository": "Github-depot", + "migrate": { + "migration_failed": "Migrering mislyktes", + "migration_irreversible_warning": "Skjemamigreringsprosessen er ikke reversibel. Når migreringen er utført, vil databasen din være inkompatibel med tidligere versjoner av stash.", + "migration_notes": "Migrasjonsnotater", + "migration_required": "Migrering kreves", + "perform_schema_migration": "Utfør skjemamigrering", + "backup_database_path_leave_empty_to_disable_backup": "Sti til sikkerhetskopieringsdatabase (la stå tomt for å deaktivere sikkerhetskopiering):", + "migration_failed_error": "Følgende feil oppsto under migrering av databasen:", + "schema_too_old": "Din nåværende stash-database er skjemaversjon {databaseSchema} og må migreres til versjon {appSchema}. Denne versjonen av Stash vil ikke fungere uten at databasen migreres. Hvis du ikke ønsker å migrere, må du nedgradere til en versjon som samsvarer med databaseskjemaet ditt.", + "backup_recommended": "Det anbefales at du sikkerhetskopierer den eksisterende databasen din før du migrerer. Vi kan gjøre dette for deg ved å lage en kopi av databasen din til {defaultBackupPath}.", + "migrating_database": "Migrerer database", + "migration_failed_help": "Gjør nødvendige rettelser og prøv på nytt. Ellers kan du melde fra om en feil på {githubLink} eller søke hjelp på {discordLink}." + }, + "confirm": { + "almost_ready": "Vi er nesten klare til å fullføre konfigurasjonen. Vennligst bekreft følgende innstillinger. Du kan klikke tilbake for å endre noe som er feil. Hvis alt ser bra ut, klikker du på Bekreft for å opprette systemet ditt.", + "blobs_directory": "Binær datakatalog", + "blobs_use_database": "", + "cache_directory": "Cache-katalog", + "configuration_file_location": "konfigurasjonsfilplassering:", + "database_file_path": "Banen til databasefilen", + "generated_directory": "Generert katalog", + "nearly_there": "Nesten der!", + "stash_library_directories": "Stash bibliotekkataloger" + }, + "stash_setup_wizard": "Stash Setup Wizard", + "success": { + "download_ffmpeg": "Last ned ffmpeg", + "getting_help": "Får hjelp", + "help_links": "Hvis du støter på problemer eller har spørsmål eller forslag, kan du gjerne åpne en sak i {githubLink} eller spørre fellesskapet i {discordLink}.", + "next_config_step_one": "Du blir deretter tatt til konfigurasjonssiden. Denne siden lar deg tilpasse hvilke filer som skal inkluderes og ekskluderes, angi et brukernavn og passord for å beskytte systemet ditt, og en hel rekke andre alternativer.", + "next_config_step_two": "Når du er fornøyd med disse innstillingene, kan du begynne å skanne innholdet ditt inn i Stash ved å klikke på {localized_task}, deretter {localized_scan}.", + "open_collective": "Sjekk ut vår {open_collective_link} for å se hvordan du kan bidra til den fortsatte utviklingen av Stash.", + "support_us": "Støtt oss", + "thanks_for_trying_stash": "Takk for at du prøvde Stash!", + "your_system_has_been_created": "Suksess! Systemet ditt er opprettet!", + "in_app_manual_explained": "Du oppfordres til å sjekke ut manualen i appen, som du finner via ikonet øverst til høyre på skjermen. Det ser slik ut: {icon}", + "missing_ffmpeg": "Du mangler den nødvendige ffmpeg-binærfilen. Du kan laste den ned automatisk til konfigurasjonskatalogen din ved å merke av i boksen nedenfor. Alternativt kan du oppgi stier til ffmpeg- og ffprobe-binærfilene i systeminnstillingene. Disse binærfilene må være tilstede for at Stash skal fungere.", + "welcome_contrib": "Vi tar også imot bidrag i form av kode (feilrettinger, forbedringer og nye funksjoner), testing, feilrapporter, forbedrings- og funksjonsforespørsler og brukerstøtte. Detaljer finner du i bidragsdelen i appens brukerhåndbok." + }, + "welcome": { + "in_current_stash_directory": "I katalogen {path}:", + "in_the_current_working_directory": "I {path}, arbeidskatalogen, for øyeblikket:", + "in_the_current_working_directory_disabled": "I {path}, arbeidskatalogen:", + "next_step": "Når alt dette er avklart, og du er klar til å fortsette med å sette opp et nytt system, må du velge hvor du vil lagre konfigurasjonsfilen.", + "store_stash_config": "Hvor vil du lagre Stash-konfigurasjonen din?", + "unexpected_explained": "Hvis du får denne skjermen uventet, kan du prøve å starte Stash på nytt i riktig arbeidsmappe eller med -c-flagget.", + "in_the_current_working_directory_disabled_macos": "Støttes ikke når du kjører Stash.app,

        kjør stash-macos for å sette opp i arbeidskatalogen", + "config_path_logic_explained": "Stash prøver først å finne konfigurasjonsfilen sin (config.yml) fra gjeldende arbeidsmappe, og hvis den ikke finner den der, går den tilbake til {fallback_path}. Du kan også få Stash til å lese fra en spesifikk konfigurasjonsfil ved å kjøre den med alternativene -c '' eller --config ''.", + "unable_to_locate_config": "Hvis du leser dette, fant ikke Stash en eksisterende konfigurasjon. Denne veiviseren vil veilede deg gjennom prosessen med å sette opp en ny konfigurasjon." + }, + "welcome_to_stash": "Velkommen til Stash" + }, + "stashbox": { + "submission_successful": "Innlevering vellykket", + "go_review_draft": "Gå til {endpoint_name} for å se gjennom utkastet.", + "selected_stash_box": "Valgt Stash-Box-endepunkt", + "source": "Stash-Box-kilde", + "submission_failed": "Innsending mislyktes", + "submit_update": "Finnes allerede i {endpoint_name}" + }, + "performer_image": "Skuespiller Bilde", + "countables": { + "images": "{antall, flertall, ett {bilde} andre {bilder}}", + "files": "{antall, flertall, én {fil} andre {filer}}", + "galleries": "{antall, flertall, ett {galleri} andre {gallerier}}", + "markers": "{antall, flertall, én {markør} andre {markører}}", + "scenes": "{antall, flertall, én {Scene} andre {Scener}}", + "studios": "{antall, flertall, ett {studio} andre {studioer}}", + "tags": "{antall, flertall, én {Tag} andre {Tags}}", + "groups": "{antall, flertall, én {gruppe} andre {grupper}}", + "performers": "{antall, flertall, én {utøver} andre {utøvere}}" + }, + "sort_name": "Sorter Navn", + "include_sub_studios": "Inkluder datterselskapsstudioer", + "include_sub_tag_content": "Inkluder innhold i undertagger", + "include_sub_tags": "Inkluder undertagger", + "include_sub_studio_content": "Inkluder innhold fra understudioer", + "dimensions": "Dimensjoner", + "criterion_modifier_values": { + "any_of": "noen av", + "none": "Ingen", + "only": "Bare", + "any": "Hvilken som helst" + }, + "configuration": "Konfigurasjon", + "connection_monitor": { + "websocket_connection_failed": "Klarte ikke å opprette websocket-tilkobling: se nettleserkonsollen for detaljer", + "websocket_connection_reestablished": "Websocket-tilkoblingen er gjenopprettet" + }, + "containing_group": "Inneholder gruppe", + "containing_group_count": "Inneholder gruppeantall", + "containing_groups": "Inneholder grupper", + "country": "Land", + "cover_image": "Forsidebilde", + "created_at": "Opprettet", + "criterion": { + "greater_than": "Større enn", + "less_than": "Mindre enn", + "value": "Verdi" + }, + "criterion_modifier": { + "between": "mellom", + "equals": "Er", + "format_string": "{kriterium} {modifikatorString} {verdiString}", + "format_string_excludes": "{kriterium} {modifierString} {valueString} (ekskluderer {excludedString})", + "format_string_excludes_depth": "{kriterium} {modifierString} {valueString} (ekskluderer {excludedString}) (+{dybde, flertall, =-1 {all} andre {{dybde}}})", + "includes": "inkluderer", + "includes_all": "inkluderer alle", + "is_null": "er null", + "less_than": "er mindre enn", + "matches_regex": "samsvarer med regulært uttrykk", + "not_between": "ikke mellom", + "not_equals": "er ikke", + "greater_than": "er større enn", + "format_string_depth": "{kriterium} {modifierString} {valueString} (+{dybde, flertall, =-1 {all} other {{dybde}}})", + "excludes": "ekskluderer", + "not_matches_regex": "samsvarer ikke med regex", + "not_null": "er ikke null" + }, + "custom": "Tilpasset", + "date": "Dato", + "date_format": "ÅÅÅÅ-MM-DD", + "datetime_format": "ÅÅÅÅ-MM-DD TT:MM", + "death_date": "Dødsdato", + "death_year": "Dødsår", + "descending": "Synkende", + "description": "Beskrivelse", + "detail": "Detalj", + "details": "Detaljer", + "developmentVersion": "Utviklingsversjon", + "disambiguation": "Presisering", + "duration": "Varighet", + "ethnicity": "Etnisitet", + "existing_value": "eksisterende verdi", + "eye_color": "Øyefarge", + "fake_tits": "Falske pupper", + "false": "Falsk", + "favourite": "Favoritt", + "file_count": "Antall filer", + "file_mod_time": "Filendringstid", + "files": "Filer", + "files_amount": "{value} filer", + "filesize": "Filstørrelse", + "filter": "Filter", + "filter_name": "Filternavn", + "filters": "Filtre", + "folder": "Mappe", + "framerate": "Bildefrekvens", + "frames_per_second": "{verdi} fps", + "front_page": { + "types": { + "saved_filter": "Lagrede filter", + "premade_filter": "Forhåndslaget filter" + } + }, + "galleries": "Gallerier", + "gallery": "Galleri", + "gallery_count": "Antall Galleri", + "gender": "Kjønn", + "gender_types": { + "FEMALE": "Kvinne", + "INTERSEX": "Intersex", + "MALE": "Mann", + "NON_BINARY": "Ikke Binær", + "TRANSGENDER_MALE": "Transgender Mann", + "TRANSGENDER_FEMALE": "Transgender kvinne" + }, + "group": "Gruppe", + "group_count": "Gruppetall", + "handy_connection_status": { + "connecting": "Kobler til", + "disconnected": "Koblet fra", + "missing": "Mangler", + "ready": "Klar", + "syncing": "Synkroniserer med server", + "uploading": "Laster opp skript", + "error": "Feil ved tilkobling til Handy" + }, + "hasChapters": "Har Kapitler", + "hasMarkers": "Har Markører", + "height": "Høyde", + "height_cm": "Høyde (cm)", + "help": "Hjelp", + "ignore_auto_tag": "Ignorer Automatisk Tagging", + "image": "Bilde", + "image_count": "Antall Bilder", + "images": "Bilder", + "include_parent_tags": "Inkluder overordnede tagger", + "include_sub_groups": "Inkluder undergrupper", + "index_of_total": "{indeks} av {totalt}", + "instagram": "Instagram", + "interactive": "Interaktiv", + "interactive_speed": "Interaktiv hastighet", + "isMissing": "Mangler", + "last_played_at": "Sist Spilt", + "loading": { + "generic": "Laster inn …", + "plugins": "Laster inn plugins …" + }, + "login": { + "login": "Logg inn", + "username": "Brukernavn", + "password": "Passord", + "invalid_credentials": "Ugyldig brukernavn eller passord", + "internal_error": "Uventet intern feil. Se loggene for mer informasjon" + }, + "marker_count": "Antall markører", + "markers": "Markører", + "measurements": "Mål", + "metadata": "Metadata", + "name": "Navn", + "new": "Ny", + "none": "Ingen", + "o_history": "O Historie", + "odate_recorded_no": "Ingen O-dato registrert", + "operations": "Operasjoner", + "organized": "Organisert", + "orientation": "Orientering", + "pagination": { + "current_total": "{nåværende} av {totalt}", + "first": "Første", + "last": "Siste", + "next": "Neste", + "previous": "Tilbake" + }, + "parent_of": "Forelder til {barn}", + "parent_studio": "Foreldrestudio", + "parent_studios": "Foreldrestudio", + "parent_tag_count": "Antall foreldremerker", + "parent_tags": "Foreldre Tagger", + "penis_length_cm": "Penis Lengde (cm)", + "perceptual_similarity": "Perseptuell Likhet (pHash)", + "performer": "Skuespiller", + "performer_age": "Skuespiller Alder", + "performer_count": "Skuespiller Antall", + "performers": "Skuespillere", + "photographer": "Fotograf", + "piercings": "Piercinger", + "playdate_recorded_no": "Ingen avspillningsdatodato registrert", + "plays": "{verdi} avspillinger", + "primary_file": "Primær fil", + "primary_tag": "Primær Tag", + "random": "Tilfeldig", + "recently_added_objects": "Nylig lagt til {objekter}", + "recently_released_objects": "Nylig utgitte {objekter}", + "resolution": "Oppløsning", + "resume_time": "Gjenoppta tid", + "sceneTagger": "Scene Tagger", + "scene_code": "Studio Kode", + "scene_count": "Antall Scener", + "scene_created_at": "Scene opprettet", + "scene_date": "Dato for scenen", + "scene_id": "Scene-ID", + "scene_tags": "Scene Tagger", + "scene_updated_at": "Scene Oppdatert", + "scenes": "Scener", + "scenes_updated_at": "Scener Oppdatert", + "second": "Sekund", + "seconds": "Sekunder", + "settings": "Innstillinger", + "stash_id": "Stash ID", + "stash_id_endpoint": "Stash ID-sluttpunkt", + "stash_ids": "Stash-ID-er", + "statistics": "Statistikk", + "status": "Status: {statusTekst}", + "studio": "Studio", + "studio_and_parent": "Studio og foreldre", + "studio_count": "Antall Studio", + "donate": "Donere", + "file": "fil", + "empty_server": "Legg til noen scener på serveren din for å se anbefalinger på denne siden.", + "penis_length": "Penis Lengde", + "performer_favorite": "Favoritt Skuespiller", + "eta": "ETA", + "performer_tags": "Skuespiller Tagger", + "play_duration": "Spillevarighet", + "hair_color": "Hårfarge", + "include_sub_group_content": "Inkluder innhold i undergrupper", + "o_count": "O Antall", + "toast": { + "merged_tags": "Sammenslåtte tagger", + "generating_screenshot": "Genererer skjermbilde …", + "added_generation_job_to_queue": "Lagt til genereringsjobb i køen", + "created_entity": "Opprettet {entity}", + "default_filter_set": "Standard filtersett", + "delete_past_tense": "Slettet {count, flertall, en {{singularEntity}} annen {{pluralEntity}}}", + "image_index_too_large": "Feil: Bildeindeksen er større enn antallet bilder i galleriet", + "merged_scenes": "Sammenslåtte scener", + "rescanning_entity": "Skanner på nytt {count, flertall, én {{singularEntity}} annen {{pluralEntity}}}…", + "saved_entity": "Lagret {entity}", + "started_generating": "Begynte å generere", + "started_importing": "Begynte å importere", + "updated_entity": "Oppdatert {entity}", + "started_auto_tagging": "Startet automatisk merking", + "removed_entity": "Fjernet {count, flertall, én {{singularEntity}} annen {{pluralEntity}}}", + "added_entity": "Lagt til {count, flertall, én {{singularEntity}} annen {{pluralEntity}}}", + "reassign_past_tense": "Filen er tildelt på nytt" + }, + "time_end": "Sluttid", + "sub_tags": "Undertagger", + "tag_count": "Antall Tagger", + "total": "Total", + "updated_at": "Oppdatert", + "url": "URL", + "urls": "URLs", + "validation": { + "blank": "${path} må ikke være tomt", + "end_time_before_start_time": "Sluttiden må være større enn eller lik starttiden", + "required": "${path} er et obligatorisk felt", + "unique": "${path} må være unik", + "date_invalid_form": "${path} må være i formatet ÅÅÅÅ-MM-DD" + }, + "video_codec": "Videokodek", + "videos": "Videoer", + "view_all": "Vis Alle", + "weight": "Vekt", + "weight_kg": "Vekt (kg)", + "studio_tags": "Studio Tagger", + "studios": "Studioer", + "sub_group_count": "Antall undergrupper", + "sub_group_of": "Undergruppe av {forelder}", + "sub_group_order": "Undergruppeordre", + "sub_groups": "Undergrupper", + "sub_tag_count": "Antall undertagger", + "sub_tag_of": "Undertagg av {parent}", + "subsidiary_studio_count": "Antall datterselskaper i studioer", + "subsidiary_studios": "Datterselskapsstudioer", + "synopsis": "Synopsis", + "tag": "Tag", + "tag_sub_tag_tooltip": "Har under-tagger", + "tags": "Tagger", + "tattoos": "Tatoveringer", + "time": "Tid", + "title": "Tittel", + "true": "Sant", + "twitter": "X", + "type": "Type", + "unknown_date": "Ukjent dato", + "years_old": "år gammel", + "zip_file_count": "Antall zip-filer", + "tag_parent_tooltip": "Har foreldretagger", + "sub_group": "Undergruppe" } diff --git a/ui/v2.5/src/locales/nl-NL.json b/ui/v2.5/src/locales/nl-NL.json index ceaf2ab27..b05de034f 100644 --- a/ui/v2.5/src/locales/nl-NL.json +++ b/ui/v2.5/src/locales/nl-NL.json @@ -141,7 +141,18 @@ "make_primary": "Als primair aanduiden", "reload": "Herladen", "copy_to_clipboard": "Kopiëren naar klembord", - "swap": "Omwisselen" + "swap": "Omwisselen", + "sidebar": { + "close": "Sluit zijbalk", + "open": "Open zijbalk", + "toggle": "Schakelaar Zijbalk" + }, + "show_results": "Resultaat tonen", + "show_count_results": "{count} resultaten tonen", + "play": "Afspelen", + "load_filter": "Laad filter", + "load": "Laden", + "add_stash_id": "Stash ID toevoegen" }, "actions_name": "Acties", "age": "Leeftijd", @@ -175,11 +186,12 @@ "set_cover_label": "Scèneomslag instellen", "set_tag_desc": "Voorzie een scène van labels door ze te overschrijven of samen te voegen met reeds aanwezige.", "set_tag_label": "Labels instellen", - "show_male_desc": "Geef aan of mannen gelabeld mogen worden.", - "show_male_label": "Mannen tonen", "source": "Bron", "mark_organized_label": "Markeren als geordend na opslaan", - "mark_organized_desc": "Markeer een scène als geordend na klikken op opslaan." + "mark_organized_desc": "Markeer een scène als geordend na klikken op opslaan.", + "errors": { + "blacklist_duplicate": "Dubbel zwarte lijst item" + } }, "noun_query": "Zoekvraag", "results": { @@ -279,7 +291,9 @@ "password_desc": "Wachtwoord om je verzameling te openen. Laat leeg om inloggen uit te schakelen", "stash-box_integration": "Stash-boxintegratie", "username": "Gebruikersnaam", - "username_desc": "Gebruikersnaam om je verzameling te openen. Laat leeg om inloggen uit te schakelen" + "username_desc": "Gebruikersnaam om je verzameling te openen. Laat leeg om inloggen uit te schakelen", + "log_file_max_size": "Maximale loggrootte", + "log_file_max_size_desc": "Maximale grootte in megabytes van het logbestand voordat het wordt gecomprimeerd. 0 MB is uitgeschakeld. Vereist herstart." }, "cache_location": "Locatie van de cachemap. Vereist als je streamt via HLS (zoals op Apple apparaten) of DASH.", "cache_path_head": "Cache pad", @@ -343,12 +357,59 @@ "database": "Database", "ffmpeg": { "download_ffmpeg": { - "heading": "Download FFmpeg" + "heading": "FFmpeg downloaden", + "description": "Downloadt FFmpeg naar de configuratiemap en wist de ffmpeg- en ffprobe-paden zodat deze uit de configuratiemap kunnen worden opgehaald." }, "hardware_acceleration": { - "heading": "FFmpeg hardwarecodering" + "heading": "FFmpeg hardwarecodering", + "desc": "Gebruikt beschikbare hardware om video te coderen voor live transcodering." + }, + "ffmpeg_path": { + "heading": "FFmpeg uitvoerbaar pad", + "description": "Pad naar het uitvoerbare bestand van ffmpeg (niet alleen de map). Indien leeg, wordt ffmpeg vanuit de omgeving opgelost via $PATH, de configuratiemap of vanuit $HOME/.stash" + }, + "ffprobe_path": { + "heading": "FFprobe uitvoerbaar pad", + "description": "Pad naar het uitvoerbare bestand ffprobe (niet alleen de map). Indien leeg, wordt ffprobe vanuit de omgeving opgelost via $PATH, de configuratiemap of vanuit $HOME/.stash" + }, + "live_transcode": { + "input_args": { + "desc": "Geavanceerd: Extra argumenten om door te geven aan ffmpeg vóór het invoerveld bij het live transcoderen van video.", + "heading": "FFmpeg Live Transcode Invoer Argumenten" + }, + "output_args": { + "desc": "Geavanceerd: Extra argumenten om door te geven aan ffmpeg vóór het uitvoerveld bij het live transcoderen van video.", + "heading": "FFmpeg Live Transcode Uitvoer Argumenten" + } + }, + "transcode": { + "input_args": { + "desc": "Geavanceerd: Extra argumenten om door te geven aan ffmpeg vóór het invoerveld bij het genereren van video.", + "heading": "FFmpeg Transcode Invoer Argumenten" + }, + "output_args": { + "heading": "FFmpeg Transcode Uitvoer Argumenten", + "desc": "Geavanceerd: Extra argumenten die aan ffmpeg moeten worden doorgegeven vóór het uitvoerveld bij het genereren van video." + } } - } + }, + "blobs_storage": { + "heading": "Type binaire gegevensopslag", + "description": "Waar binaire gegevens zoals scènecovers, performer-, studio- en tagafbeeldingen worden opgeslagen. Nadat u deze waarde hebt gewijzigd, moeten de bestaande gegevens worden gemigreerd met behulp van de taken Blobs migreren. Zie de pagina Taken voor meer informatie over migratie." + }, + "blobs_path": { + "description": "Locatie in het bestandssysteem waar binaire gegevens worden opgeslagen. Alleen van toepassing bij gebruik van het Bestandssysteem blob-opslagtype. WAARSCHUWING: om dit te wijzigen, moeten bestaande gegevens handmatig worden verplaatst.", + "heading": "Pad naar binair gegevensbestandssysteem" + }, + "heatmap_generation": "Funscript Heatmap Generatie", + "gallery_cover_regex_desc": "Regexp gebruikt om een afbeelding te identificeren als galerijomslag", + "plugins_path": { + "description": "Map locatie van plugin-configuratiebestanden", + "heading": "Plugin Pad" + }, + "gallery_cover_regex_label": "Galerie omslagpatroon", + "funscript_heatmap_draw_range": "Bereik opnemen in gegenereerde heatmaps", + "funscript_heatmap_draw_range_desc": "Teken het bewegingsbereik op de y-as van de gegenereerde heatmap. Bestaande heatmaps moeten na wijziging opnieuw worden gegenereerd." }, "library": { "exclusions": "Uitzonderingen", @@ -360,7 +421,9 @@ }, "plugins": { "hooks": "Triggers", - "triggers_on": "Reageert op" + "triggers_on": "Reageert op", + "available_plugins": "Beschikbare Plugins", + "installed_plugins": "Geïnstalleerde Plugins" }, "scraping": { "entity_metadata": "{entityType} Metadata", @@ -371,7 +434,9 @@ "scrapers": "Schrapers", "search_by_name": "Zoek op naam", "supported_types": "Ondersteunde types", - "supported_urls": "URL's" + "supported_urls": "URL's", + "installed_scrapers": "Geïnstalleerde Schrapers", + "available_scrapers": "Beschikbare schrapers" }, "stashbox": { "add_instance": "Voeg stash-box instance toe", @@ -380,7 +445,9 @@ "endpoint": "Eindpunt", "graphql_endpoint": "GraphQL eindpunt", "name": "Naam", - "title": "Stash-box Eindpunten" + "title": "Stash-box Eindpunten", + "max_requests_per_minute_description": "Gebruikt de standaardwaarde van {defaultValue} als deze is ingesteld op 0", + "max_requests_per_minute": "Max aanvragen per minuut" }, "system": { "transcoding": "Transcoderen" @@ -434,7 +501,8 @@ "source": "Bron", "source_options": "{source} Opties", "sources": "Bronnen", - "strategy": "Strategie" + "strategy": "Strategie", + "skip_multiple_matches": "Sla overeenkomsten met meer dan één resultaat over" }, "import_from_exported_json": "Import van geëxporteerde JSON in de map metadata. Maakt de bestaande database leeg.", "incremental_import": "Incrementele import uit een meegeleverde export zip-bestand.", @@ -449,7 +517,30 @@ "scanning_paths": "Scannen van de volgende paden" }, "scan_for_content_desc": "Scan naar nieuwe inhoud en voeg deze toe aan de database.", - "set_name_date_details_from_metadata_if_present": "Stel de naam, datum, details in vanuit Embedded File Metadata" + "set_name_date_details_from_metadata_if_present": "Stel de naam, datum, details in vanuit Embedded File Metadata", + "clean_generated": { + "blob_files": "Blob bestanden", + "image_thumbnails_desc": "Afbeeldingsminiaturen en clips", + "markers": "Markeer Voorbeelden", + "previews": "Scene Voorbeelden", + "sprites": "Scène Sprites", + "transcodes": "Scène Transcoderingen", + "description": "Verwijdert gegenereerde bestanden zonder bijbehorende database-invoer.", + "previews_desc": "Scene voorbeelden en afbeeldingsminiaturen", + "image_thumbnails": "Afbeeldingsminiaturen" + }, + "anonymising_database": "Anonimiseer database", + "anonymise_database": "Maakt een kopie van de database naar de backups-map, waarbij alle gevoelige gegevens anoniem worden gemaakt. Deze kan vervolgens aan anderen worden verstrekt voor probleemoplossing en debugging. De originele database wordt niet gewijzigd. De geanonimiseerde database gebruikt de bestandsnaamindeling {filename_format}.", + "anonymise_and_download": "Maakt een geanonimiseerde kopie van de database en downloadt het resulterende bestand.", + "generate_clip_previews_during_scan": "Genereer voorbeelden van Afbeelingsfragmenten", + "migrate_blobs": { + "delete_old": "Verwijder oude gegevens" + }, + "migrate_scene_screenshots": { + "delete_files": "Verwijder screenshotbestanden" + }, + "generate_sprites_during_scan_tooltip": "De set afbeeldingen die onder de videospeler worden weergegeven voor eenvoudige navigatie.", + "generate_video_covers_during_scan": "Scène-covers genereren" }, "tools": { "scene_duplicate_checker": "Scène Duplicator Checker", @@ -468,7 +559,9 @@ "whitespace_chars": "WhiteSpace-tekens", "whitespace_chars_desc": "Deze tekens worden vervangen door witruimte in de titel" }, - "scene_tools": "Scene gereedschap" + "scene_tools": "Scene gereedschap", + "graphql_playground": "GraphQL speeltuin", + "heading": "Hulpmiddelen" }, "ui": { "basic_settings": "Basis instellingen", @@ -497,11 +590,27 @@ "description": "Verwijder de mogelijkheid om nieuwe objecten te maken uit de dropdown menu", "heading": "Schakel het maken van dropdowns uit" }, - "heading": "Aanpassen" + "heading": "Aanpassen", + "rating_system": { + "type": { + "options": { + "decimal": "Decimaal", + "stars": "Sterren" + } + }, + "star_precision": { + "options": { + "full": "Vol", + "half": "Half", + "quarter": "Kwart", + "tenth": "Tiende" + } + } + } }, "funscript_offset": { "description": "Time Offset in milliseconden voor het afspelen van interactieve scripts.", - "heading": "Funscript Offset (ms)" + "heading": "" }, "handy_connection": { "connect": "Connecteer", @@ -515,7 +624,7 @@ }, "handy_connection_key": { "description": "Handy connection key om te gebruiken voor interactieve scènes. Instellen van deze sleutel staat Stash toe om uw huidige scène-informatie met HandyFeeling.com te delen", - "heading": "Handy Connection Key" + "heading": "" }, "image_lightbox": { "heading": "Afbeelding Lightbox" @@ -575,7 +684,9 @@ "continue_playlist_default": { "description": "Speel de volgende scène in de wachtrij wanneer video is voltooid", "heading": "Ga Standaard door met de afspeellijst" - } + }, + "always_start_from_beginning": "Start video altijd vanaf het begin", + "enable_chromecast": "Chromecast inschakelen" } }, "scene_wall": { @@ -593,7 +704,29 @@ "description": "Diavoorstelling is beschikbaar in galerijen in de muurweergavemodus", "heading": "Diavoorstellingsvertraging (in seconden)" }, - "title": "Gebruikers interface" + "title": "Gebruikers interface", + "custom_javascript": { + "heading": "Aangepaste JavaScript", + "option_label": "Aangepaste JavaScript ingeschakeld" + }, + "custom_locales": { + "heading": "Aangepaste lokalisatie", + "option_label": "Aangepaste lokalisatie ingeschakeld" + }, + "detail": { + "enable_background_image": { + "description": "Achtergrondfoto op detailscherm weergeven.", + "heading": "Achtergrondfoto inschakelen" + }, + "heading": "Detailscherm", + "show_all_details": { + "heading": "Alle details weergeven" + } + }, + "image_wall": { + "margin": "Marge (pixels)", + "direction": "Richting" + } }, "advanced_mode": "Geavanceerde modus" }, @@ -602,11 +735,11 @@ "files": "{count, plural, one {Bestand} other {Bestanden}}", "galleries": "{count, plural, one {Galerij} other {Galerijen}}", "images": "{count, plural, one {Afbeelding} other {Afbeeldingen}}", - "markers": "{count, plural, one {Marker} other {Markers}}", - "performers": "{count, plural, one {Performer} other {Performers}}", - "scenes": "{count, plural, one {Scene} other {Scenes}}", - "studios": "{count, plural, one {Studio} other {Studios}}", - "tags": "{count, plural, one {Tag} other {Tags}}" + "markers": "", + "performers": "", + "scenes": "", + "studios": "", + "tags": "" }, "country": "Land", "cover_image": "Cover afbeelding", @@ -678,7 +811,6 @@ "destination": "Bestemming", "source": "Afkomst" }, - "overwrite_filter_confirm": "Weet u zeker dat u de bestaande opgeslagen zoekopdracht {entityName} wilt overschrijven?", "scene_gen": { "force_transcodes": "Genereren van transcode forceren", "force_transcodes_tooltip": "Standaard worden transcodes alleen gegenereerd als het videobestand niet wordt ondersteund in de browser. Indien ingeschakeld, worden transcodes gegenereerd, zelfs als het videobestand in de browser lijkt te worden ondersteund.", @@ -707,12 +839,14 @@ "preview_seg_count_head": "Aantal segmenten in voorbeeld", "preview_seg_duration_desc": "Duur van elk voorbeeldsegment, in seconden.", "preview_seg_duration_head": "Voorbeeld Segment Duur", - "sprites": "Scene Scrubber Sprites", + "sprites": "Scène Scrubber Sprites", "sprites_tooltip": "De set afbeeldingen die onder de videospeler worden weergegeven voor eenvoudige navigatie.", "transcodes": "Transcoderingen", "transcodes_tooltip": "MP4-conversies van niet-ondersteunde video-indelingen", "video_previews": "Voorbeelden", - "video_previews_tooltip": "Videovoorbeelden die worden afgespeeld wanneer u over een scène beweegt" + "video_previews_tooltip": "Videovoorbeelden die worden afgespeeld wanneer u over een scène beweegt", + "covers": "Scène-covers", + "image_thumbnails": "Afbeeldingsminiaturen" }, "scenes_found": "{count} scenes gevonden", "scrape_entity_query": "{entity_type} Schraper Query", @@ -720,7 +854,18 @@ "scrape_results_existing": "Bestaande", "scrape_results_scraped": "Geschraapt", "set_image_url_title": "Afbeelding URL", - "unsaved_changes": "Niet-opgeslagen wijzigingen gaan verloren. Weet je zeker dat je wilt vertrekken?" + "unsaved_changes": "Niet-opgeslagen wijzigingen gaan verloren. Weet je zeker dat je wilt vertrekken?", + "merge": { + "destination": "Bestemming", + "source": "Bron" + }, + "performers_found": "{count} artiesten gevonden", + "imagewall": { + "direction": { + "column": "Kolom", + "row": "Rij" + } + } }, "dimensions": "Dimensies", "director": "Regisseur", @@ -742,7 +887,12 @@ "medium": "Medium" }, "search_accuracy_label": "Zoek accuraatheid", - "title": "Dubbele Scènes" + "title": "Dubbele Scènes", + "duration_options": { + "equal": "Gelijk" + }, + "select_none": "Niets selecteren", + "select_options": "Selecteer Opties…" }, "duplicated_phash": "Gedupliceerd (phash)", "duration": "Looptijd", @@ -783,11 +933,11 @@ "filter_name": "Filter Naam", "filters": "Filters", "framerate": "Frame snelheid", - "frames_per_second": "{value} frames per seconde", + "frames_per_second": "{value} fps", "front_page": { "types": { "premade_filter": "Vooraf gemaakte filter", - "saved_filter": "Opgeslagen Filter" + "saved_filter": "Opgeslagen filter" } }, "galleries": "Galerijen", @@ -798,11 +948,11 @@ "FEMALE": "Vrouw", "INTERSEX": "Intersex", "MALE": "Man", - "NON_BINARY": "Non-Binar", + "NON_BINARY": "Non-binair", "TRANSGENDER_FEMALE": "Transgender Vrouw", "TRANSGENDER_MALE": "Transgender Man" }, - "hair_color": "Haar kleur", + "hair_color": "Haarkleur", "handy_connection_status": { "connecting": "Verbinden", "disconnected": "Verbinding verbroken", @@ -812,7 +962,7 @@ "syncing": "Synchroniseren met server", "uploading": "Script uploaden" }, - "hasMarkers": "Heeft Markeringen", + "hasMarkers": "Markeringen", "height": "Hoogte", "help": "Help", "ignore_auto_tag": "Negeer automatische tag", @@ -828,7 +978,8 @@ "isMissing": "Is Missende", "library": "Bibliotheek", "loading": { - "generic": "Laden…" + "generic": "Laden…", + "plugins": "Plugins laden…" }, "marker_count": "Marker Aantal", "markers": "Markeringen", @@ -841,18 +992,17 @@ "interactive_speed": "Interactieve snelheid", "performer_card": { "age": "{age} {years_old}", - "age_context": "{age} {years_old} in deze scène" + "age_context": "{age} {years_old} bij productie" }, "phash": "PHash", "stream": "Stream", "video_codec": "Video Codec" }, - "megabits_per_second": "{value} megabits per seconde", + "megabits_per_second": "{value} mbps", "metadata": "Metadata", "name": "Naam", "new": "Nieuw", "none": "Geen", - "o_counter": "O-Teller", "operations": "Operaties", "organized": "Georganiseerd", "pagination": { @@ -950,7 +1100,7 @@ }, "paths": { "database_filename_empty_for_default": "database bestandsnaam (leeg als standaard)", - "description": "Vervolgens moeten we bepalen waar we je pornocollectie kunnen vinden, waar we de stash-database en gegenereerde bestanden kunnen opslaan. Deze instellingen kunnen indien nodig later worden gewijzigd.", + "description": "Vervolgens moeten we bepalen waar we je collectie kunnen vinden, waar we de stash-database en gegenereerde bestanden kunnen opslaan. Deze instellingen kunnen indien nodig later worden gewijzigd.", "path_to_generated_directory_empty_for_default": "pad naar gegenereerde map (standaard leeg)", "set_up_your_paths": "Stel je paden in", "stash_alert": "Er zijn geen bibliotheekpaden geselecteerd. Er kan dan geen media worden gescand in Stash. Weet je zeker dat?", @@ -959,7 +1109,7 @@ "where_can_stash_store_its_generated_content": "Waar kan Stash de gegenereerde inhoud opslaan?", "where_can_stash_store_its_generated_content_description": "Om thumbnails, previews en sprites aan te bieden, genereert Stash afbeeldingen en video's. Dit omvat ook transcodes voor niet-ondersteunde bestandsindelingen. Standaard zal Stash een generated directory aanmaken in de directory die uw configuratiebestand bevat. Als u wilt wijzigen waar deze gegenereerde media wordt opgeslagen, voert u een absoluut of relatief (ten opzichte van de huidige werkmap) pad in. Stash maakt deze map aan als deze nog niet bestaat.", "where_is_your_porn_located": "Waar staat je porno?", - "where_is_your_porn_located_description": "Voeg mappen toe die uw pornovideo's en afbeeldingen bevatten. Stash gebruikt deze mappen om video's en afbeeldingen te vinden tijdens het scannen." + "where_is_your_porn_located_description": "Voeg mappen toe die uw video's en afbeeldingen bevatten. Stash gebruikt deze mappen om video's en afbeeldingen te vinden tijdens het scannen." }, "stash_setup_wizard": "Stash-installatiewizard", "success": { @@ -996,7 +1146,8 @@ "stats": { "image_size": "Afbeelding groote", "scenes_duration": "Scene duur", - "scenes_size": "Scene groote" + "scenes_size": "Scene groote", + "scenes_played": "Scènes gespeeld" }, "status": "Status: {statusText}", "studio": "Studio", @@ -1048,5 +1199,80 @@ "circumcised_types": { "CUT": "Ja", "UNCUT": "Nee" - } + }, + "folder": "Folder", + "hasChapters": "Hoofdstukken", + "image_index": "Afbeelding #", + "include_sub_groups": "Inclusief subgroepen", + "index_of_total": "{index} van {total}", + "orientation": "Oriëntatie", + "group": "Groep", + "group_count": "Aantal groepen", + "height_cm": "Lengte (cm)", + "history": "Geschiedenis", + "groups": "Groepen", + "file_count": "Aantal bestanden", + "files_amount": "{value} bestanden", + "include_sub_group_content": "Inclusief subgroep inhoud", + "login": { + "username": "Gebruikersnaam", + "password": "Wachtwoord", + "login": "Login", + "invalid_credentials": "Ongeldige gebruikersnaam of wachtwoord" + }, + "group_scene_number": "Scènenummer", + "include_sub_studio_content": "Inclusief substudio inhoud", + "last_played_at": "Laatst gespeeld op", + "include_sub_tag_content": "Inclusief sub-tag inhoud", + "age_on_date": "{age} tijdens productie", + "studio_tagger": { + "query_all_studios_in_the_database": "Alle studio's in de database", + "current_page": "Huidige pagina", + "status_tagging_job_queued": "Status: Tagging-taak in de wachtrij", + "config": { + "no_instances_found": "Geen gevallen gevonden", + "these_fields_will_not_be_changed_when_updating_studios": "Deze velden worden niet gewijzigd bij het updaten van studio's." + }, + "failed_to_save_studio": "Opslaan van studio “{studio}” mislukt", + "name_already_exists": "Naam bestaat reeds", + "network_error": "Netwerkfout", + "no_results_found": "Geen resultaten gevonden.", + "number_of_studios_will_be_processed": "{studio_count} studio's worden verwerkt", + "refresh_tagged_studios": "Vernieuwen getagde studio's", + "refreshing_will_update_the_data": "Vernieuwen zal de gegevens van alle getagde studio's van de stash-box bijwerken.", + "create_or_tag_parent_studios": "Maak ontbrekende of label bestaande moederstudio's" + }, + "zip_file_count": "Aantal Zipbestanden", + "unknown_date": "Onbekende datum", + "urls": "URL's", + "date_format": "JJJJ-MM-DD", + "description": "Omschrijving", + "distance": "Afstand", + "package_manager": { + "description": "Omschrijving", + "install": "Installeer", + "package": "Pakket", + "source": { + "name": "Naam" + }, + "uninstall": "Verwijderen", + "unknown": "", + "update": "Updaten", + "version": "Versie" + }, + "penis": "Penis", + "photographer": "Fotograaf", + "second": "Seconde", + "statistics": "Statistieken", + "time": "Tijd", + "criterion_modifier_values": { + "none": "Geen" + }, + "custom_fields": { + "field": "Veld", + "value": "Waarde" + }, + "datetime_format": "YYYY-MM-DD HH:MM", + "sub_group": "Subgroep", + "sub_groups": "Subgroepen" } diff --git a/ui/v2.5/src/locales/nn-NO.json b/ui/v2.5/src/locales/nn-NO.json index 81d94cf8e..995336e58 100644 --- a/ui/v2.5/src/locales/nn-NO.json +++ b/ui/v2.5/src/locales/nn-NO.json @@ -102,7 +102,13 @@ "performers_found": "Fann {count} utøvarar", "delete_entity_title": "{count, plural, one {Slett {singularEntity}} other {Slett {pluralEntity}}}", "scenes_found": "Fann {count} scener", - "dont_show_until_updated": "Ikkje vis før neste oppdatering" + "dont_show_until_updated": "Ikkje vis før neste oppdatering", + "imagewall": { + "direction": { + "column": "Kolonne", + "row": "Rad" + } + } }, "date": "Dato", "bitrate": "Bitrate", @@ -143,6 +149,9 @@ } } } + }, + "images": { + "heading": "Bilete" } }, "about": { @@ -199,7 +208,6 @@ "last": "Siste" }, "o_count": "Tal på O", - "o_counter": "O-teljar", "organized": "Organisert", "playdate_recorded_no": "Ingen avspelingsdato er registrert", "play_duration": "Avspelingslengd", @@ -229,5 +237,16 @@ "sub_group": "Undergruppe", "sub_group_count": "Tal på undergrupper", "sub_group_of": "Undergruppe av {parent}", - "sub_group_order": "Undergruppesortert" + "sub_group_order": "Undergruppesortert", + "groups": "Grupper", + "performers": "Utøvarar", + "studios": "Studio", + "image": "Bilete", + "images": "Bilete", + "scene": "Scene", + "group": "Gruppe", + "galleries": "Galleri", + "scenes": "Scener", + "studio": "Studio", + "performer": "Utøvar" } diff --git a/ui/v2.5/src/locales/pl-PL.json b/ui/v2.5/src/locales/pl-PL.json index 794497801..0afd99dc5 100644 --- a/ui/v2.5/src/locales/pl-PL.json +++ b/ui/v2.5/src/locales/pl-PL.json @@ -141,7 +141,9 @@ "remove_from_containing_group": "Usuń z grupy", "reset_cover": "Przywróć domyślną okładkę", "add_sub_groups": "Dodaj podgrupy", - "view_history": "Zobacz historię" + "view_history": "Zobacz historię", + "play": "Odtwarzaj", + "show_results": "Pokaż wyniki" }, "actions_name": "Działania", "age": "Wiek", @@ -187,8 +189,6 @@ "set_cover_label": "Ustawianie obrazu okładki sceny", "set_tag_desc": "Dołączanie tagów do sceny poprzez nadpisywanie lub łączenie z istniejącymi tagami w scenie.", "set_tag_label": "Ustaw tagi", - "show_male_desc": "Włączenie opcji tagowania wykonawców płci męskiej.", - "show_male_label": "Pokaż męskich aktorów", "source": "Źródło", "mark_organized_desc": "Po kliknięciu przycisku Zapisz scena zostaje natychmiast oznaczona jako zorganizowana.", "mark_organized_label": "Oznacz jako zorganizowane przy zapisywaniu" @@ -346,6 +346,9 @@ "desc": "Zaawansowane: Dodatkowe argumenty do przekazania do FFmpeg przed polem wyjściowym podczas generowania wideo.", "heading": "Argumenty wyjścia dla transkodowania z użyciem FFmpeg" } + }, + "download_ffmpeg": { + "heading": "Pobierz FFmpeg" } }, "funscript_heatmap_draw_range": "Bierz pod uwagę zakres dla wygenerowanych heatmap", @@ -832,7 +835,6 @@ "destination": "Cel", "source": "Źródło" }, - "overwrite_filter_confirm": "Czy na pewno chcesz nadpisać istniejące zapisane zapytanie {entityName}?", "reassign_entity_title": "Przypisz {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "reassign_files": { "destination": "Przypisz ponownie do" @@ -1032,7 +1034,6 @@ "name": "Nazwa", "new": "Dodaj", "none": "Brak", - "o_counter": "O-Licznik", "operations": "Operacje", "organized": "Uporządkowany", "pagination": { @@ -1076,7 +1077,6 @@ "no_results_found": "Nie znaleziono wyników.", "number_of_performers_will_be_processed": "{performer_count} wykonawców zostanie przetworzonych", "performer_already_tagged": "Aktor już otagowany", - "performer_names_separated_by_comma": "Nazwy aktorów oddzielone przecinkami", "performer_selection": "Wybór aktorów", "performer_successfully_tagged": "Aktor pomyślnie otagowany:", "query_all_performers_in_the_database": "Wszyscy aktorzy w bazie danych", diff --git a/ui/v2.5/src/locales/pt-BR.json b/ui/v2.5/src/locales/pt-BR.json index b759122d7..64b0e4e4c 100644 --- a/ui/v2.5/src/locales/pt-BR.json +++ b/ui/v2.5/src/locales/pt-BR.json @@ -124,7 +124,28 @@ "use_default": "Usar padrão", "view_random": "Mostrar aleatoriamente", "remove_date": "Remover data", - "view_history": "Visualizar Histórico" + "view_history": "Visualizar Histórico", + "add_manual_date": "Inserir data manualmente", + "add_sub_groups": "Adicionar sub-grupos", + "add_o": "Adicionar O", + "add_play": "Adicionar play", + "add_stash_id": "Adicionar Stash ID", + "choose_date": "Escolher uma data", + "clean_generated": "Limpar os arquivos gerados", + "clear_date_data": "Limpar dados de datas", + "create_new": "Criar novo", + "disable": "Desabilitar", + "enable": "Habilitar", + "load": "Carregar", + "load_filter": "Carregar filtro", + "play": "Assistir", + "remove_from_containing_group": "Remover do Grupo", + "reset_play_duration": "Redefinir duração", + "reset_resume_time": "Redefinir tempo de retomada", + "reset_cover": "Restaurar a capa padrão", + "copy_to_clipboard": "Copiar para a área de transferência", + "encoding_image": "Codificando imagem…", + "reload": "Recarregar" }, "actions_name": "Ações", "age": "Idade", @@ -173,8 +194,6 @@ "set_cover_label": "Definir imagem da capa da cena", "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", @@ -689,7 +708,6 @@ "destination": "Destino", "source": "Fonte" }, - "overwrite_filter_confirm": "Tem certeza de que deseja sobrescrever a consulta salva existente {entityName}?", "scene_gen": { "force_transcodes": "Forçar geração de transcodificação", "force_transcodes_tooltip": "Por padrão, transcodificações são geradas apenas quando o arquivo de vídeo não é suportado pelo navegador. Quando ativado, transcodificações serão geradas mesmo quando o vídeo parecer ser suportado no navegador.", @@ -865,7 +883,6 @@ "name": "Nome", "new": "Novo", "none": "Nenhum", - "o_counter": "O-contador", "operations": "Operações", "organized": "Organizado", "pagination": { @@ -906,7 +923,6 @@ "no_results_found": "Nenhum resultado encontrado.", "number_of_performers_will_be_processed": "{performer_count} artistas serão processados", "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 etiquetado com sucesso:", "query_all_performers_in_the_database": "Todos os artistas no banco de dados", diff --git a/ui/v2.5/src/locales/ro-RO.json b/ui/v2.5/src/locales/ro-RO.json index f3fc389ba..defa78a8d 100644 --- a/ui/v2.5/src/locales/ro-RO.json +++ b/ui/v2.5/src/locales/ro-RO.json @@ -88,7 +88,8 @@ "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." + "import_warning": "Ești sigur că vrei să imporți? Asta va șterge baza de date si va reimporta din metadatele tale exporatate.", + "dry_mode_selected": "\"Modul uscat\" selectat. Nu se va șterge nimic, doar se va loga." }, "temp_disable": "Dezactivează temporar…", "temp_enable": "Activează temporar…", @@ -131,7 +132,18 @@ "reshuffle": "Reamestecă", "scrape_query": "Extrage date", "scrape_scene_fragment": "Extrage fragment cu fragment", - "scrape_with": "Extrage cu…" + "scrape_with": "Extrage cu…", + "selective_clean": "Curățare selectivă", + "selective_scan": "Scanare selectivă", + "set_cover": "Setează ca fundal", + "swap": "Schimbă", + "unset": "Nesetat", + "view_history": "Vezi istoric", + "sidebar": { + "close": "Închide bara laterală", + "open": "Deschide bara laterală" + }, + "split": "Împarte" }, "actions_name": "Acțiuni", "age": "Vârstă", @@ -160,18 +172,27 @@ "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ă" + "source": "Sursă", + "query_mode_metadata": "Date meta", + "mark_organized_label": "Marchează ca Organizat la salvare", + "errors": { + "blacklist_duplicate": "Lucru de pe lista neagră duplicat" + }, + "mark_organized_desc": "Marchează scena ca Organizată imediat după ce butonul de Salvare este apăsat.", + "query_mode_label": "Mod de căutare", + "blacklist_desc": "Lucrurile din lista neagră sunt excluse din căutări. De reținut, căutările sunt expresii si diferențiază între litere mari și mici. Înaintea anumitor caractere, trebuie pus caracterul backslash: {chars_require_escape}" }, "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" + "unnamed": "Fără denumire", + "fp_matches": "Durația este aceeași", + "duration_off": "Durația diferă cu cel puțin {number}s" }, - "verb_toggle_config": "{toggle} {configuration}" + "verb_toggle_config": "{toggle} {configuration}", + "noun_query": "Căutare" }, "config": { "about": { @@ -253,7 +274,6 @@ "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", @@ -372,7 +392,6 @@ "metadata": "Metadate", "name": "Nume", "new": "Nou", - "o_counter": "O-Contor", "operations": "Operațiuni", "organized": "Organizat", "pagination": { @@ -495,5 +514,21 @@ "view_all": "Vezi Toate", "weight": "Greutate", "years_old": "ani", - "containing_group": "Grup aparținător" + "containing_group": "Grup aparținător", + "aliases": "Porecle", + "appears_with": "Apare cu", + "audio_codec": "Codec Audio", + "between_and": "și", + "blobs_storage_type": { + "database": "Bază de date", + "filesystem": "Sistem de fișiere" + }, + "captions": "Subtitrări", + "chapters": "Capitole", + "circumcised_types": { + "CUT": "Circumcis", + "UNCUT": "Necircumcis" + }, + "circumcised": "Circumcizie", + "age_on_date": "{age} la producție" } diff --git a/ui/v2.5/src/locales/ru-RU.json b/ui/v2.5/src/locales/ru-RU.json index b5e6d8dcf..e832e3ed1 100644 --- a/ui/v2.5/src/locales/ru-RU.json +++ b/ui/v2.5/src/locales/ru-RU.json @@ -128,7 +128,7 @@ "assign_stashid_to_parent_studio": "Присвоить Stash ID для текущей родительской студии и обновить метаданные", "add_manual_date": "Добавить дату вручную", "add_o": "Добавить О", - "add_play": "Добавить проигрывание", + "add_play": "Добавить воспроизведение", "choose_date": "Выбрать дату", "clean_generated": "Очистить сгенерированные файлы", "clear_date_data": "Очистить информацию о дате", @@ -139,7 +139,19 @@ "reset_cover": "Восстановить обложку по умолчанию", "set_cover": "Установить как обложку", "add_sub_groups": "Добавить подгруппы", - "remove_from_containing_group": "Удалить из группы" + "remove_from_containing_group": "Удалить из группы", + "reset_play_duration": "Сбросить время воспроизведения", + "reset_resume_time": "Сбросить точку продолжения", + "show_results": "Показать результаты", + "sidebar": { + "toggle": "Показать/скрыть боковую панель", + "open": "Открыть панель", + "close": "Закрыть панель" + }, + "show_count_results": "Показать {count} результат(ов)", + "play": "Воспроизвести", + "load": "Загрузить", + "load_filter": "Загрузить фильтр" }, "actions_name": "Действия", "age": "Возраст", @@ -179,11 +191,12 @@ "set_cover_label": "Выставить обложку для данной сцены", "set_tag_desc": "Прикрепить теги к сцене, перезаписав или соединив с существующими тегами на сцене.", "set_tag_label": "Установить теги", - "show_male_desc": "Включить или выключить доступность пометки тегами мужских актёров.", - "show_male_label": "Показывать актеров мужского пола", "source": "Источник", "mark_organized_label": "Отметить как Организованную при сохранении", - "mark_organized_desc": "Сразу же отметить сцену как Организованную после нажатия кнопки Сохранить." + "mark_organized_desc": "Сразу же отметить сцену как Организованную после нажатия кнопки Сохранить.", + "errors": { + "blacklist_duplicate": "Дублировать элемент чёрного списка" + } }, "noun_query": "Запрос", "results": { @@ -435,7 +448,9 @@ "endpoint": "Конечная точка", "graphql_endpoint": "Конечная точка GraphQL", "name": "Имя", - "title": "Конечные точки Stash-box" + "title": "Конечные точки Stash-box", + "max_requests_per_minute": "Максимальное количество запросов в минуту", + "max_requests_per_minute_description": "Использует значение по умолчанию {defaultValue}, если задано 0" }, "system": { "transcoding": "Транскодирование" @@ -541,7 +556,8 @@ "sprites": "Спрайты Сцен", "transcodes": "Транскоды Сцен" }, - "rescan": "Пересканировать файлы" + "rescan": "Пересканировать файлы", + "rescan_tooltip": "Повторно просканировать все файлы в пути. Используется для обновления метаданных и сканирования ZIP-файлов." }, "tools": { "scene_duplicate_checker": "Проверка сцен на дубликаты", @@ -560,7 +576,9 @@ "whitespace_chars": "Символы пробелов", "whitespace_chars_desc": "Эти символы будут заменены пробелами в названии" }, - "scene_tools": "Инструменты видео" + "scene_tools": "Инструменты видео", + "graphql_playground": "Песочница GraphQL", + "heading": "Инструменты" }, "ui": { "abbreviate_counters": { @@ -694,7 +712,7 @@ } }, "scene_list": { - "heading": "Сетка", + "heading": "Сеточный вид", "options": { "show_studio_as_text": "Отображать названия студии как текст" } @@ -720,7 +738,8 @@ "description": "Кнопка VR будет показана только для сцен с данным тегом.", "heading": "Тег VR" }, - "disable_mobile_media_auto_rotate": "Отключить автоповорот в полноэкранном режиме на мобильных устройствах" + "disable_mobile_media_auto_rotate": "Отключить автоповорот в полноэкранном режиме на мобильных устройствах", + "show_range_markers": "Показать маркеры диапазона" } }, "scene_wall": { @@ -883,7 +902,6 @@ "destination": "Назначение", "source": "Источник" }, - "overwrite_filter_confirm": "Вы уверены, что хотите перезаписать существующий сохраненный запрос {entityName}?", "reassign_entity_title": "{count, plural, one {Переназначить {singularEntity}} other {Переназначить {pluralEntity}}}", "reassign_files": { "destination": "Переназначить на" @@ -944,7 +962,9 @@ }, "clear_play_history_confirm": "Вы уверены, что хотите удалить историю просмотра?", "performers_found": "{count} исполнителей найдено", - "clear_o_history_confirm": "Вы уверены, что хотите очистить историю О?" + "clear_o_history_confirm": "Вы уверены, что хотите очистить историю О?", + "overwrite_filter_warning": "Сохранённый фильтр «{entityName}» будет перезаписан.", + "set_default_filter_confirm": "Вы уверены, что хотите установить этот фильтр по умолчанию?" }, "dimensions": "Размер", "director": "Режиссер", @@ -954,7 +974,8 @@ "list": "Список", "tagger": "Теггер", "unknown": "Неизвестный", - "wall": "Стена" + "wall": "Стена", + "label_current": "Режим отображения: {current}" }, "donate": "Пожертвование", "dupe_check": { @@ -1070,7 +1091,8 @@ "last_played_at": "Воспроизводился в последний раз", "library": "Библиотека", "loading": { - "generic": "Загрузка…" + "generic": "Загрузка…", + "plugins": "Загрузка плагинов…" }, "marker_count": "Количество маркеров", "markers": "Маркеры", @@ -1097,14 +1119,14 @@ "name": "Имя", "new": "Новый", "none": "Отсутствует", - "o_counter": "О-Счетчик", "operations": "Операции", "organized": "Организован", "pagination": { "first": "Первая", "last": "Последняя", "next": "Следующая", - "previous": "Предыдущий" + "previous": "Предыдущий", + "current_total": "{current} из {total}" }, "parent_of": "Родитель {children}", "parent_studios": "Родительские студии", @@ -1138,7 +1160,6 @@ "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": "Все актеры в базе данных", @@ -1182,7 +1203,9 @@ "name": "Фильтр", "saved_filters": "Сохраненные фильтры", "update_filter": "Обновить фильтр", - "edit_filter": "Изменить фильтр" + "edit_filter": "Изменить фильтр", + "more_filter_criteria": "+ещё {count}", + "search_term": "Поисковый запрос" }, "seconds": "Секунды", "settings": "Настройки", @@ -1204,7 +1227,9 @@ "errors": { "something_went_wrong": "О, нет! Что-то пошло не так!", "something_went_wrong_description": "Если это похоже на проблему с вашими входными данными, нажмите «Назад», чтобы исправить их. В противном случае сообщите об ошибке на {githubLink} или обратитесь за помощью в {discordLink}.", - "something_went_wrong_while_setting_up_your_system": "Что-то пошло не так при настройке вашей системы. Мы получили следующую ошибку: {error}" + "something_went_wrong_while_setting_up_your_system": "Что-то пошло не так при настройке вашей системы. Мы получили следующую ошибку: {error}", + "unexpected_error": "Произошла непредвиденная ошибка: {error}", + "unable_to_retrieve_system_status": "Не удалось получить статус системы: {error}" }, "folder": { "file_path": "Путь файла", @@ -1373,7 +1398,6 @@ "refreshing_will_update_the_data": "Обновление затронет данные всех тегированных студий в данной инсталляции stash-box.", "status_tagging_studios": "Статус: Тегирование студий", "studio_already_tagged": "Студия уже тегирована", - "studio_names_separated_by_comma": "Названия студий, разделенные запятыми", "studio_selection": "Выбор студии", "studio_successfully_tagged": "Студия успешно тегирована", "to_use_the_studio_tagger": "Для использования тегирования студий stash-box нужно настроить.", @@ -1432,7 +1456,15 @@ "image_index_greater_than_zero": "Индекс изображения должен быть больше 0", "header": "Ошибка", "loading_type": "Ошибка загрузки {type}", - "something_went_wrong": "Что-то пошло по пизде." + "something_went_wrong": "Что-то пошло по пизде.", + "custom_fields": { + "field_name_length": "Имя поля должно содержать меньше 65 символов", + "field_name_required": "Имя поля обязательно", + "field_name_whitespace": "Имя поля не должно начинаться или заканчиваться пробелом", + "duplicate_field": "Имя поля должно быть уникальным" + }, + "invalid_javascript_string": "Недопустимый код JavaScript: {error}", + "invalid_json_string": "Недопустимая JSON-строка: {error}" }, "date_format": "ГГГГ-ММ-ДД", "datetime_format": "ГГГГ-ММ-ДД ЧЧ:ММ", @@ -1451,7 +1483,8 @@ "date_invalid_form": "${path} должен быть в формате ГГГГ-ММ-ДД", "blank": "${path} не должен быть пустым", "required": "${path} обязательное поле", - "unique": "${path} должен быть уникальным" + "unique": "${path} должен быть уникальным", + "end_time_before_start_time": "Время окончания должно быть больше или равно времени начала" }, "unknown_date": "Неизвестная дата", "urls": "URLы", @@ -1471,5 +1504,43 @@ "age_on_date": "{age} на момент съемки", "sub_group_count": "Кол-во подгрупп", "sub_group": "Подгруппа", - "studio_tags": "Теги студии" + "studio_tags": "Теги студии", + "criterion_modifier_values": { + "any": "Любой", + "any_of": "Любой из", + "none": "Отсутствует", + "only": "Только" + }, + "containing_groups": "Группы, в которые входит объект", + "custom_fields": { + "criteria_format_string": "criterion} (пользовательское поле) {modifierString} {valueString}", + "criteria_format_string_others": "{criterion} (пользовательское поле) {modifierString} {valueString} (и ещё {others})", + "field": "Поле", + "title": "Настраиваемые поля", + "value": "Значение" + }, + "containing_group": "Содержащая группа", + "containing_group_count": "Количество содержащих групп", + "time_end": "Время окончания", + "eta": "Ожидаемое время завершения", + "login": { + "username": "Имя пользователя", + "password": "Пароль", + "login": "Логин", + "internal_error": "Произошла внутренняя ошибка. Подробности смотрите в логах.", + "invalid_credentials": "Неверное имя пользователя или пароль" + }, + "groups": "Группы", + "include_sub_tag_content": "Учитывать содержимое вложенных тегов", + "sort_name": "Сортировать по имени", + "include_sub_group_content": "Включать содержимое подгрупп", + "include_sub_studio_content": "Включить данные дочерних студий", + "group": "Группа", + "group_count": "Количество групп", + "group_scene_number": "Номер сцены", + "include_sub_groups": "Включать подгруппы", + "studio_count": "Количество студий", + "sub_group_of": "Входит в группу {parent}", + "sub_group_order": "Сортировка подгрупп", + "sub_groups": "Подгруппы" } diff --git a/ui/v2.5/src/locales/sv-SE.json b/ui/v2.5/src/locales/sv-SE.json index 2180a77f2..31bcf3b1d 100644 --- a/ui/v2.5/src/locales/sv-SE.json +++ b/ui/v2.5/src/locales/sv-SE.json @@ -141,7 +141,18 @@ "add_sub_groups": "Lägg Till Undergrupper", "remove_from_containing_group": "Ta bort från Grupp", "reset_resume_time": "Återställ återupptagningstid", - "set_cover": "Välj som Omslag" + "set_cover": "Välj som Omslag", + "play": "Spela", + "show_count_results": "Visa {count} resultat", + "sidebar": { + "toggle": "Ändra sidolisten", + "close": "Stäng sidolisten", + "open": "Öppna sidolisten" + }, + "show_results": "Visa resultat", + "load": "Ladda", + "load_filter": "Ladda filter", + "add_stash_id": "Lägg till Stash ID" }, "actions_name": "Handlingar", "age": "Ålder", @@ -190,11 +201,13 @@ "set_cover_label": "Välj scenens miniatyrbild", "set_tag_desc": "Tagga scenen antingen genom att skriva över eller slå samman med de redan existerande.", "set_tag_label": "Tagga", - "show_male_desc": "Välj huruvida manliga stjärnor kommer vara tillgängliga att tagga.", - "show_male_label": "Visa manliga stjärnor", "source": "Källa", "errors": { "blacklist_duplicate": "Duplicera svartlistat objekt" + }, + "performer_genders": { + "heading": "Stjärnors kön", + "description": "Stjärnor med dessa kön kommer visas när scener taggas." } }, "noun_query": "Query", @@ -295,7 +308,9 @@ "password_desc": "Lösenord till Stash. Lämna tom för att inaktivera användarautentisering", "stash-box_integration": "Integration med Stash-box", "username": "Användarnamn", - "username_desc": "Användarnamn till Stash. Lämna tom för att inaktivera användarautentisering" + "username_desc": "Användarnamn till Stash. Lämna tom för att inaktivera användarautentisering", + "log_file_max_size": "Maximal loggstorlek", + "log_file_max_size_desc": "Maximal storlek i megabytes för loggfilen innan den komprimeras. 0MB tolkas som avstängt. Kräver omstart." }, "backup_directory_path": { "description": "Filsökväg för SQLite-databas backupfil", @@ -411,6 +426,10 @@ "plugins_path": { "description": "Sökväg till pluginkonfigurationsfiler", "heading": "Pluginsökväg" + }, + "delete_trash_path": { + "description": "Sökväg dit raderade filer kommer flyttas till instället för att permanent raderas. Lämna blankt för att permanent radera filer.", + "heading": "Skräpsökväg" } }, "library": { @@ -447,7 +466,9 @@ "endpoint": "Adress", "graphql_endpoint": "GraphQL-adress", "name": "Namn", - "title": "Stash-box Adresser" + "title": "Stash-box Adresser", + "max_requests_per_minute": "Högsta antal förfrågningar per minut", + "max_requests_per_minute_description": "Använder standardvärdet {defaultValue} om detta är 0" }, "system": { "transcoding": "Omkodning" @@ -573,7 +594,9 @@ "whitespace_chars": "Blankstegstecken", "whitespace_chars_desc": "Dessa tecken kommer ersättas med blanksteg i titeln" }, - "scene_tools": "Scenverktyg" + "scene_tools": "Scenverktyg", + "graphql_playground": "GraphQL lekplats", + "heading": "Verktyg" }, "ui": { "abbreviate_counters": { @@ -798,6 +821,18 @@ "use_stash_hosted_funscript": { "description": "När aktiverat kommer funscripts att skickas direkt från Stash till din Handy-enhet utan att använda tredjeparts Handy-servern. Kräver att Stash kan nås från din Handy-enhet och att en API-nyckel är genererad om stash har lösenord aktiverat.", "heading": "Skicka funscripts direkt" + }, + "performer_list": { + "heading": "Lista av stjärnor", + "options": { + "show_links_on_grid_card": { + "heading": "Visa länkar på stjärnors kort" + } + } + }, + "sfw_mode": { + "description": "Aktivera om stash inte används för vuxet innehåll. Döljer eller ändrar gränssnitt som är mer fokuserade på vuxet innehåll.", + "heading": "Vuxet Innehållsläge" } }, "advanced_mode": "Avancerat Läge" @@ -896,7 +931,8 @@ "label": "Bläddringsläge", "pan_y": "Panorera Y", "zoom": "Zooma" - } + }, + "disable_animation": "Stäng av övergångsanimationen mellan bilder" }, "merge": { "destination": "Destination", @@ -907,7 +943,6 @@ "destination": "Mål", "source": "Källa" }, - "overwrite_filter_confirm": "Är du säker på att du vill skriva över existerande sökning {entityName}?", "performers_found": "{count} stjärnor hittade", "reassign_entity_title": "{count, plural, one {Omplacera {singularEntity}} other {Omplacera {pluralEntity}}}", "reassign_files": { @@ -960,7 +995,12 @@ "set_image_url_title": "URL till bild", "unsaved_changes": "Osparade ändringar. Är du säker att du vill lämna?", "clear_o_history_confirm": "Är du säker på att du vill radera O-historiken?", - "clear_play_history_confirm": "Är du säker på att du vill radera uppspelningshistoriken?" + "clear_play_history_confirm": "Är du säker på att du vill radera uppspelningshistoriken?", + "overwrite_filter_warning": "Sparat filter \"{entityName}\" kommer skrivas över.", + "set_default_filter_confirm": "Är du säker att du vill ställa in detta filter som standard?", + "clear_o_history_confirm_sfw": "Är du säker att du vill radera gillningshistoriken?", + "delete_alert_to_trash": "De följande {count, plural, one {{singularEntity}} other {{pluralEntity}}} kommer flyttas till skräpet:", + "stashid_exists_warning": "Det existerande stash id:et för denna stash-box kommer ersättas." }, "dimensions": "Mått", "director": "Regissör", @@ -970,7 +1010,8 @@ "list": "Lista", "tagger": "Taggaren", "unknown": "Okänd", - "wall": "Vägg" + "wall": "Vägg", + "label_current": "Visningsläge: {current}" }, "donate": "Donera", "dupe_check": { @@ -1083,8 +1124,8 @@ "syncing": "Synkar med server", "uploading": "Laddar upp skript" }, - "hasChapters": "Har Kapitel", - "hasMarkers": "Har Markörer", + "hasChapters": "Kapitel", + "hasMarkers": "Markörer", "height": "Längd", "height_cm": "Längd (cm)", "help": "Hjälp", @@ -1118,7 +1159,7 @@ "interactive_speed": "Interaktiv Hastighet", "performer_card": { "age": "{age} {years_old}", - "age_context": "{age} {years_old} i den här scenen" + "age_context": "{age} {years_old} vid produktion" }, "phash": "PHash", "play_count": "Visningar", @@ -1132,7 +1173,6 @@ "name": "Namn", "new": "Ny", "none": "Ingen", - "o_counter": "O-räknare", "operations": "Operationer", "organized": "Organiserad", "pagination": { @@ -1178,7 +1218,6 @@ "no_results_found": "Inga resultat hittades.", "number_of_performers_will_be_processed": "{performer_count} stjärnor kommer behandlas", "performer_already_tagged": "Stjärna som redan är taggad", - "performer_names_separated_by_comma": "Namn på stjärnor separerade med ett komma", "performer_selection": "Val av stjärna", "performer_successfully_tagged": "Lyckad taggning av stjärna:", "query_all_performers_in_the_database": "Alla stjärnor i databasen", @@ -1191,7 +1230,8 @@ "untagged_performers": "Ej taggade stjärnor", "update_performer": "Uppdatera stjärna", "update_performers": "Uppdatera stjärnor", - "updating_untagged_performers_description": "Uppdatering av ej taggade stjärnor kommer försöka att matcha alla stjärnor som saknar StashID och uppdatera metadatan." + "updating_untagged_performers_description": "Uppdatering av ej taggade stjärnor kommer försöka att matcha alla stjärnor som saknar StashID och uppdatera metadatan.", + "performer_names_or_stashids_separated_by_comma": "Stjärnors namn eller StashID separerade med komma" }, "performer_tags": "Stjärntagg", "performers": "Stjärnor", @@ -1222,7 +1262,9 @@ "edit_filter": "Ändra Filter", "name": "Filter", "saved_filters": "Sparade filter", - "update_filter": "Uppdatera filter" + "update_filter": "Uppdatera filter", + "more_filter_criteria": "+{count} fler", + "search_term": "Sökterm" }, "second": "Sekund", "seconds": "Sekunder", @@ -1269,7 +1311,7 @@ }, "paths": { "database_filename_empty_for_default": "databasfilnamn (blank för standard)", - "description": "Härnäst, måste vi avgöra var Stash hittar din porrsamling, och vart databasen, de genererade filerna, och cachen ska lagras . Om det skulle behövas kan dessa inställningar ändras senare.", + "description": "Härnäst, måste vi avgöra var Stash hittar din media, och var databasen, de genererade filerna, och cachen ska lagras . Om det skulle behövas kan dessa inställningar ändras senare.", "path_to_blobs_directory_empty_for_default": "sökväg för blobmapp (tom för standard)", "path_to_cache_directory_empty_for_default": "sökväg till cachemappen (tomt för standard)", "path_to_generated_directory_empty_for_default": "sökväg till mappen för genererade filer (blank för standard)", @@ -1282,12 +1324,15 @@ "where_can_stash_store_cache_files": "Var kan Stash lagra cachefiler?", "where_can_stash_store_cache_files_description": "För att viss funktionalitet som HLS/DASH-liveomkodning ska fungera kräver Stash en cache-mapp för tillfälliga filer. Som standard kommer Stash skapa en Cache mapp i mappen som innehåller konfigurationsfilen. Om du vill ändra detta skriv en absolut eller relativ (till den nuvarande platsen) sökväg. Stash kommer skapa mappen om den inte redan finns på platsen.", "where_can_stash_store_its_database": "Var kan Stash lagra sin databas?", - "where_can_stash_store_its_database_description": "Stash använder en SQLite-databas för att spara metadata till din porr. Som standard kommer databasen att skapas som stash-go.sqlite i mappen som innehåller din konfigurationsfil. Om du vill ändra detta, ange en absolut eller relativ (till den nuvarande arbetsmappen) filnamn.", + "where_can_stash_store_its_database_description": "Stash använder en SQLite-databas för att spara metadata till din media. Som standard kommer databasen att skapas som stash-go.sqlite i mappen som innehåller din konfigurationsfil. Om du vill ändra detta, ange en absolut eller relativ (till den nuvarande arbetsmappen) filnamn.", "where_can_stash_store_its_database_warning": "VARNING: att lagra databasen på ett annat system än det som Stash körs ifrån (t.ex. databasen på en NAS medan Stash körs från en annan dator) är inte stöttat! SQLite är inte avsett för att använding över nätverket, och att försöka oavsett kan väldigt enkelt korrumpera hela databasen.", "where_can_stash_store_its_generated_content": "Var kan Stash spara sitt genererade innehåll?", "where_can_stash_store_its_generated_content_description": "För att kunna erbjuda miniatyrbilder, förhandsvisningar och sprites måste Stash generera bilder och videor. Detta inkluderar också omkodning av ej stöttade filformat. Som standard skapar Stash en generated mapp i mappen som innehåller din konfigurationsfil. Om du vill ändra detta, ange en absolut eller relativ (till din nuvarande arbetsmapp) sökväg. Stash kommer skapa denna mapp om den inte redan finns.", - "where_is_your_porn_located": "Var är din porr lagrad?", - "where_is_your_porn_located_description": "Lägg till mappar som innehåller dina porrvideor och bilder. Stash kommer använda dessa mappar för att hitta videor och bilder under skanningen." + "where_is_your_porn_located": "Var är din media lagrad?", + "where_is_your_porn_located_description": "Lägg till mappar som innehåller dina videor och bilder. Stash kommer använda dessa mappar för att hitta videor och bilder under skanningen.", + "sfw_content_settings": "Används stash inte för vuxet innehåll?", + "sfw_content_settings_description": "stash kan användas för att hantera icke-vuxen media som fotografier, konst, serietidningar e.t.c.. Aktivering av detta kommer anpassa gränssnittet till att vara mer passande för icke-vuxen media.", + "use_sfw_content_mode": "Använd icke-vuxet läge" }, "stash_setup_wizard": "Stash Starthjälp", "success": { @@ -1374,7 +1419,6 @@ "status_tagging_job_queued": "Status: Taggjob köat", "status_tagging_studios": "Status: Taggar studior", "studio_already_tagged": "Studio redan taggad", - "studio_names_separated_by_comma": "Studionamn separerade med komma", "studio_selection": "Studioval", "studio_successfully_tagged": "Studiotaggning lyckades", "tag_status": "Taggstatus", @@ -1382,7 +1426,8 @@ "untagged_studios": "Otaggade studior", "update_studio": "Uppdatera Studio", "update_studios": "Uppdatera Studior", - "updating_untagged_studios_description": "Uppdatera otaggade studior kommer försöka matcha studior som saknar Stash ID och uppdatera metadata." + "updating_untagged_studios_description": "Uppdatera otaggade studior kommer försöka matcha studior som saknar Stash ID och uppdatera metadata.", + "studio_names_or_stashids_separated_by_comma": "Studionamn eller StashID separerade med komma" }, "studios": "Studior", "sub_tag_count": "Antal Underordnade Taggar", @@ -1422,7 +1467,7 @@ "url": "URL", "urls": "URL:er", "validation": { - "date_invalid_form": "${path} måste vara i formatet ÅÅÅÅ-MM-DD", + "date_invalid_form": "${path} måste vara i formatet ÅÅÅÅ, ÅÅÅÅ-MM, eller ÅÅÅÅ-MM-DD", "required": "${path} är ett obligatoriskt fält", "unique": "${path} måste vara unik", "blank": "${path} får inte lämnas tom", @@ -1516,8 +1561,29 @@ "custom_fields": { "field": "Fält", "title": "Skäddarsydda Fält", - "value": "Värde" + "value": "Värde", + "criteria_format_string": "{criterion} (eget fält) {modifierString} {valueString}", + "criteria_format_string_others": "{criterion} (eget fält) {modifierString} {valueString} (+{others} andra)" }, "eta": "Uppskattad återstående tid", - "sort_name": "Sorteringsnamn" + "sort_name": "Sorteringsnamn", + "login": { + "username": "Användarnamn", + "password": "Lösenord", + "internal_error": "Oväntad internt fel. Se loggen för mer information", + "login": "Inlogg", + "invalid_credentials": "Ogiltigt användarnamn eller lösenord" + }, + "age_on_date": "{age} vid produktion", + "scenes_duration": "Scen Speltid", + "last_o_at_sfw": "Senaste Gillning Vid", + "o_count_sfw": "Gillningar", + "o_history_sfw": "Gillningshistorik", + "odate_recorded_no_sfw": "Inget Gillningsdatum Sparat", + "stashbox_search": { + "header": "Sök {entityType} från StashBox", + "no_results": "Inga resultat hittades.", + "placeholder_name_or_id": "{entityType} namn eller StashID...", + "select_stashbox": "Välj StashBox..." + } } diff --git a/ui/v2.5/src/locales/th-TH.json b/ui/v2.5/src/locales/th-TH.json index 0376e880d..ee727e348 100644 --- a/ui/v2.5/src/locales/th-TH.json +++ b/ui/v2.5/src/locales/th-TH.json @@ -169,8 +169,6 @@ "set_cover_label": "ตั้ง Cover รูปภาพของฉาก", "set_tag_desc": "แนบแท็กกับฉาก โดยเขียนทับหรือรวมกับแท็กที่มีอยู่ในฉาก", "set_tag_label": "ตั้งแท็ก", - "show_male_desc": "สลับว่าจะให้นักแสดงชายพร้อมแท็กหรือไม่", - "show_male_label": "โชว์นักแสดงชาย", "source": "ต้นทาง", "mark_organized_desc": "ตั้งค่า scene เป็น organized ทันทีที่กดบันทึกข้อมูล", "mark_organized_label": "ตั้งค่าเป็น organized เมื่อบันทึกข้อมูล" @@ -799,7 +797,6 @@ "batch_update_studios": "อัพเดตสตูดิโอพร้อมกันหลายแห่ง", "no_results_found": "ไม่พบผลลัพธ์", "status_tagging_job_queued": "สถานะ: เพิ่มงานเพิ่มข้อมูลแล้ว", - "studio_names_separated_by_comma": "ชื่อสตูดิโอคั่นแต่ละแห่ง คั่นด้วยเครื่องหมายคอมมา (,)", "to_use_the_studio_tagger": "ต้องทำการตั้งค่า stash-box ก่อนถึงจะใช้งานเครื่องมือเพิ่มข้อมูลสตูดิโอได้", "status_tagging_studios": "สถานะ: กำลังเพิ่มข้อมูลสตูดิโอ", "studio_already_tagged": "มีข้อมูลสตูดิโอนี้แล้ว", @@ -940,7 +937,6 @@ "no_results_found": "ไม่พบผลลัพธ์", "update_performer": "เครื่องมืออัปเดตข้อมูลนักแสดง", "to_use_the_performer_tagger": "ต้องทำการตั้งค่า stash-box ก่อนถึงจะใช้งานเครื่องมือเพิ่มข้อมูลนักแสดงได้", - "performer_names_separated_by_comma": "ชื่อนักแสดงแต่ละคน คั่นด้วยเครื่องหมายคอมมา (,)", "refreshing_will_update_the_data": "การรีเฟรชจะอัปเดตนักแสดงที่มีข้อมูลแล้วด้วยข้อมูลจาก stash-box ที่เลือก", "name_already_exists": "พบนักแสดงที่ใช้ชื่อนี้แล้ว", "number_of_performers_will_be_processed": "จะอัปเดตนักแสดงจำนวน {performer_count} คน", @@ -1078,7 +1074,6 @@ "destination": "ปลายทาง", "source": "ต้นทาง" }, - "overwrite_filter_confirm": "คุณแน่ใจว่าต้องการเขียนทับเงื่อนไขการค้นหา{entityName}ใช่หรือไม่?", "reassign_files": { "destination": "ย้ายไปที่" }, @@ -1303,7 +1298,6 @@ "name": "ชื่อเรื่อง", "new": "เพิ่ม", "none": "ไม่มี", - "o_counter": "O-Counter", "o_history": "ประวัติ O", "organized": "จัดระเบียบแล้ว", "disambiguation": "แก้ความกำกวม", diff --git a/ui/v2.5/src/locales/tr-TR.json b/ui/v2.5/src/locales/tr-TR.json index 8bd6f874c..3db1d8e1b 100644 --- a/ui/v2.5/src/locales/tr-TR.json +++ b/ui/v2.5/src/locales/tr-TR.json @@ -137,11 +137,24 @@ "assign_stashid_to_parent_studio": "Mevcut ana stüdyoya Stash ID atayın ve üstverileri güncelleyin", "download_anonymised": "Anonim olarak indir", "add_manual_date": "Elle tarih ekle", - "reset_resume_time": "Devam etme süresini sıfırla" + "reset_resume_time": "Devam etme süresini sıfırla", + "split": "Ayır", + "swap": "Değiştir", + "encoding_image": "Resim kodlanıyor…", + "sidebar": { + "close": "Kenar çubuğunu kapat", + "open": "Kenar çubuğunu aç", + "toggle": "Kenar çubuğunu aç/kapat" + }, + "play": "Oynat", + "show_results": "Sonuçları göster", + "show_count_results": "{count} sonucu göster", + "load": "Yükle", + "load_filter": "Filtre yükle" }, "actions_name": "Eylemler", "age": "Yaş", - "aliases": "Takma isimler", + "aliases": "Diğer Adlar", "all": "tümü", "also_known_as": "Diğer adıyla", "ascending": "Artan", @@ -152,7 +165,7 @@ "career_length": "Kariyer Uzunluğu", "component_tagger": { "config": { - "active_instance": "Aktif stash-box:", + "active_instance": "Aktif stash-box oturumu:", "blacklist_desc": "Kara listeye alınan kelimeler sorguya eklenmez. Sözkonusu kelimeler kurallı ifadelerdir (regex) ve büyük-küçük harf ayrımına duyarlı değillerdir. Belirli karakterler ters bölü işaretiyle ayrılmalıdır: {chars_require_escape}", "blacklist_label": "Kara liste", "query_mode_auto": "Otomatik", @@ -170,8 +183,6 @@ "set_cover_label": "Sahne için kapak resmi seç", "set_tag_desc": "Sahneye etiket ekle (varolan etiketlerle birleştir veya üzerine yaz).", "set_tag_label": "Etiketleri düzenle", - "show_male_desc": "Erkek oyuncuları etiketleme işlemini aç/kapat.", - "show_male_label": "Erkek oyuncuları göster", "source": "Kaynak", "errors": { "blacklist_duplicate": "Yinelenen kara liste öğesi" @@ -228,7 +239,7 @@ "system": "Sistem", "tasks": "Görevler", "tools": "Araçlar", - "changelog": "Sürüm Notları" + "changelog": "Değişiklik Günlüğü" }, "dlna": { "allow_temp_ip": "{tempIP} IP adresine izin ver", @@ -287,13 +298,13 @@ "create_galleries_from_folders_desc": "Seçili ise resim içeren dizinlerden galeriler oluşturur.", "create_galleries_from_folders_label": "Resim içeren dizinlerden galeri oluştur", "db_path_head": "Veritabanı Yolu", - "directory_locations_to_your_content": "İçeriğiniz için dizin lokasyonları", + "directory_locations_to_your_content": "İçeriğiniz için dizin konumları", "excluded_image_gallery_patterns_desc": "Tarama ve Temizleme işlemine eklenmeyecek Resim ve Galeri dosyaları/dosya konumları için kurallı ifadeler (Regexp)", "excluded_image_gallery_patterns_head": "Dışta tutulan Resim/Galeri Kuralları", "excluded_video_patterns_desc": "Tarama ve Temizleme işlemine eklenmeyecek Video dosyaları/dosya konumları için kurallı ifadeler (Regexp)", "excluded_video_patterns_head": "Dışta tutulan Video Kuralları", "gallery_ext_desc": "Galeri ZIP dosyaları olarak tanımlanacak dosya uzantıları listesi (virgülle ayrılmış).", - "gallery_ext_head": "Galeri ZIP dosya Uzantıları", + "gallery_ext_head": "Galeri ZIP Uzantıları", "generated_file_naming_hash_desc": "Oluşturulacak dosya isimleri için MD5 veya oshash kullanın. Bu değeri değiştirmek, tüm sahneler için MD5/oshash hesaplaması gerektirir. Bu değeri değiştirdikten sonra mevcut tüm ek dosyalar yeniden oluşturulacak veya yer değiştirecektir. Yer değiştirme işlemleri için Görevler sayfasını ziyaret edin.", "generated_file_naming_hash_head": "Oluşturulan dosya adı imzası", "generated_files_location": "Oluşturulan ek dosyalar için dizin konumu (yer işaretleri, sahne önizlemeler, küçük resimler vb.)", @@ -323,7 +334,7 @@ "heading": "Veri Toplayıcı Yolu" }, "scraping": "Veri Toplama", - "sqlite_location": "SQLite veritabanı için dizin konumu (değiştirirseniz yeniden başlatma gerekir)", + "sqlite_location": "SQLite veritabanı için dosya konumu (yeniden başlatma gerektirir). UYARI: Veritabanını, Stash sunucusunun çalıştığı sistemden farklı bir sistemde (yani ağ üzerinden) depolamak desteklenmemektedir!", "video_ext_desc": "Video olarak işlem görecek dosya uzantı listesi (virgülle ayrılmış).", "video_ext_head": "Video Uzantıları", "video_head": "Video", @@ -386,12 +397,14 @@ "python_path": { "heading": "Python Yürütülebilir Yolu", "description": "Python yürütülebilir dosyasının yolu (yalnızca klasörün değil). Komut dosyası veri kazıyıcılar ve eklentiler için kullanılır. Boşsa, python ortamdan çözümlenecektir" - } + }, + "heatmap_generation": "Funscript Isı Haritası Oluşturma", + "funscript_heatmap_draw_range_desc": "Oluşturulan ısı haritasının y ekseninde hareket aralığını çizin. Değişiklik yapıldıktan sonra mevcut ısı haritalarının yeniden oluşturulması gerekecektir." }, "library": { "exclusions": "Dışta Tutulanlar", "gallery_and_image_options": "Galeri ve Resim seçenekleri", - "media_content_extensions": "Medya içerik uzantıları" + "media_content_extensions": "Medya İçeriği Uzantıları" }, "logs": { "log_level": "Kayıt Tutma Seviyesi" @@ -422,7 +435,9 @@ "endpoint": "Bağlantı Noktası", "graphql_endpoint": "GraphQL bağlantı noktası", "name": "Ad", - "title": "Stash-box Bağlantı Noktaları" + "title": "Stash-box Bağlantı Noktaları", + "max_requests_per_minute": "Dakika başına maksimum istek", + "max_requests_per_minute_description": "0 olarak ayarlanırsa, varsayılan değer olan {defaultValue} kullanılır" }, "system": { "transcoding": "Video Dönüştürme" @@ -515,7 +530,8 @@ "anonymise_and_download": "Veritabanının anonimleştirilmiş bir kopyasını oluşturur ve elde edilen dosyayı indirir.", "anonymise_database": "Tüm hassas verileri anonimleştirerek veritabanının bir kopyasını yedekler dizinine alır. Bu daha sonra sorun giderme ve hata ayıklama amacıyla başkalarına sağlanabilir. Orijinal veritabanı değiştirilmez. Anonimleştirilmiş veritabanı {filename_format} dosya adı biçimini kullanır.", "migrate_scene_screenshots": { - "delete_files": "Ekran görüntülerini sil" + "delete_files": "Ekran görüntülerini sil", + "overwrite_existing": "Mevcut blob'ları ekran görüntüsü verileriyle üzerine yaz" }, "rescan_tooltip": "Yoldaki her dosyayı yeniden tarayın. Dosya üstverilerini güncellemeyi zorlamak ve zip dosyalarını yeniden taramak için kullanılır.", "generate_clip_previews_during_scan": "Resim klipleri için önizlemeler oluştur" @@ -537,7 +553,9 @@ "whitespace_chars": "Boşluk karakterleri", "whitespace_chars_desc": "Bu karakterler başlıkta boşluk karakteri ile değiştirilecektir" }, - "scene_tools": "Sahne Araçları" + "scene_tools": "Sahne Araçları", + "graphql_playground": "GraphQL oyun alanı", + "heading": "Araçlar" }, "ui": { "basic_settings": "Temel Seçenekler", @@ -580,7 +598,7 @@ "half": "Yarım", "quarter": "Çeyrek", "tenth": "Onda bir", - "full": "Dolu" + "full": "Tam" }, "label": "Derecelendirme Yıldızı Hassasiyeti" } @@ -605,7 +623,8 @@ "heading": "Resim önizlemelerini kaydet" }, "create_image_clips_from_videos": { - "heading": "Video Uzantılarını Resim Klibi Olarak Tara" + "heading": "Video Uzantılarını Resim Klibi Olarak Tara", + "description": "Bir kütüphanede videolar devre dışı bırakıldığında, video dosyaları (video uzantısı ile biten dosyalar) Resim Klibi olarak taranacaktır." } } }, @@ -618,7 +637,7 @@ "heading": "Maksimum döngü süresi" }, "menu_items": { - "description": "Gezinti çubuğunda farklı türdeki içerikleri göster veya gizle", + "description": "Gezinti çubuğunda farklı içerik türlerini göster veya gizle", "heading": "Menü Öğeleri" }, "performers": { @@ -645,7 +664,7 @@ } }, "scene_player": { - "heading": "Sahne Oynatıcısı", + "heading": "Sahne Oynatıcı", "options": { "auto_start_video": "Videoları otomatik başlat", "auto_start_video_on_play_selected": { @@ -664,7 +683,9 @@ "track_activity": "Sahne Oynatma Geçmişi'ni etkinleştir", "enable_chromecast": "Chromecast'i Etkinleştir", "show_ab_loop_controls": "AB Loop eklenti kontrollerini göster", - "show_scrubber": "Video İlerleme Çubuğunu Göster" + "show_scrubber": "Video İlerleme Çubuğunu Göster", + "disable_mobile_media_auto_rotate": "Mobil cihazlarda tam ekran medyanın otomatik döndürülmesini devre dışı bırak", + "show_range_markers": "Zaman İşaretleyicilerini Göster" } }, "scene_wall": { @@ -688,6 +709,10 @@ "show_all_details": { "heading": "Tüm ayrıntıları göster", "description": "Etkinleştirildiğinde, varsayılan olarak tüm içerik ayrıntıları gösterilecek ve her ayrıntı öğesi tek bir sütuna sığacak" + }, + "compact_expanded_details": { + "heading": "Kompakt genişletilmiş ayrıntılar", + "description": "Bu seçenek etkinleştirildiğinde, kompakt görünüm korunurken genişletilmiş ayrıntılar gösterilir" } }, "custom_javascript": { @@ -696,12 +721,12 @@ "description": "Değişikliklerin etkili olması için sayfanın yeniden yüklenmesi gerekir. Özel Javascript ile Stash'in gelecekteki sürümleri arasında uyumluluk garantisi yoktur." }, "custom_locales": { - "heading": "Özel yerelleştirme", + "heading": "Özel Yerelleştirme", "option_label": "Özel yerelleştirme etkin", "description": "Bireysel yerel ayar dizelerini geçersiz kılın. Ana liste için https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/locales/en-GB.json adresine bakın. Değişikliklerin etkili olması için sayfanın yeniden yüklenmesi gerekir." }, "studio_panel": { - "heading": "Stüdyo görünümü", + "heading": "Stüdyo Görünümü", "options": { "show_child_studio_content": { "heading": "Alt stüdyo içeriğini görüntüle", @@ -716,7 +741,7 @@ "description": "Etiket görünümündeyken alt etiketlerdeki içeriği de görüntüleyin" } }, - "heading": "Etiket görünümü" + "heading": "Etiket Görünümü" }, "abbreviate_counters": { "heading": "Sayaçları kısalt", @@ -735,10 +760,20 @@ "connect": "Bağlan", "status": { "heading": "Handy Bağlantı Durumu" - } + }, + "server_offset": { + "heading": "Sunucu Zaman Farkı" + }, + "sync": "Senkronize et" }, "use_stash_hosted_funscript": { "heading": "Funscript'leri doğrudan sun" + }, + "show_tag_card_on_hover": { + "heading": "Etiket kartı araç ipuçları" + }, + "scroll_attempts_before_change": { + "heading": "Geçiş Öncesi Kaydırma Denemeleri" } }, "advanced_mode": "Gelişmiş Mod" @@ -829,7 +864,6 @@ "destination": "Hedef Noktası", "source": "Kaynak" }, - "overwrite_filter_confirm": "Kayıtlı {entityName} sorgusunun üzerine yazmak istediğinizden emin misiniz?", "scene_gen": { "force_transcodes": "Dönüştürülmüş video oluşturmayı zorla", "force_transcodes_tooltip": "Varsayılan olarak, video yalnızca video dosyası tarayıcı tarafından desteklenmediğinde dönüştürülür. Etkinleştirildiğinde, video dosyası tarayıcı tarafından destekleniyorsa bile video dönüştürülür.", @@ -839,7 +873,7 @@ "marker_image_previews": "İşaretleyici Hareketli Resim Önizlemeleri", "marker_image_previews_tooltip": "Hareketli Yer İmi WebP önizlemeleri, Önizleme Türü sadece Hareketli Resim olarak seçilmişse gereklidir.", "marker_screenshots": "İşaretleyici Ekran Görüntüleri", - "marker_screenshots_tooltip": "Yer İmi hareketsiz JPG resimleri, Önizleme Türü sadece Hareketsiz Resim olarak seçilmişse gereklidir.", + "marker_screenshots_tooltip": "Yer İmi hareketsiz JPG resimleri", "markers": "İşaretleyici Önizlemeleri", "markers_tooltip": "Belirlenen zamandan itibaren başlayan 20 saniyelik videolar.", "overwrite": "Varolan oluşturulmuş dosyaların üzerine yaz", @@ -877,7 +911,9 @@ "unsaved_changes": "Değişiklikler kaydedilmedi. Sayfadan ayrılmak istediğinize emin misiniz?", "clear_play_history_confirm": "Oynatma geçmişini temizlemek istediğinize emin misiniz?", "merge": { - "source": "Kaynak" + "source": "Kaynak", + "destination": "Hedef", + "empty_results": "Hedef alan değerleri değişmeyecektir." }, "dont_show_until_updated": "Sonraki güncellemeye kadar gösterme", "clear_o_history_confirm": "O geçmişini temizlemek istediğinize emin misiniz?", @@ -894,7 +930,8 @@ "reassign_files": { "destination": "Şuraya yeniden ata" }, - "reassign_entity_title": "{count, plural, one {Yeniden ata {singularEntity}} other {Yeniden ata {pluralEntity}}}" + "reassign_entity_title": "{count, plural, one {Yeniden ata {singularEntity}} other {Yeniden ata {pluralEntity}}}", + "set_default_filter_confirm": "Bu filtreyi varsayılan olarak ayarlamak istediğinize emin misiniz?" }, "dimensions": "Boyutlar", "director": "Yönetmen", @@ -903,7 +940,8 @@ "list": "Liste", "tagger": "Etiketleyici", "unknown": "Bilinmeyen", - "wall": "Duvar" + "wall": "Duvar", + "label_current": "Görüntüleme Modu: {current}" }, "donate": "Bağış Yap", "dupe_check": { @@ -918,7 +956,8 @@ "search_accuracy_label": "Arama Kesinliği", "title": "Yinelenen Sahneler", "duration_options": { - "equal": "Eşit" + "equal": "Eşit", + "any": "Herhangi" }, "duration_diff": "Maksimum Süre Farkı", "select_all_but_largest_file": "En büyük dosya hariç, yinelenen her gruptaki her dosyayı seç", @@ -981,11 +1020,12 @@ "include_sub_tags": "Alt etiketleri dahil et", "instagram": "Instagram", "interactive": "Etkileşimli", - "interactive_speed": "Etkileşim hızı", + "interactive_speed": "Etkileşimli Hız", "isMissing": "Eksik", "library": "Kütüphane", "loading": { - "generic": "Yükleniyor…" + "generic": "Yükleniyor…", + "plugins": "Eklentiler yükleniyor…" }, "marker_count": "İşaretleyici Sayısı", "markers": "İşaretleyiciler", @@ -995,7 +1035,7 @@ "checksum": "Sağlama Toplamı (checksum)", "downloaded_from": "İndirildiği Yer", "hash": "Dosya İmzası (Hash)", - "interactive_speed": "Etkileşim hızı", + "interactive_speed": "Etkileşimli Hız", "performer_card": { "age": "{age} {years_old}", "age_context": "Bu sahnede {age} {years_old}" @@ -1012,7 +1052,6 @@ "name": "Ad", "new": "Yeni", "none": "Hiçbiri", - "o_counter": "O-Sayacı", "operations": "İşlemler", "organized": "Düzenlendi", "pagination": { @@ -1049,7 +1088,8 @@ "name": "Filtre", "saved_filters": "Kaydedilmiş filtreler", "update_filter": "Filtreyi Güncelle", - "edit_filter": "Filtreyi Düzenle" + "edit_filter": "Filtreyi Düzenle", + "search_term": "Arama terimi" }, "seconds": "Saniye", "settings": "Ayarlar", @@ -1094,7 +1134,7 @@ }, "paths": { "database_filename_empty_for_default": "veritabanı adı (varsayılan için boş bırakın)", - "description": "Sırada porno koleksiyonunuzun hangi dizinde olduğunun, stash veritabanının ve oluşturulan ek dosyaların nereye kaydedileceğinin belirlenmesi var. Bu ayarları sonradan değiştirebilirsiniz.", + "description": "Sırada, porno koleksiyonunuzun nerede bulunacağını ve Stash veritabanının, oluşturulan dosyaların ve önbellek dosyalarının nerede depolanacağını belirlememiz gerekiyor. Bu ayarlar daha sonra gerekirse değiştirilebilir.", "path_to_generated_directory_empty_for_default": "oluşturulan ek dosyalar için dizin konumu (varsayılan için boş bırakın)", "set_up_your_paths": "Yollarınızı ayarlayın", "stash_alert": "Herhangi bir kütüphane konumu seçilmedi. Hiçbir medya Stash'e taranamayacak. Emin misiniz?", @@ -1109,7 +1149,8 @@ "path_to_cache_directory_empty_for_default": "önbellek dizini yolu (varsayılan için boş bırakın)", "store_blobs_in_database": "Blob'ları veritabanında depola", "path_to_blobs_directory_empty_for_default": "blobs dizini yolu (varsayılan için boş bırakın)", - "where_can_stash_store_cache_files_description": "Stash, HLS/DASH canlı video dönüştürme gibi bazı işlevlerin çalışabilmesi için geçici dosyalara yönelik bir önbellek dizini gerektirir. Varsayılan olarak, Stash yapılandırma dosyanızı içeren dizin içinde bir cache dizini oluşturacaktır. Bunu değiştirmek istiyorsanız, lütfen mutlak veya göreceli (geçerli çalışma dizinine) bir yol girin. Mevcut değilse, Stash bu dizini oluşturacaktır." + "where_can_stash_store_cache_files_description": "Stash, HLS/DASH canlı video dönüştürme gibi bazı işlevlerin çalışabilmesi için geçici dosyalara yönelik bir önbellek dizini gerektirir. Varsayılan olarak, Stash yapılandırma dosyanızı içeren dizin içinde bir cache dizini oluşturacaktır. Bunu değiştirmek istiyorsanız, lütfen mutlak veya göreceli (geçerli çalışma dizinine) bir yol girin. Mevcut değilse, Stash bu dizini oluşturacaktır.", + "where_can_stash_store_blobs_description_addendum": "Alternatif olarak bu verileri veritabanında saklayabilirsiniz. Not:Bu işlem veritabanınızın boyutunu artıracak ve veritabanı taşıma süresini uzatacaktır." }, "stash_setup_wizard": "Stash Kurulum Sihirbazı", "success": { @@ -1129,11 +1170,12 @@ "welcome": { "config_path_logic_explained": "Stash (config.yml) yapılandırma dosyasını ilk olarak mevcut dizinde bulmaya çalışır. Eğer bulamazsa, $HOME/.stash/config.yml dizinini (Windows işletim sistemi için %USERPROFILE%\\.stash\\config.yml dizini) araştırır. Öte yandan -c '' veya --config '' seçeneklerini kullanarak özelleştirilmiş bir yapılandırma dosyası da kullanabilirsiniz.", "in_current_stash_directory": "{path} yolunda:", - "in_the_current_working_directory": "Mevcut dizinde", + "in_the_current_working_directory": "", "next_step": "Eğer yeni bir sistem oluşturmak için hazırsanız, yapılandırma dosyasının nereye kaydedileceğini seçin ve Sonraki düğmesine basın.", "store_stash_config": "Stash yapılandırmasını nereye kaydetmek istiyorsunuz?", "unable_to_locate_config": "Eğer bunu okuyorsanız, Stash herhangi bir mevcut yapılandırma bulamamış demektir. Bu sihirbaz yeni bir yapılandırma sırasında size yol gösterecektir.", - "unexpected_explained": "Eğer beklenmedik bir şekilde bu ekranı gördüyseniz, Stash uygulamasını doğru dizinden başlatın veya başlatma komutuna -c değişkenini ekleyin." + "unexpected_explained": "Eğer beklenmedik bir şekilde bu ekranı gördüyseniz, Stash uygulamasını doğru dizinden başlatın veya başlatma komutuna -c değişkenini ekleyin.", + "in_the_current_working_directory_disabled": "{path} yolundaki çalışma dizini:" }, "welcome_specific_config": { "config_path": "Stash, yapılandırma dosyası için bu dizini kullanacak: {path}", @@ -1142,7 +1184,7 @@ }, "welcome_to_stash": "Stash uygulamasına hoşgeldiniz" }, - "stash_id": "Stash Kimliği (ID)", + "stash_id": "Stash Kimliği", "stash_ids": "Stash Kimliği", "stats": { "image_size": "Toplam resim boyutu", @@ -1182,7 +1224,8 @@ "started_importing": "İçe aktarma başladı", "updated_entity": "{entity} güncellendi", "merged_scenes": "Birleştirilmiş sahneler", - "reassign_past_tense": "Dosya yeniden atandı" + "reassign_past_tense": "Dosya yeniden atandı", + "removed_entity": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} kaldırıldı" }, "total": "Toplam", "true": "Doğru", @@ -1207,7 +1250,9 @@ "edit_excluded_fields": "Hariç Tutulan Alanları Düzenle", "no_fields_are_excluded": "Hiçbir alan hariç tutulmadı", "create_parent_label": "Ana stüdyo oluştur", - "these_fields_will_not_be_changed_when_updating_studios": "Stüdyolar güncellenirken bu alanlar değişmeyecektir." + "these_fields_will_not_be_changed_when_updating_studios": "Stüdyolar güncellenirken bu alanlar değişmeyecektir.", + "active_stash-box_instance": "Aktif stash-box oturumu:", + "no_instances_found": "Oturum bulunamadı" }, "batch_update_studios": "Stüdyoları Toplu Güncelle", "failed_to_save_studio": "Stüdyo kaydedilemedi \"{studio}\"", @@ -1227,7 +1272,11 @@ "tag_status": "Etiket Durumu", "untagged_studios": "Etiketlenmemiş stüdyolar", "updating_untagged_studios_description": "Etiketlenmemiş stüdyoları güncellemek, stashid'si olmayan tüm stüdyoları eşleştirmeye ve üstverileri güncellemeye çalışacaktır.", - "name_already_exists": "Ad zaten mevcut" + "name_already_exists": "Ad zaten mevcut", + "status_tagging_job_queued": "Durum: Etiketleme işi sıraya alındı", + "any_names_entered_will_be_queried": "Girilen tüm isimler karşıdaki Stash-Box oturumundan sorgulanacak ve bulunursa eklenecektir. Yalnızca tam eşleşmeler bir eşleşme olarak kabul edilecektir.", + "refreshing_will_update_the_data": "Yenileme işlemi stash-box oturumundaki tüm etiketli stüdyoların verilerini güncelleyecektir.", + "to_use_the_studio_tagger": "Stüdyo etiketleyiciyi kullanmak için bir stash-box oturumunun yapılandırılması gerekir." }, "blobs_storage_type": { "database": "Veritabanı", @@ -1261,7 +1310,8 @@ "update": "Güncelle", "version": "Sürüm", "confirm_delete_source": "Kaynağı silmek istediğinize emin misiniz {name} ({url})?", - "required_by": "{packages} için gerekli" + "required_by": "{packages} için gerekli", + "confirm_uninstall": "{number} paketi kaldırmak istediğinize emin misiniz?" }, "penis": "Penis", "penis_length": "Penis Uzunluğu", @@ -1272,7 +1322,9 @@ "excluded_fields": "Hariç tutulan alanlar:", "edit_excluded_fields": "Hariç Tutulan Alanları Düzenle", "no_fields_are_excluded": "Hiçbir alan hariç tutulmadı", - "these_fields_will_not_be_changed_when_updating_performers": "Oyuncular güncellenirken bu alanlar değiştirilmeyecektir." + "these_fields_will_not_be_changed_when_updating_performers": "Oyuncular güncellenirken bu alanlar değiştirilmeyecektir.", + "active_stash-box_instance": "Aktif stash-box oturumu:", + "no_instances_found": "Oturum bulunamadı" }, "current_page": "Geçerli sayfa", "network_error": "Ağ Hatası", @@ -1294,7 +1346,9 @@ "updating_untagged_performers_description": "Etiketlenmemiş oyuncuları güncellemek, stashid'si olmayan tüm oyuncuları eşleştirmeye ve üstverileri güncellemeye çalışacaktır.", "add_new_performers": "Yeni Oyuncular Ekle", "refresh_tagged_performers": "Etiketlenmiş oyuncuları yenile", - "performer_already_tagged": "Oyuncu zaten etikelenmiş" + "performer_already_tagged": "Oyuncu zaten etikelenmiş", + "any_names_entered_will_be_queried": "Girilen tüm isimler karşıdaki Stash-Box oturumundan sorgulanacak ve bulunursa eklenecektir. Yalnızca tam eşleşmeler bir eşleşme olarak kabul edilecektir.", + "refreshing_will_update_the_data": "Yenileme işlemi stash-box oturumundaki tüm etiketli oyuncuların verisini güncelleyecektir." }, "photographer": "Fotoğrafçı", "play_count": "Oynatma Sayısı", @@ -1330,7 +1384,13 @@ "loading_type": "{type} yüklenirken hata oluştu", "something_went_wrong": "Bir şeyler ters gitti.", "lazy_component_error_help": "Stash'i yakın zamanda güncellediyseniz, lütfen sayfayı yeniden yükleyin ya da tarayıcınızın önbelleğini temizleyin.", - "invalid_json_string": "Geçersiz JSON dizesi: {error}" + "invalid_json_string": "Geçersiz JSON dizesi: {error}", + "custom_fields": { + "duplicate_field": "Alan adı benzersiz olmalıdır", + "field_name_required": "Alan adı gereklidir", + "field_name_whitespace": "Alan adının başında veya sonunda boşluk bulunamaz", + "field_name_length": "Alan adı 65 karakterden az olmalıdır" + } }, "sub_group_order": "Alt Grup Sırası", "validation": { @@ -1358,7 +1418,8 @@ "disconnected": "Bağlantı kesildi", "error": "Handy'e bağlanırken hata oluştu", "ready": "Hazır", - "uploading": "Komut dosyası yükleniyor" + "uploading": "Komut dosyası yükleniyor", + "syncing": "Sunucu ile senkronize ediliyor" }, "o_count": "O Sayısı", "o_history": "O Geçmişi", @@ -1381,7 +1442,9 @@ "zip_file_count": "Zip Dosyası Sayısı", "last_played_at": "Son Oynatma Tarihi", "criterion_modifier_values": { - "only": "Sadece" + "only": "Sadece", + "none": "Hiçbiri", + "any_of": "Herhangi biri" }, "history": "Geçmiş", "existing_value": "mevcut değer", @@ -1418,6 +1481,20 @@ "sub_group_of": "{parent} öğesinin alt grubu", "time_end": "Bitiş Zamanı", "custom_fields": { - "value": "Değer" - } + "value": "Değer", + "field": "Alan", + "title": "Özel Alanlar", + "criteria_format_string": "{criterion} (özel alan) {modifierString} {valueString}" + }, + "eta": "Tahmini Kalan Süre", + "login": { + "password": "Şifre", + "internal_error": "Beklenmeyen dahili hata. Daha fazla ayrıntı için günlüklere bakın", + "login": "Giriş Yap", + "username": "Kullanıcı Adı", + "invalid_credentials": "Geçersiz kullanıcı adı veya şifre" + }, + "age_on_date": "Videoda {age} yaşında", + "time": "Başlangıç Zamanı", + "disambiguation": "Ad Ayrımı" } diff --git a/ui/v2.5/src/locales/uk-UA.json b/ui/v2.5/src/locales/uk-UA.json index bbf0e426f..003090197 100644 --- a/ui/v2.5/src/locales/uk-UA.json +++ b/ui/v2.5/src/locales/uk-UA.json @@ -141,7 +141,17 @@ "copy_to_clipboard": "Копіювати до буфера обміну", "set_back_image": "Зворотне зображення…", "set_front_image": "Переднє зображення…", - "unset": "Скинути" + "unset": "Скинути", + "load": "Завантажити", + "load_filter": "Завантажити фільтр", + "play": "Відтворити", + "show_results": "Висвітити вислід", + "show_count_results": "Висвітити {count} висліди", + "sidebar": { + "close": "Сховати бічну панель", + "open": "Розгорнути бічну панель", + "toggle": "Перемикнути бічну панель" + } }, "actions_name": "Дії", "age": "Вік", @@ -172,10 +182,8 @@ "query_mode_dir": "Каталог", "query_mode_label": "Режим запиту", "set_tag_desc": "Додайте мітки до сцени, або замінивши, або об'єднавши з існуючими мітками на сцені.", - "show_male_desc": "Перемикати, чи будуть доступні чоловічі виконавці для позначення.", "set_cover_desc": "Замінити обкладинку сцени, якщо вона знайдена.", "set_cover_label": "Встановити обкладинку сцени", - "show_male_label": "Показати чоловічих виконавців", "errors": { "blacklist_duplicate": "Дублікат елемента чорного списку" } @@ -520,7 +528,9 @@ "title": "Парсер Імені Файлу Сцени" }, "scene_tools": "Інструменти Сцени", - "scene_duplicate_checker": "Перевірка сцен на дублікати" + "scene_duplicate_checker": "Перевірка сцен на дублікати", + "graphql_playground": "GraphQL ігровий майданчик", + "heading": "Начиння" }, "ui": { "scene_player": { @@ -543,7 +553,8 @@ "enable_chromecast": "Увімкнути Chromecast", "show_scrubber": "Показати скруббер", "track_activity": "Увімкнути історію відтворення сцен", - "auto_start_video": "Автозапуск відео" + "auto_start_video": "Автозапуск відео", + "show_range_markers": "Виявити Позначки Охоплення" }, "heading": "Плеєр сцени" }, @@ -697,7 +708,8 @@ "description": "За замовчуванням використовуються відео-прев’ю (mp4). Для меншого навантаження на CPU під час перегляду можна використовувати анімовані зображення (webp) як прев’ю. Однак їх потрібно генерувати додатково до відео-прев’ю, і вони займають більше місця на диску.", "options": { "animated": "Анімоване зображення", - "static": "Статичне зображення" + "static": "Статичне зображення", + "video": "Відео" }, "heading": "Тип попереднього перегляду" }, @@ -743,7 +755,15 @@ "toggle_sound": "Увімкнути звук" } }, - "title": "Користувацький інтерфейс" + "title": "Користувацький інтерфейс", + "performer_list": { + "heading": "Перелік виконавців", + "options": { + "show_links_on_grid_card": { + "heading": "Відображати посилання на картки виконавців" + } + } + } }, "plugins": { "hooks": "Хуки", @@ -774,7 +794,9 @@ "endpoint": "Кінцева точка", "name": "Назва", "graphql_endpoint": "Кінцева точка GraphQL", - "api_key": "API ключ" + "api_key": "API ключ", + "max_requests_per_minute": "Макс запитів у хвилину", + "max_requests_per_minute_description": "Викорситовує значення за замовчуванням {defaultValue} якщо встановлено в 0" }, "system": { "transcoding": "Транскодування" @@ -962,7 +984,6 @@ "failed_to_save_studio": "Не вдалося зберегти студію \"{studio}\"", "number_of_studios_will_be_processed": "Будуть оброблені {studio_count} студії", "query_all_studios_in_the_database": "Усі студії в базі даних", - "studio_names_separated_by_comma": "Імена студій, розділені комою", "add_new_studios": "Додати нові студії", "name_already_exists": "Ім'я вже існує", "refresh_tagged_studios": "Оновити затеговані студії", @@ -992,7 +1013,6 @@ "generic": "Завантаження…", "plugins": "Завантаження плагінів…" }, - "o_counter": "O-Лічильник", "performer_tagger": { "status_tagging_job_queued": "Статус: Задача проставлення міток в черзі", "number_of_performers_will_be_processed": "Будуть оброблені {performer_count} виконавців", @@ -1006,7 +1026,6 @@ "no_fields_are_excluded": "Жодне поле не виключено" }, "failed_to_save_performer": "Не вдалося зберегти виконавця \"{performer}\"", - "performer_names_separated_by_comma": "Імена виконавців, розділені комою", "refreshing_will_update_the_data": "Оновлення оновить дані будь-яких виконавців, позначених мітками, з екземпляра stash-box.", "query_all_performers_in_the_database": "Усі виконавці в базі даних", "updating_untagged_performers_description": "Оновлення невідмічених виконавців спробує знайти відповідність для виконавців без stashid і оновити метадані.", @@ -1061,7 +1080,6 @@ "synopsis": "Синопсис", "dialogs": { "delete_gallery_files": "Видалити папку галереї/zip-файл та всі зображення, які не прив'язані до жодної іншої галереї.", - "overwrite_filter_confirm": "Ви впевнені, що хочете перезаписати існуючий збережений запит {entityName}?", "scene_gen": { "marker_image_previews_tooltip": "Також створюйте анімовані (webp) прев’ю, які необхідні лише тоді, коли тип прев’ю для стіни сцен/маркерів встановлено на Анімоване зображення. Під час перегляду вони споживають менше ресурсів CPU, ніж відео-прев’ю, але генеруються додатково до них і займають більше місця на диску.", "transcodes_tooltip": "MP4-транскоди будуть попередньо згенеровані для всього контенту; корисно для повільних процесорів, але вимагає набагато більше дискового простору", @@ -1275,7 +1293,12 @@ "not_equals": "не є", "greater_than": "більше ніж", "includes_all": "включає все", - "is_null": "є null" + "is_null": "є null", + "between": "поміж", + "excludes": "виключення", + "format_string_excludes": "{criterion} {modifierString} {valueString} (за виключенням {excludedString})", + "format_string_excludes_depth": "{criterion} {modifierString} {valueString} (за виключенням {excludedString}) (+{глибина, множина, =-1 {all} інші {{depth}}})", + "includes": "включно" }, "toast": { "rescanning_entity": "Повторне сканування {count, plural, one {{singularEntity}} other {{pluralEntity}}}…", @@ -1322,7 +1345,8 @@ "created_at": "Створено", "criterion": { "greater_than": "Більше ніж", - "less_than": "Менше ніж" + "less_than": "Менше ніж", + "value": "Значення" }, "containing_group": "Група, що містить", "cover_image": "Обкладинка зображення", @@ -1330,7 +1354,8 @@ "custom_fields": { "title": "Користувацькі поля", "field": "Поле", - "value": "Значення" + "value": "Значення", + "criteria_format_string": "{criterion} (своє поле) {modifierString} {valueString}" }, "death_date": "Дата смерті", "developmentVersion": "Розробницька версія", @@ -1417,5 +1442,8 @@ "plays": "{value} відтворень", "subsidiary_studios": "Дочірні студії", "subsidiary_studio_count": "Кількість дочірніх студій", - "age_on_date": "{age} років під час зйомок" + "age_on_date": "{age} років під час зйомок", + "configuration": "Обрис", + "country": "Країна", + "custom": "Свій" } diff --git a/ui/v2.5/src/locales/ur-PK.json b/ui/v2.5/src/locales/ur-PK.json new file mode 100644 index 000000000..9558feeaa --- /dev/null +++ b/ui/v2.5/src/locales/ur-PK.json @@ -0,0 +1,14 @@ +{ + "actions": { + "add": "شامل کریں", + "allow": "اجازت دیں", + "add_directory": "ڈکشنری میں شامل کریں", + "cancel": "منسوخ کریں", + "add_manual_date": "تاریخ شامل کریں", + "add_o": "مٹھ کی گنتئ بڑہاین", + "add_play": "پلۓ شامل کریں", + "allow_temporarily": "وقتئ اجازت دیں", + "anonymise": "بےنام کریں", + "apply": "لاگو کریں" + } +} diff --git a/ui/v2.5/src/locales/vi-VN.json b/ui/v2.5/src/locales/vi-VN.json index 72749ca64..6ba70eefc 100644 --- a/ui/v2.5/src/locales/vi-VN.json +++ b/ui/v2.5/src/locales/vi-VN.json @@ -134,14 +134,24 @@ "clean_confirm_message": "Bạn có chắc chắn muốn làm sạch không? Thao tác này sẽ xóa thông tin cơ sở dữ liệu và nội dung đã tạo cho tất cả các cảnh và bộ sưu tập không còn tồn tại trong hệ thống tệp.", "import_warning": "Bạn có chắc chắn muốn nhập không? Thao tác này sẽ xóa cơ sở dữ liệu và nhập lại từ siêu dữ liệu đã xuất của bạn." }, - "temp_disable": "Tạm thời vô hiệu hóa…", - "temp_enable": "Tạm thời kích hoạt…", + "temp_disable": "Vô hiệu hóa tạm thời…", + "temp_enable": "Kích hoạt tạm thời…", "unset": "Bỏ thiết lập", - "use_default": "Dùng mặc định", + "use_default": "Dùng thiết lập mặc định", "view_history": "Xem lịch sử", "view_random": "Xem ngẫu nhiên", "set_front_image": "Ảnh trước…", - "select_entity": "Chọn {entityType}" + "select_entity": "Chọn {entityType}", + "play": "Phát", + "show_results": "Hiển thị kết quả", + "show_count_results": "Hiển thị {count} kết quả", + "sidebar": { + "toggle": "Bật thanh bên", + "open": "Mở thanh bên", + "close": "Đóng thanh bên" + }, + "load": "Nạp", + "load_filter": "Nạp bộ lọc" }, "actions_name": "Hành động", "age": "Tuổi", @@ -158,25 +168,884 @@ "bitrate": "Bit Rate", "blobs_storage_type": { "database": "Cơ sở dữ liệu", - "filesystem": "Files hệ thống" + "filesystem": "Tập tin hệ thống" }, "captions": "Tiêu đề", "career_length": "Tuổi nghề", - "chapters": "Chapters", + "chapters": "Chương", "circumcised_types": { "CUT": "Cắt", "UNCUT": "Không cắt" }, - "circumcised": "Cắt bao quy đầu", + "circumcised": "Đã cắt bao quy đầu", "component_tagger": { "config": { "blacklist_desc": "Các mục trong danh sách đen sẽ bị loại trừ khỏi các truy vấn. Lưu ý rằng chúng là các biểu thức chính quy và không phân biệt chữ hoa chữ thường. Một số ký tự cần phải được thoát bằng dấu gạch chéo ngược: {chars_require_escape}", "blacklist_label": "Danh sách đen", - "mark_organized_desc": "Ngay lập tức đánh dấu cảnh là Đã tổ chức sau khi nhấn nút Lưu.", + "mark_organized_desc": "Ngay lập tức đánh dấu cảnh là Đã sắp xếp sau khi nhấn nút Lưu.", "active_instance": "Phiên bản stash-box đang hoạt động:", - "mark_organized_label": "Đánh dấu là Đã tổ chức khi lưu.", + "mark_organized_label": "Đánh dấu là Đã sắp xếp khi lưu.", "query_mode_auto": "Tự động", - "query_mode_auto_desc": "Sử dụng siêu dữ liệu nếu có, hoặc tên tệp." + "query_mode_auto_desc": "Sử dụng siêu dữ liệu nếu có, hoặc tên tệp", + "query_mode_filename": "Tên tệp tin", + "query_mode_filename_desc": "Chỉ dùng tên tệp tin", + "query_mode_label": "Chế độ truy vấn", + "query_mode_path": "Đường dẫn", + "query_mode_path_desc": "Dùng toàn bộ đường dẫn tệp tin", + "set_cover_desc": "Thay thế bìa nền nếu đã được tìm thấy.", + "set_cover_label": "Đặt ảnh bìa nền", + "set_tag_label": "Đặt các thẻ", + "query_mode_dir_desc": "Chỉ dùng thư mục chính của file video", + "set_tag_desc": "Đính kèm các thẻ vào cảnh, bằng cách ghi đè hoặc ghép với các thẻ có sẵn trên cảnh.", + "source": "Nguồn", + "errors": { + "blacklist_duplicate": "Các mục bị trùng lặp trong danh sách đen" + }, + "query_mode_dir": "Danh sách", + "query_mode_metadata_desc": "Chỉ dùng dữ liệu mô tả", + "query_mode_metadata": "Thông tin mô tả" + }, + "noun_query": "Truy vấn", + "results": { + "duration_unknown": "Thời gian không xác định", + "fp_matches": "Thời lượng tương ứng", + "fp_matches_multi": "Thời lượng khớp {matchCount}/{durationsLength} dấu vân tay", + "hash_matches": "Khớp với {hash_type}", + "match_failed_already_tagged": "Phân cảnh đã được gắn thẻ", + "match_failed_no_result": "Không có kết quả được tìm thấy", + "match_success": "Phân cảnh đã được gán thẻ thành công", + "phash_matches": "Khớp với {count} PHashes", + "duration_off": "Thời gian tắt ít nhất {number} giây", + "fp_found": "{fpCount, plural, =0 {Không có dấu vết trùng khớp mới được tìm thấy} other {# dấu vết trùng khớp mới đã được tìm thấy}}", + "unnamed": "Chưa được đặt tên" + }, + "verb_match_fp": "Các dấu vân tay trùng khớp", + "verb_matched": "Trùng khớp", + "verb_scrape_all": "Loại bỏ tất cả", + "verb_toggle_config": "{toggle} {configuration}", + "verb_toggle_unmatched": "{toggle} phân cảnh không trùng khớp", + "verb_submit_fp": "Gửi {fpCount, plural, one{# Fingerprint} other{# Fingerprint}}" + }, + "config": { + "about": { + "new_version_notice": "[MỚI]", + "build_hash": "Mã băm bản xây dựng:", + "check_for_new_version": "Kiểm tra phiên bản mới", + "latest_version": "Phiên bản mới nhất", + "latest_version_build_hash": "Mã bản dựng mới nhất:", + "release_date": "Ngày phát hành:", + "stash_discord": "Tham gia vào kênh {url} của chúng tôi", + "stash_open_collective": "Giúp đỡ chúng tôi qua {url}", + "version": "Phiên bản", + "build_time": "Thời điểm tạo:", + "stash_wiki": "Trang {url} của Stash", + "stash_home": "Trang chủ Stash tại {url}" + }, + "application_paths": { + "heading": "Đường dẫn tới ứng dụng" + }, + "categories": { + "about": "Về", + "changelog": "Nhật ký thay đổi", + "interface": "Giao diện", + "logs": "Tập nhật ký", + "plugins": "Các phần bổ trợ", + "security": "Bảo mật", + "services": "Các dịch vụ", + "system": "Hệ thống", + "tasks": "Các công việc", + "tools": "Các công cụ", + "metadata_providers": "Các bên cung cấp thông tin dữ liệu", + "scraping": "Đang quét dữ liệu" + }, + "dlna": { + "allow_temp_ip": "Cho phép {tempIP}", + "allowed_ip_addresses": "Các địa chỉ IP được cho phép", + "default_ip_whitelist": "Danh sách các IP mặc định được cho phép", + "disabled_dlna_temporarily": "Vô hiệu DLNA tạm thời", + "disallowed_ip": "Các IP bị cấm", + "enabled_by_default": "Mặc định được kích hoạt", + "enabled_dlna_temporarily": "Kích hoạt DLNA tạm thời", + "network_interfaces": "Các giao diện", + "recent_ip_addresses": "Các địa chỉ IP gần đây", + "server_display_name": "Tên hiển thị của máy chủ", + "server_display_name_desc": "Tên hiển thị cho máy chủ DLNA. Mặc định là {server_name} nếu để trống.", + "server_port": "Cổng máy chủ", + "server_port_desc": "Cổng cho máy chủ DLNA dùng. Yêu cầu khởi động lại DLNA sau khi sửa đổi.", + "successfully_cancelled_temporary_behaviour": "Hủy bỏ hành vi tạm thời thành công", + "until_restart": "cho tới lúc khởi động lại", + "video_sort_order": "Thứ tự sắp xếp video mặc định", + "video_sort_order_desc": "Thứ tự sắp xếp các video mặc định.", + "allowed_ip_temporarily": "Các địa chỉ IP được cho phép tạm thời", + "default_ip_whitelist_desc": "Các địa chỉ IP mặc định được phép sử dụng DLNA. Dùng {wildcard} để cho phép tất cả các địa chỉ IP.", + "network_interfaces_desc": "Các giao diện để hiển thị máy chủ DLNA. Danh sách trống nghĩa là chạy trên mọi máy chủ. Yêu cần khởi động lại DLNA sau khi sửa đổi." + }, + "general": { + "auth": { + "api_key": "Khóa API", + "authentication": "Xác thực", + "clear_api_key": "Xóa khóa API", + "credentials": { + "heading": "Các thông tin xác thực", + "description": "Tài khoản/mật khẩu dùng để kiểm soát quyền truy cập Stash." + }, + "generate_api_key": "Tạo khóa API", + "log_file": "File nhật ký", + "log_file_desc": "Đường dẫn tới file lưu nhật ký. Để trống để vô hiệu lưu nhật ký vào file. Yêu cầu khởi động lại.", + "log_http": "Nhật ký truy cập HTTP", + "log_to_terminal": "Xuất nhật ký lên terminal", + "maximum_session_age": "Thời gian phiên tối đa", + "maximum_session_age_desc": "Thời gian chờ tối đa trước khi phiên đăng nhập hết hạn, tính bằng giây. Yêu cầu khởi động lại.", + "password": "Mật khẩu", + "password_desc": "Mật khẩu để truy cập Stash. Để trống để tắt xác thực người dùng", + "stash-box_integration": "Tích hợp Stash-box", + "username": "Tên người dùng", + "username_desc": "Tên người dùng để truy cập Stash. Để trống để tắt xác thực người dùng", + "log_http_desc": "Xuất nhật ký truy cập HTTP lên terminal. Yêu cầu khởi động lại.", + "api_key_desc": "Khóa API cho các hệ thống ngoài. Chỉ yêu cầu khóa khi tên người dùng / mật khẩu được thiết lập. Tên người dùng phải được lưu trước khi tạo khóa API.", + "log_to_terminal_desc": "Đẩy nhật ký lên terminal bên cạnh việc lưu vào file. Luôn bật nếu đang vô hiệu lưu vào file. Yêu cầu khởi động lại." + }, + "backup_directory_path": { + "heading": "Đường dẫn tới thư mục sao lưu", + "description": "Vị trí thư mục để sao lưu file dữ liệu SQLite" + }, + "blobs_path": { + "heading": "Đường dẫn tới hệ thống tập tin dữ liệu binary", + "description": "Chỗ nào trong hệ thống file để lưu dữ liệu binary. Chỉ áp dụng cho lựa chọn lưu trữ blob trong hệ thống tệp tin. CẢNH BÁO: thay đổi cài đặt này yêu cầu di chuyển thủ công các dữ liệu hiện tại." + }, + "blobs_storage": { + "heading": "Loại lưu trữ dữ liệu binary", + "description": "Chỗ nào để lưu trữ dữ liệu binary như bìa phân cảnh, người biểu diễn, studio và các nhãn ảnh. Sau khi thay đổi giá trị này, dữ liệu hiện tại phải được chuyển đổi bằng cách sử dụng các tác vụ Chuyển Đổi Các Blob. Xem trang Các Tác Vụ để chuyển đổi." + }, + "cache_location": "Vị trí thư mục cache. Yêu cầu dùng nếu đang truyền trực tiếp sử dụng HLS (như trên thiết bị Apple) hoặc DASH.", + "cache_path_head": "Đường dẫn cache", + "calculate_md5_and_ohash_desc": "Tính toán hàm băm MD5 bên cạnh oshash. Bật lên sẽ làm quá trình quét ban đầu diễn ra chậm hơn. Hàm băm tên file phải đặt về oshash để vô hiệu MD5.", + "calculate_md5_and_ohash_label": "Tính toán MD5 cho các video", + "check_for_insecure_certificates": "Kiểm tra các chứng chỉ không an toàn", + "check_for_insecure_certificates_desc": "Một số trang sử dụng chứng chỉ ssl không an toàn. Nếu bỏ tick, bộ thu thập sẽ bỏ qua quy trình kiểm tra tính an toàn của chứng chỉ và sẽ thu thập toàn bộ các trang đó. Nếu bạn gặp vấn đề về chứng chỉ khi thu thập dữ liệu thì hãy bỏ tick.", + "create_galleries_from_folders_desc": "Nếu bật, các thư mục chứa ảnh sẽ mặc định được tạo thành thư viện. Tạo một tệp có tên .forcegallery hoặc .nogallery trong thư mục để ép buộc hoặc ngăn việc tạo thư viện.", + "create_galleries_from_folders_label": "Tạo thư viện ảnh từ các thư mục chứa hình ảnh", + "database": "Cơ sở dữ liệu", + "db_path_head": "Vị trí tệp cơ sở dữ liệu", + "directory_locations_to_your_content": "Vị trí thư mục chứa nội dung của bạn", + "excluded_image_gallery_patterns_head": "Các mẫu tên ảnh/thư viện cần loại trừ", + "excluded_video_patterns_desc": "Biểu thức chính quy (regex) của các tệp video hoặc đường dẫn cần loại trừ khỏi quá trình Quét và thêm vào mục Dọn dẹp", + "excluded_video_patterns_head": "Các mẫu tên video cần loại trừ", + "ffmpeg": { + "download_ffmpeg": { + "heading": "Tải xuống FFmpeg", + "description": "Tải FFmpeg vào thư mục cấu hình và xóa đường dẫn ffmpeg và ffprobe hiện tại để sử dụng từ thư mục cấu hình." + }, + "ffmpeg_path": { + "heading": "Đường dẫn tệp FFmpeg", + "description": "Đường dẫn đến tệp ffmpeg (không chỉ là thư mục). Nếu để trống, hệ thống sẽ tự tìm ffmpeg từ biến môi trường $PATH, thư mục cấu hình, hoặc từ $HOME/.stash" + }, + "ffprobe_path": { + "description": "Đường dẫn đến tệp ffprobe (không chỉ là thư mục). Nếu để trống, hệ thống sẽ tự tìm ffprobe từ biến môi trường $PATH, thư mục cấu hình hoặc từ $HOME/.stash", + "heading": "Đường dẫn tệp FFprobe" + }, + "hardware_acceleration": { + "desc": "Sử dụng phần cứng hiện có để mã hóa video trong quá trình chuyển mã trực tiếp.", + "heading": "Mã hóa phần cứng bằng FFmpeg" + }, + "live_transcode": { + "output_args": { + "heading": "Tham số đầu ra cho chuyển mã trực tiếp bằng FFmpeg", + "desc": "Nâng cao: Các tham số bổ sung sẽ được truyền vào FFmpeg trước trường đầu ra khi chuyển mã video trực tiếp." + }, + "input_args": { + "desc": "Nâng cao: Các tham số bổ sung sẽ được truyền vào FFmpeg trước trường đầu vào khi chuyển mã video trực tiếp.", + "heading": "Tham số đầu vào cho chuyển mã trực tiếp bằng FFmpeg" + } + }, + "transcode": { + "input_args": { + "desc": "Nâng cao: Các tham số bổ sung sẽ được truyền vào FFmpeg trước trường đầu vào khi tạo video.", + "heading": "Tham số đầu vào khi chuyển mã bằng FFmpeg" + }, + "output_args": { + "heading": "Tham số đầu ra khi chuyển mã bằng FFmpeg", + "desc": "Nâng cao: Các tham số bổ sung sẽ được truyền vào FFmpeg trước trường đầu ra khi tạo video." + } + } + }, + "funscript_heatmap_draw_range": "Bao gồm khoảng giá trị trong các bản đồ nhiệt được tạo ra", + "gallery_cover_regex_desc": "Biểu thức chính quy dùng để nhận diện ảnh bìa của thư viện", + "gallery_cover_regex_label": "Mẫu tên file ảnh dùng làm bìa thư viện", + "gallery_ext_desc": "Danh sách phần mở rộng tệp (file extensions), phân cách bằng dấu phẩy, sẽ được nhận diện là tệp thư viện dạng nén (zip).", + "gallery_ext_head": "Phần mở rộng tệp thư viện nén", + "generated_file_naming_hash_head": "Mã băm dùng để đặt tên cho các tệp được tạo ra", + "generated_files_location": "Vị trí thư mục lưu trữ các tệp được tạo (dấu cảnh, ảnh xem trước cảnh, ảnh sprite, v.v.)", + "generated_path_head": "Thư mục lưu trữ các tệp sinh ra tự động", + "hashing": "Băm (tạo mã băm)", + "heatmap_generation": "Tạo bản đồ nhiệt từ Funscript", + "image_ext_desc": "Danh sách phần mở rộng tệp được nhận diện là hình ảnh, ngăn cách bằng dấu phẩy.", + "image_ext_head": "Đuôi file ảnh", + "include_audio_desc": "Bao gồm luồng âm thanh khi tạo bản xem trước.", + "include_audio_head": "Bao gồm âm thanh", + "maximum_streaming_transcode_size_head": "Kích thước tối đa cho các luồng video đã chuyển mã", + "maximum_transcode_size_desc": "Kích thước tối đa cho các video chuyển mã được tạo ra", + "maximum_transcode_size_head": "Kích thước chuyển mã tối đa", + "metadata_path": { + "heading": "Vị trí thư mục chứa siêu dữ liệu", + "description": "Vị trí thư mục được sử dụng khi thực hiện xuất hoặc nhập toàn bộ dữ liệu" + }, + "number_of_parallel_task_for_scan_generation_head": "Số lượng tác vụ song song cho quá trình quét/tạo dữ liệu", + "parallel_scan_head": "Quét/Tạo dữ liệu song song", + "plugins_path": { + "description": "Vị trí thư mục chứa các tệp cấu hình plugin", + "heading": "Đường dẫn plugins" + }, + "preview_generation": "Tạo bản xem trước", + "python_path": { + "description": "Đường dẫn đến tệp Python (không chỉ là thư mục). Được sử dụng cho các trình quét (scraper) và plugin viết bằng script. Nếu để trống, hệ thống sẽ tự tìm Python từ môi trường", + "heading": "Đường dẫn tệp thực thi Python" + }, + "scrapers_path": { + "description": "Vị trí thư mục chứa các tệp cấu hình của trình quét (scraper)", + "heading": "Đường dẫn đến thư mục chứa các scraper" + }, + "scraping": "Thu thập dữ liệu từ website", + "video_ext_desc": "Danh sách phần mở rộng tệp sẽ được nhận diện là video, phân cách bằng dấu phẩy.", + "video_ext_head": "Định dạng của video", + "video_head": "Video", + "chrome_cdp_path": "Đường dẫn giao thức CDP của Chrome", + "chrome_cdp_path_desc": "Đường dẫn đến tệp thực thi của Chrome, hoặc một địa chỉ từ xa (bắt đầu bằng http:// hoặc https://, ví dụ http://localhost:9222/json/version) trỏ đến một phiên bản Chrome đang chạy.", + "excluded_image_gallery_patterns_desc": "Biểu thức chính quy (regex) của tệp ảnh và thư mục thư viện cần loại trừ khỏi quá trình Quét và thêm vào mục Dọn dẹp", + "funscript_heatmap_draw_range_desc": "Vẽ phạm vi chuyển động trên trục y của bản đồ nhiệt được tạo. Các bản đồ nhiệt hiện có sẽ cần được tạo lại sau khi thay đổi tùy chọn này.", + "generated_file_naming_hash_desc": "Sử dụng MD5 hoặc oshash để đặt tên cho các tệp được tạo. Việc thay đổi tùy chọn này yêu cầu tất cả các cảnh (scenes) phải có giá trị MD5/oshash tương ứng. Sau khi thay đổi, các tệp đã tạo trước đó sẽ cần được di chuyển hoặc tạo lại. Vui lòng xem trang Nhiệm vụ (Tasks) để thực hiện di chuyển.", + "maximum_streaming_transcode_size_desc": "Kích thước tối đa cho các luồng video đã chuyển mã", + "number_of_parallel_task_for_scan_generation_desc": "Đặt giá trị là 0 để hệ thống tự động phát hiện. Cảnh báo: chạy nhiều tác vụ hơn mức cần thiết để sử dụng 100% CPU sẽ làm giảm hiệu suất và có thể gây ra các sự cố khác.", + "scraper_user_agent": "User Agent cho trình quét (scraper)", + "scraper_user_agent_desc": "Chuỗi User-Agent được sử dụng trong các yêu cầu HTTP khi quét dữ liệu (scrape)", + "sqlite_location": "Vị trí tệp cơ sở dữ liệu SQLite (cần khởi động lại). CẢNH BÁO: Lưu cơ sở dữ liệu trên một hệ thống khác với nơi chạy Stash server (ví dụ: qua mạng) là không được hỗ trợ!", + "logging": "Ghi nhật ký" + }, + "advanced_mode": "Chế độ nâng cao", + "stashbox": { + "name": "Tên", + "title": "Các điểm cuối Stash-box", + "add_instance": "Thêm một phiên bản Stash-box", + "api_key": "API key", + "endpoint": "Điểm cuối (Endpoint)", + "graphql_endpoint": "Điểm cuối GraphQL", + "description": "Stash-box hỗ trợ gán tag tự động cho cảnh và diễn viên dựa trên dấu vân tay (fingerprints) và tên tệp.\nEndpoint và khóa API có thể được tìm thấy trong trang tài khoản của bạn trên phiên hoạt động của stash-box. Tên định danh là bắt buộc nếu bạn thêm nhiều hơn một phiên hoạt động.", + "max_requests_per_minute": "Số lượng yêu cầu tối đa mỗi phút", + "max_requests_per_minute_description": "Sử dụng giá trị mặc định là {defaultValue} nếu đặt là 0" + }, + "system": { + "transcoding": "Chuyển đổi định dạng video" + }, + "tasks": { + "added_job_to_queue": "Đã thêm {operation_name} vào hàng đợi công việc", + "anonymising_database": "Đang ẩn danh cơ sở dữ liệu", + "auto_tag": { + "auto_tagging_all_paths": "Tự động gắn tag cho tất cả các đường dẫn", + "auto_tagging_paths": "Đang tự động gắn tag cho các đường dẫn sau" + }, + "backing_up_database": "Sao lưu dữ liệu", + "backup_and_download": "Thực hiện sao lưu cơ sở dữ liệu và tải xuống tệp kết quả.", + "backup_database": "Thực hiện sao lưu cơ sở dữ liệu vào thư mục sao lưu (backups), với định dạng tên tệp là {filename_format}", + "cleanup_desc": "Kiểm tra các tệp bị thiếu và xóa chúng khỏi cơ sở dữ liệu. Đây là hành động có tính phá hủy.", + "clean_generated": { + "blob_files": "Tệp nhị phân (blob)", + "description": "Xóa các tệp đã được tạo nhưng không còn bản ghi tương ứng trong cơ sở dữ liệu.", + "image_thumbnails": "Ảnh thu nhỏ", + "markers": "Xem trước điểm đánh dấu", + "previews": "Xem trước của cảnh", + "previews_desc": "Ảnh và đoạn xem trước cảnh", + "sprites": "Bản ghép khung hình của cảnh", + "transcodes": "Bản video đã chuyển mã của cảnh", + "image_thumbnails_desc": "Ảnh thu nhỏ và đoạn xem trước" + }, + "data_management": "Quản lý dữ liệu", + "dont_include_file_extension_as_part_of_the_title": "Không bao gồm phần đuôi tệp trong tiêu đề", + "empty_queue": "Hiện không có tác vụ nào đang chạy.", + "generate": { + "generating_from_paths": "Đang tạo dữ liệu cho các cảnh từ những đường dẫn sau", + "generating_scenes": "Đang tạo dữ liệu cho {num} {scene}" + }, + "generate_clip_previews_during_scan": "Tạo bản xem trước cho các đoạn ảnh clip", + "generate_desc": "Tạo các tệp hỗ trợ bao gồm ảnh, sprite, video, vtt và các tệp khác.", + "generate_phashes_during_scan": "Tạo mã băm theo đặc điểm hình ảnh", + "generate_previews_during_scan": "Tạo ảnh xem trước động", + "anonymise_and_download": "Tạo một bản sao ẩn danh của cơ sở dữ liệu và tải xuống tệp kết quả.", + "generate_previews_during_scan_tooltip": "Cũng tạo các ảnh xem trước động (định dạng WebP), chỉ cần thiết khi kiểu xem trước Cảnh/Dấu mốc (Scene/Marker Wall Preview Type) được đặt thành Ảnh động. Khi duyệt nội dung, ảnh WebP tiêu tốn ít CPU hơn so với video preview, nhưng sẽ được tạo thêm bên cạnh video và có kích thước tệp lớn hơn.", + "anonymise_database": "Tạo một bản sao của cơ sở dữ liệu trong thư mục sao lưu (backups), đồng thời ẩn danh toàn bộ dữ liệu nhạy cảm. Bản sao này có thể được cung cấp cho người khác để hỗ trợ khắc phục sự cố và gỡ lỗi. Cơ sở dữ liệu gốc sẽ không bị thay đổi. Tệp cơ sở dữ liệu ẩn danh sẽ sử dụng định dạng tên file là {filename_format}.", + "generate_phashes_during_scan_tooltip": "Dùng để loại bỏ trùng lặp và nhận diện Scene.", + "defaults_set": "Giá trị mặc định đã được thiết lập và sẽ được sử dụng khi nhấn nút {action} trên trang Tác vụ (Tasks).", + "export_to_json": "Xuất nội dung cơ sở dữ liệu sang định dạng JSON trong thư mục metadata.", + "auto_tag_based_on_filenames": "Tự động gắn tag cho nội dung dựa trên đường dẫn tệp.", + "auto_tagging": "Gắn tag tự động", + "generate_sprites_during_scan": "Tạo ảnh xem trước cho thanh tua", + "generate_sprites_during_scan_tooltip": "Tập hợp hình ảnh được hiển thị bên dưới trình phát video để hỗ trợ điều hướng dễ dàng.", + "generate_thumbnails_during_scan": "Tạo ảnh thu nhỏ cho các hình ảnh", + "generate_video_covers_during_scan": "Tạo ảnh bìa cho từng cảnh quay", + "generate_video_previews_during_scan": "Tạo đoạn xem trước video", + "generate_video_previews_during_scan_tooltip": "Tạo video xem trước phát tự động khi rê chuột lên cảnh quay", + "generated_content": "Nội dung được tạo tự động", + "identify": { + "and_create_missing": "và tạo các mục còn thiếu", + "create_missing": "Tạo phần bị thiếu", + "default_options": "Tùy Chọn Mặc Định", + "description": "Tự động điền thông tin cảnh quay từ stash-box và các nguồn dữ liệu quét.", + "heading": "Nhận diện", + "identifying_from_paths": "Đang nhận diện các cảnh quay từ các đường dẫn sau", + "identifying_scenes": "Đang nhận diện {num} {scene}", + "include_male_performers": "Bao gồm diễn viên nam", + "set_cover_images": "Thiết lập ảnh bìa", + "set_organized": "Đánh dấu đã sắp xếp", + "skip_multiple_matches_tooltip": "Nếu tùy chọn này không được bật và có nhiều kết quả được trả về, một kết quả sẽ được chọn ngẫu nhiên để khớp", + "skip_single_name_performers_tooltip": "Nếu tùy chọn này không được bật, các diễn viên có tên chung chung như Samantha hoặc Olga vẫn sẽ được gán khớp", + "source": "Nguồn", + "source_options": "Tùy chọn cho {source}", + "sources": "Các nguồn", + "strategy": "Chiến lược", + "tag_skipped_matches": "Gắn thẻ cho các kết quả bị bỏ qua bằng", + "tag_skipped_performer_tooltip": "Tạo một thẻ như 'Nhận diện: Diễn viên một tên' để bạn có thể lọc trong chế độ xem Scene Tagger và tự chọn cách xử lý các diễn viên này", + "tag_skipped_performers": "Gắn thẻ cho các diễn viên bị bỏ qua bằng", + "field_options": "Tùy chọn trường dữ liệu", + "skip_single_name_performers": "Bỏ qua các diễn viên chỉ có một tên mà không có thông tin phân biệt rõ ràng", + "field_behaviour": "{strategy} {field}", + "field": "Trường dữ liệu", + "tag_skipped_matches_tooltip": "Tạo một thẻ như 'Nhận diện: Nhiều kết quả khớp' để bạn có thể lọc trong chế độ xem Scene Tagger và tự chọn kết quả đúng bằng tay", + "explicit_set_description": "Các thiết lập dưới đây sẽ áp dụng trừ khi có thiết lập riêng từ từng nguồn.", + "skip_multiple_matches": "Bỏ qua nếu tìm thấy nhiều kết quả trùng khớp" + }, + "incremental_import": "Chỉ nhập những dữ liệu còn thiếu từ tệp ZIP được cung cấp.", + "job_queue": "Hàng đợi tác vụ", + "maintenance": "Công cụ bảo trì hệ thống", + "migrate_blobs": { + "delete_old": "Xóa dữ liệu không còn sử dụng", + "description": "Di chuyển dữ liệu blob sang hệ thống lưu trữ blob hiện tại. Quá trình di chuyển này nên được thực hiện sau khi thay đổi hệ thống lưu trữ. Có thể chọn xóa dữ liệu cũ sau khi hoàn tất di chuyển." + }, + "migrate_scene_screenshots": { + "delete_files": "Xóa các tệp ảnh chụp màn hình", + "overwrite_existing": "Ghi đè các blob hiện có bằng dữ liệu ảnh chụp màn hình", + "description": "Di chuyển ảnh chụp cảnh quay sang hệ thống lưu trữ blob mới. Quá trình này nên được thực hiện sau khi nâng cấp hệ thống hiện tại lên phiên bản 0.20. Có thể tùy chọn xóa các ảnh chụp cũ sau khi di chuyển." + }, + "migrations": "Chuyển đổi dữ liệu", + "only_dry_run": "Chỉ thực hiện chạy thử. Không xóa bất kỳ dữ liệu nào", + "optimise_database": "Cố gắng cải thiện hiệu suất bằng cách phân tích và sau đó tái tạo lại toàn bộ tệp cơ sở dữ liệu.", + "plugin_tasks": "Các tác vụ của tiện ích mở rộng", + "rescan": "Quét lại tất cả các tệp", + "rescan_tooltip": "Quét lại mọi tệp trong đường dẫn. Dùng để buộc cập nhật metadata của tệp và quét lại các tệp ZIP.", + "scan": { + "scanning_all_paths": "Đang quét tất cả các đường dẫn", + "scanning_paths": "Đang quét các đường dẫn sau" + }, + "scan_for_content_desc": "Quét nội dung mới và thêm vào cơ sở dữ liệu.", + "set_name_date_details_from_metadata_if_present": "Thiết lập tên, ngày và thông tin chi tiết từ metadata được nhúng trong tệp", + "migrate_hash_files": "Được sử dụng sau khi thay đổi mã hash đặt tên tệp để đổi tên các tệp đã tạo sang định dạng hash mới.", + "import_from_exported_json": "Nhập dữ liệu từ tệp JSON đã xuất trong thư mục metadata. Hành động này sẽ xóa toàn bộ cơ sở dữ liệu hiện tại.", + "optimise_database_warning": "Cảnh báo: trong khi tác vụ này đang chạy, mọi thao tác có thay đổi cơ sở dữ liệu sẽ bị lỗi. Tùy vào kích thước cơ sở dữ liệu, quá trình này có thể mất vài phút để hoàn tất. Ngoài ra, bạn cần ít nhất dung lượng ổ đĩa trống tương đương với kích thước cơ sở dữ liệu, nhưng khuyến nghị là 1.5 lần để đảm bảo an toàn." + }, + "library": { + "exclusions": "Danh sách loại trừ", + "gallery_and_image_options": "Tùy chọn Thư viện và Hình ảnh", + "media_content_extensions": "Phần mở rộng của nội dung đa phương tiện" + }, + "logs": { + "log_level": "Cấp độ ghi nhật ký" + }, + "plugins": { + "available_plugins": "Plugin khả dụng", + "hooks": "Cơ chế kích hoạt tự động", + "installed_plugins": "Cài đặt plugin", + "triggers_on": "Kích hoạt khi" + }, + "scraping": { + "available_scrapers": "Scraper khả dụng", + "entity_scrapers": "Thông tin mô tả cho {entityType}", + "excluded_tag_patterns_head": "Mẫu tag bị loại trừ", + "installed_scrapers": "Các trình quét đã cài đặt", + "scraper": "Trình quét dữ liệu tự động", + "scrapers": "Các trình quét dữ liệu", + "search_by_name": "Tìm kiếm theo tên", + "supported_types": "Các loại được hỗ trợ", + "supported_urls": "URLs", + "entity_metadata": "Siêu dữ liệu {entityType}", + "excluded_tag_patterns_desc": "Biểu thức chính quy của tên tag cần loại trừ khỏi kết quả quét dữ liệu" + }, + "tools": { + "scene_duplicate_checker": "Kiểm tra cảnh quay bị trùng", + "scene_filename_parser": { + "add_field": "Thêm trường dữ liệu", + "capitalize_title": "Tự động viết hoa chữ cái đầu của mỗi từ trong tiêu đề", + "display_fields": "Hiển thị các trường dữ liệu", + "escape_chars": "Sử dụng \\ để thoát các ký tự đặc biệt", + "filename": "Tên tệp", + "filename_pattern": "Mẫu tên tệp", + "ignore_organized": "Bỏ qua các cảnh đã được sắp xếp", + "ignored_words": "Từ bị bỏ qua", + "matches_with": "Khớp với {i}", + "title": "Trình phân tích tên tệp cảnh quay", + "whitespace_chars": "Ký tự khoảng trắng", + "select_parser_recipe": "Chọn công thức phân tích", + "whitespace_chars_desc": "Các ký tự này sẽ được thay thế bằng khoảng trắng trong tiêu đề" + }, + "scene_tools": "Công cụ xử lý cảnh quay", + "graphql_playground": "Công cụ GraphGL", + "heading": "Công cụ" + }, + "ui": { + "abbreviate_counters": { + "description": "Rút gọn số đếm trong giao diện thẻ và trang chi tiết, ví dụ '1831' sẽ được định dạng thành '1.8K'.", + "heading": "Rút gọn số đếm" + }, + "basic_settings": "Cài đặt cơ bản", + "custom_css": { + "heading": "CSS tùy chỉnh", + "option_label": "Đã bật CSS tùy chỉnh", + "description": "Trang cần được tải lại để các thay đổi có hiệu lực. Không có gì đảm bảo rằng CSS tùy chỉnh sẽ tương thích với các phiên bản Stash trong tương lai." + }, + "custom_javascript": { + "description": "Hãy tải lại trang để áp dụng thay đổi. JavaScript tùy chỉnh có thể không tương thích với các bản cập nhật sau này.", + "option_label": "Đã bật JavaScript tùy chỉnh", + "heading": "JavaScript tùy chỉnh" + }, + "custom_locales": { + "heading": "Bản địa hóa tùy chỉnh", + "option_label": "Đã bật bản địa hóa tùy chỉnh", + "description": "Ghi đè các chuỗi ngôn ngữ riêng lẻ. Xem danh sách chính tại https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/locales/en-GB.json. Trang cần được tải lại để các thay đổi có hiệu lực." + }, + "delete_options": { + "description": "Thiết lập mặc định cho thao tác xóa ảnh, gallery và cảnh.", + "heading": "Tùy chọn xóa", + "options": { + "delete_file": "Xóa tệp theo mặc định", + "delete_generated_supporting_files": "Xóa các tệp hỗ trợ đã tạo theo mặc định" + } + }, + "desktop_integration": { + "desktop_integration": "Tích hợp Desktop", + "notifications_enabled": "Cho phép Thông báo", + "send_desktop_notifications_for_events": "Gửi thông báo trên màn hình máy tính cho các sự kiện", + "skip_opening_browser": "Bỏ qua việc mở trình duyệt", + "skip_opening_browser_on_startup": "Bỏ qua việc tự động mở trình duyệt khi khởi động" + }, + "detail": { + "compact_expanded_details": { + "description": "Khi được bật, tùy chọn này sẽ hiển thị chi tiết mở rộng trong khi vẫn duy trì giao diện gọn gàng", + "heading": "Thu gọn chi tiết mở rộng" + }, + "enable_background_image": { + "description": "Hiển thị ảnh nền trên trang chi tiết.", + "heading": "Bật ảnh nền" + }, + "heading": "Trang Chi tiết", + "show_all_details": { + "heading": "Hiển thị tất cả chi tiết", + "description": "Khi được bật, tất cả các chi tiết nội dung sẽ được hiển thị theo mặc định và mỗi mục chi tiết sẽ nằm gọn trong một cột duy nhất" + } + }, + "editing": { + "disable_dropdown_create": { + "description": "Xóa bỏ khả năng tạo đối tượng mới từ các bộ chọn thả xuống", + "heading": "Tắt chức năng tạo mới từ danh sách thả xuống" + }, + "heading": "Chỉnh sửa", + "rating_system": { + "star_precision": { + "label": "Độ chính xác của sao đánh giá", + "options": { + "full": "Đầy", + "half": "Một nữa", + "quarter": "1/4", + "tenth": "1/10" + } + }, + "type": { + "label": "Loại Hệ thống Đánh giá", + "options": { + "decimal": "Thập phân", + "stars": "Sao" + } + } + }, + "max_options_shown": { + "label": "Số lượng mục tối đa hiển thị trong danh sách thả xuống" + } + }, + "funscript_offset": { + "description": "Độ lệch thời gian tính bằng mili giây cho việc phát lại các kịch bản tương tác.", + "heading": "Độ lệch Funscript (ms)" + }, + "handy_connection": { + "connect": "Kết nối", + "status": { + "heading": "Trạng thái Kết nối Handy" + }, + "sync": "Đồng bộ hóa", + "server_offset": { + "heading": "Độ lệch Máy chủ" + } + }, + "handy_connection_key": { + "heading": "Khóa Kết nối Handy", + "description": "Khóa kết nối Handy để sử dụng cho các cảnh tương tác. Việc thiết lập khóa này sẽ cho phép Stash chia sẻ thông tin cảnh hiện tại của bạn với handyfeeling.com" + }, + "image_lightbox": { + "heading": "Hộp đèn ảnh" + }, + "image_wall": { + "direction": "Phương hướng", + "heading": "Tường ảnh", + "margin": "Khoảng lề (pixel)" + }, + "images": { + "heading": "Hình ảnh", + "options": { + "create_image_clips_from_videos": { + "heading": "Quét các Phần mở rộng Video dưới dạng Clip hình ảnh", + "description": "Khi một thư viện bị tắt Video, các Tệp video (các tệp có phần mở rộng là Video Extension) sẽ được quét dưới dạng Clip hình ảnh." + }, + "write_image_thumbnails": { + "description": "Ghi hình thu nhỏ của ảnh ra đĩa khi chúng được tạo tự động", + "heading": "Ghi hình thu nhỏ của ảnh" + } + } + }, + "interactive_options": "Tùy chọn Tương tác", + "menu_items": { + "description": "Hiển thị hoặc ẩn các loại nội dung khác nhau trên thanh điều hướng", + "heading": "Mục Menu" + }, + "minimum_play_percent": { + "description": "Phần trăm thời lượng cảnh phải được phát trước khi số lượt phát của nó được tăng lên.", + "heading": "Phần trăm Phát tối thiểu" + }, + "scene_player": { + "options": { + "enable_chromecast": "Bật Chromecast", + "show_ab_loop_controls": "Hiển thị điều khiển plugin Vòng lặp AB", + "show_scrubber": "Hiển thị Thanh điều khiển", + "vr_tag": { + "description": "Nút VR sẽ chỉ hiển thị cho các cảnh có gắn thẻ này.", + "heading": "Thẻ VR" + }, + "show_range_markers": "Hiện thị phạm vị của điểm đánh dấu", + "track_activity": "Bật lịch sử phát cảnh quay", + "auto_start_video_on_play_selected": { + "description": "Tự động phát video cảnh quay khi phát từ hàng chờ, hoặc phát các cảnh được chọn hoặc ngẫu nhiên từ trang Cảnh Quay", + "heading": "Tự động phát video khi phát các mục đã chọn" + }, + "continue_playlist_default": { + "description": "Phát cảnh tiếp theo trong hàng chờ khi video kết thúc", + "heading": "Tiếp tục danh sách phát theo mặc định" + }, + "disable_mobile_media_auto_rotate": "Tắt tự động xoay phương tiện toàn màn hình trên thiết bị Di động", + "always_start_from_beginning": "Luôn phát video từ đầu", + "auto_start_video": "Tự động phát video" + }, + "heading": "Trình phát cảnh quay" + }, + "scene_wall": { + "options": { + "toggle_sound": "Bật âm thanh", + "display_title": "Hiển thị tiêu đề và thẻ" + }, + "heading": "Tường Cảnh Quay / Điểm Đánh Dấu" + }, + "scroll_attempts_before_change": { + "heading": "Số lần thử cuộn trước khi chuyển đổi", + "description": "Số lần thử cuộn trước khi chuyển sang mục tiếp theo/trước đó. Chỉ áp dụng cho chế độ cuộn Pan Y." + }, + "show_tag_card_on_hover": { + "description": "Hiển thị thẻ thông tin thẻ khi di chuột qua các huy hiệu thẻ", + "heading": "Chú giải công cụ thẻ thông tin" + }, + "slideshow_delay": { + "heading": "Độ trễ Trình chiếu (giây)", + "description": "Trình chiếu có sẵn trong các thư viện ảnh khi ở chế độ xem dạng tường" + }, + "studio_panel": { + "heading": "Chế độ xem Studio", + "options": { + "show_child_studio_content": { + "description": "Trong chế độ xem Studio, hãy hiển thị cả nội dung từ các studio phụ", + "heading": "Hiển thị nội dung của các studio phụ" + } + } + }, + "tag_panel": { + "heading": "Chế độ xem Thẻ", + "options": { + "show_child_tagged_content": { + "heading": "Hiển thị nội dung của thẻ phụ", + "description": "Trong chế độ xem thẻ, hãy hiển thị cả nội dung từ các thẻ phụ" + } + } + }, + "title": "Giao diện Người dùng", + "use_stash_hosted_funscript": { + "heading": "Phục vụ/Truyền funscript trực tiếp", + "description": "Khi được bật, funscript sẽ được phục vụ trực tiếp từ Stash đến thiết bị Handy của bạn mà không sử dụng máy chủ Handy của bên thứ ba. Yêu cầu Stash phải truy cập được từ thiết bị Handy của bạn và cần tạo khóa API nếu Stash có cấu hình thông tin xác thực." + }, + "max_loop_duration": { + "heading": "Thời lượng lặp tối đa", + "description": "Thời lượng cảnh tối đa (tính bằng giây) mà trình phát cảnh sẽ lặp lại video - 0 để tắt" + }, + "performers": { + "options": { + "image_location": { + "description": "Đường dẫn tùy chỉnh cho ảnh mặc định của người biểu diễn. Để trống để sử dụng các mặc định có sẵn", + "heading": "Đường dẫn Ảnh Người biểu diễn Tùy chỉnh" + } + } + }, + "preview_type": { + "description": "Tùy chọn mặc định là xem trước video (mp4). Để giảm sử dụng CPU khi duyệt, bạn có thể dùng xem trước ảnh động (webp). Tuy nhiên, chúng phải được tạo ra ngoài các bản xem trước video và có kích thước tệp lớn hơn.", + "heading": "Kiểu Xem trước", + "options": { + "animated": "Ảnh động", + "static": "Ảnh tĩnh", + "video": "Video" + } + }, + "scene_list": { + "heading": "Chế độ xem dạng lưới", + "options": { + "show_studio_as_text": "Hiển thị lớp phủ studio dưới dạng văn bản" + } + }, + "language": { + "heading": "Ngôn ngữ" + } } - } + }, + "age_on_date": "{age} tại thời điểm sản xuất", + "description": "Mô Tả", + "custom_fields": { + "title": "Tùy Chỉnh Trường (dữ liệu)", + "criteria_format_string": "{Tiêu chí} (trường tùy chỉnh) {Điều kiện} {Giá trị}", + "value": "Giá trị", + "criteria_format_string_others": "{Tiêu chí} (trường tùy chỉnh) {Điều kiện} {Giá trị} (+{số lượng khác} mục khác)", + "field": "Trường (dữ liệu)" + }, + "dialogs": { + "delete_entity_title": "{count, plural, one {Xóa {singularEntity}} other {Xóa {pluralEntity}}}", + "delete_galleries_extra": "...cộng với bất kỳ tệp ảnh nào không được đính kèm vào bất kỳ thư viện ảnh nào khác.", + "delete_object_title": "Xóa {count, plural, one {{singularEntity}} other {{pluralEntity}}}", + "lightbox": { + "scale_up": { + "description": "Phóng lớn ảnh nhỏ để lấp đầy màn hình", + "label": "Phóng lớn để vừa" + }, + "delay": "Độ Trễ (Giây)", + "display_mode": { + "fit_horizontally": "Căn chỉnh vừa theo chiều ngang", + "fit_to_screen": "Vừa màn hình", + "label": "Chế độ hiển thị", + "original": "Bản gốc" + }, + "options": "Tùy Chọn", + "page_header": "Trang {page} / {total}", + "reset_zoom_on_nav": "Đặt lại mức thu phóng khi đổi ảnh", + "scroll_mode": { + "description": "Giữ phím Shift để tạm thời dùng chế độ khác.", + "pan_y": "Pan (chiều) Y", + "label": "Chế độ cuộn", + "zoom": "Phóng" + } + }, + "clear_o_history_confirm": "Bạn có chắc chắn muốn xóa lịch sử O không?", + "clear_play_history_confirm": "Bạn có chắc muốn xóa lịch sử phát không?", + "create_new_entity": "Tạo mới {entity}", + "delete_entity_simple_desc": "{count, plural, one {Bạn có chắc chắn muốn xóa {singularEntity} này không?} other {Bạn có chắc chắn muốn xóa {pluralEntity} này không?}}", + "delete_gallery_files": "Xóa thư mục/tệp zip thư viện ảnh và bất kỳ ảnh nào không được đính kèm vào thư viện ảnh khác.", + "delete_object_desc": "Bạn có chắc muốn xóa {count, plural, one {{singularEntity} này} other {{pluralEntity} này} } không?", + "dont_show_until_updated": "Không hiển thị cho đến bản cập nhật tiếp theo", + "export_include_related_objects": "Bao gồm các đối tượng liên quan khi xuất", + "export_title": "Xuất (dữ liệu)", + "imagewall": { + "direction": { + "column": "Cột", + "description": "Bố cục dựa trên cột hoặc hàng.", + "row": "Hàng" + }, + "margin_desc": "Số lượng pixel lề xung quanh mỗi ảnh." + }, + "edit_entity_title": "Chỉnh sửa {count, plural, one {{singularEntity}} other {{pluralEntity}}}", + "delete_alert": "Các {count, plural, one {{singularEntity}} other {{pluralEntity}}} sau đây sẽ bị xóa vĩnh viễn:", + "delete_confirm": "Bạn có chắc chắn muốn xóa {entityName} không?", + "delete_entity_desc": "{count, plural, one {Bạn có chắc chắn muốn xóa {singularEntity} này không? Trừ khi tệp cũng bị xóa, {singularEntity} này sẽ được thêm lại khi quá trình quét được thực hiện.} other {Bạn có chắc chắn muốn xóa {pluralEntity} này không? Trừ khi các tệp cũng bị xóa, {pluralEntity} này sẽ được thêm lại khi quá trình quét được thực hiện.}}", + "delete_object_overflow": "...và {count} {count, plural, one {{singularEntity}} other {{pluralEntity}}} khác.", + "overwrite_filter_warning": "Bộ lọc đã lưu \"{entityName}\" sẽ bị ghi đè.", + "scene_gen": { + "clip_previews": "Xem trước Hình ảnh từ Clip", + "marker_screenshots": "Ảnh màn hình Điểm đánh dấu", + "marker_screenshots_tooltip": "Điểm đánh dấu ảnh tĩnh JPG", + "overwrite": "Ghi đè các tệp tin có sẫn", + "phash": "Băm (hash) nhận thức", + "preview_generation_options": "Lựa chọn Tạo ra Bản xem trước", + "covers": "Bìa cảnh quay", + "force_transcodes": "Ép chuyển mã", + "image_previews": "Xem trước Hình ảnh Động", + "image_thumbnails": "Hình thu nhỏ của Ảnh", + "interactive_heatmap_speed": "Tạo bản đồ nhiệt và tốc độ cho các cảnh tương tác", + "marker_image_previews": "Xem trước Điểm đánh dấu Ảnh động", + "marker_image_previews_tooltip": "Ngoài ra sẽ tạo nên những hình xem trước động (webp), chỉ bắt buộc khi Loại Cảnh/Dấu Tường Xem Trước được thành Ảnh Động. Khi lướt nó sử dụng ít CPU hơn video xem trước, nhưng khi tạo nên thì đi kèm với chúng và là những file lớn hơn.", + "markers": "Xem trước Điểm đánh dấu", + "markers_tooltip": "Video 20 giây bắt đầu tại điểm thời gian đã cho sẫn.", + "override_preview_generation_options": "Ghi đè các lựa chọn tạo ra Bản xem trước", + "phash_tooltip": "Cho việc khử trùng lặp và phát hiện cảnh quay", + "preview_exclude_end_time_desc": "Bỏ qua x giây cuối cùng từ các bản xem trước cảnh. Đây có thể là một giá trị bằng giây, hoặc phần trăm (ví dụ 2%) của tổng thời gian cảnh quay.", + "preview_exclude_start_time_head": "Bỏ qua thời gian bắt đầu", + "preview_options": "Lựa chọn Bản xem trước", + "preview_preset_desc": "Phần cài đặt trước điều chỉnh kích thước, chất lượng và thời gian mã hóa của bản xem trước. Các cài đặt trước vượt quá việc \"chậm\" có hiệu năng giảm dần và không được khuyến khích.", + "preview_preset_head": "Xem trước phần cài đặt trước cho việc mã hóa", + "preview_exclude_end_time_head": "Bỏ qua thời gian kết thúc", + "force_transcodes_tooltip": "Mặc định, việc chuyển mã sẽ được tạo ra khi file video không được hỗ trợ trong trình duyệt. Khi kích hoạt, việc chuyển mã sẽ bắt đầu kể cả khi file video có vẻ được hỗ trợ bởi trình duyệt.", + "image_previews_tooltip": "Ngoài ra sẽ tạo nên những hình xem trước động (webp), chỉ bắt buộc khi Loại Cảnh/Dấu Tường Xem Trước được thành Ảnh Động. Khi lướt nó sử dụng ít CPU hơn video xem trước, nhưng khi tạo nên thì đi kèm với chúng và là những file lớn hơn.", + "preview_exclude_start_time_desc": "Bỏ qua x giây đầu tiên từ các bản xem trước cảnh. Đây có thể là một giá trị bằng giây, hoặc phần trăm (ví dụ 2%) của tổng thời gian cảnh quay.", + "override_preview_generation_options_desc": "Ghi đè các lựa chọn tạo ra Bản xem trước cho hành động này. Những thiết lập mặc định được đặt trong Hệ thống -> Tạo ra Bản xem trước.", + "preview_seg_count_desc": "Số lượng phân đoạn trong các file bản xem trước.", + "preview_seg_count_head": "Số lượng phân đoạn trong bản xem trước", + "preview_seg_duration_desc": "Độ dài của mỗi đoạn xem trước, tính theo giây.", + "preview_seg_duration_head": "Độ dài đoạn xem trước", + "sprites": "Sprites của Phần tìm Phân cảnh", + "sprites_tooltip": "Danh sách ảnh hiển thị ở dưới video để dễ thao tác", + "transcodes": "Giải mã", + "video_previews": "Xem trước", + "transcodes_tooltip": "Mã chuyển đổi MP4 sẽ được tạo trước cho tất cả nội dung; hữu ích cho CPU chậm nhưng cần nhiều dung lượng hơn", + "video_previews_tooltip": "Bản xem trước video phát khi di chuột qua một cảnh" + }, + "merge": { + "destination": "Đích đến", + "source": "Nguồn", + "empty_results": "Giá trị điền vào mục Đích đến sẽ không bị thay đổi." + }, + "merge_tags": { + "destination": "Đích đến", + "source": "Nguồn" + }, + "performers_found": "Đã tìm thấy {count} diễn viên", + "reassign_entity_title": "{count, plural, one {Chỉnh lại {singularEntity}} other {Chỉnh lại {pluralEntity}}}", + "reassign_files": { + "destination": "Chỉnh lại đến" + }, + "scrape_results_existing": "Hiện có", + "set_default_filter_confirm": "Bạn có chắc chắn muốn đặt bộ lọc này làm mặc định không?", + "set_image_url_title": "URL hình ảnh", + "unsaved_changes": "Thay đổi chưa được lưu. Bạn có chắc chắn muốn thoát không?", + "scenes_found": "tìm được {count} cảnh quay" + }, + "configuration": "Cấu Hình", + "connection_monitor": { + "websocket_connection_failed": "Không thể tạo kết nối websocket: xem bảng điều khiển trình duyệt để biết chi tiết", + "websocket_connection_reestablished": "Kết nối Websocket đã được thiết lập lại" + }, + "containing_group": "Nhóm chứa", + "containing_groups": "Các Nhóm chứa", + "countables": { + "files": "{count, plural, one {Tệp} other {Các tệp}}", + "groups": "{count, plural, one {Nhóm} other {Các nhóm}}", + "images": "{count, plural, one {Ảnh} other {Các ảnh}}", + "markers": "{count, plural, one {Dấu} other {Các dấu}}", + "scenes": "{count, plural, one {Cảnh} other {Các cảnh}}", + "studios": "{count, plural, one {Studio} other {Các studio}}", + "tags": "{count, plural, one {Thẻ} other {Các thẻ}}", + "performers": "{count, plural, one {Người biểu diễn} other {Các người biểu diễn}}", + "galleries": "{count, plural, one {Thư viện ảnh} other {Các thư viện ảnh}}" + }, + "country": "Quốc Gia", + "cover_image": "Ảnh Bìa", + "created_at": "Thời gian tạo", + "criterion": { + "greater_than": "Lớn hơn", + "less_than": "Nhỏ hơn", + "value": "Giá trị" + }, + "criterion_modifier": { + "between": "Giữa", + "equals": "Là", + "format_string": "{criterion} {modifierString} {valueString}", + "format_string_excludes": "{Tiêu chí} {Điều kiện} {Giá trị} (loại trừ {Ngoại lệ})", + "format_string_excludes_depth": "{Tiêu chí} {Điều kiện} {Giá trị} (loại trừ {Ngoại lệ}) (+{depth, plural, =-1 {tất cả} other {{depth}}})", + "greater_than": "lớn hơn", + "includes": "Bao gồm", + "includes_all": "Bao gồm tất cả", + "is_null": "rỗng", + "less_than": "nhỏ hơn", + "matches_regex": "Khớp biểu thức chính quy", + "not_between": "không nằm giữa", + "not_equals": "không phải", + "not_null": "không rỗng", + "format_string_depth": "{criterion} {modifierString} {valueString} (+{depth, plural, =-1 {all} other {{depth}}})", + "not_matches_regex": "Không khớp biểu thức chính quy", + "excludes": "Không bao gồm" + }, + "death_date": "Ngày Mất", + "death_year": "Năm Mất", + "descending": "Giảm dần", + "details": "Thông tin chi tiết", + "developmentVersion": "Phiên bản Phát triển", + "criterion_modifier_values": { + "any_of": "bất kỳ nào trong số", + "none": "không gì cả", + "only": "Duy nhất", + "any": "bất kỳ" + }, + "custom": "Tùy chỉnh", + "date": "Ngày", + "date_format": "YYYY-MM-DD", + "containing_group_count": "Số lượng Nhóm chứa", + "detail": "Chi tiết", + "datetime_format": "YYYY-MM-DD HH:MM", + "dimensions": "Kích thước", + "director": "Đạo diễn", + "display_mode": { + "grid": "Lưới", + "list": "Danh sách", + "unknown": "Chưa biết", + "label_current": "Chế độ Hiển Thị: {current}", + "wall": "Tường" + }, + "distance": "Khoảng cách", + "donate": "Ủng hộ", + "dupe_check": { + "duration_diff": "Chênh lệch thời lượng tối đa", + "only_select_matching_codecs": "Chỉ chọn nếu tất cả codec khớp với nhóm trùng lặp", + "options": { + "high": "Cao", + "low": "Thấp", + "medium": "Trung bình", + "exact": "Chính xác" + }, + "search_accuracy_label": "Độ chính xác tìm kiếm", + "select_all_but_largest_file": "Chọn mọi tệp trong mỗi nhóm trùng lặp, ngoại trừ tệp lớn nhất", + "select_all_but_largest_resolution": "Chọn mọi tệp trong mỗi nhóm được sao chép, ngoại trừ tệp có độ phân giải cao nhất", + "select_none": "Chọn Không", + "select_oldest": "Chọn tệp cũ nhất trong nhóm trùng lặp", + "select_options": "Chọn Tùy chọn…", + "select_youngest": "Chọn tệp mớinhất trong nhóm trùng lặp", + "title": "Cảnh quay trùng lặp", + "duration_options": { + "any": "Bất kỳ", + "equal": "Tương đương" + } + }, + "duplicated_phash": "Trùng lặp (pHash)", + "duration": "Thời lượng", + "effect_filters": { + "aspect": "Tỷ lệ", + "blur": "Mờ", + "brightness": "Độ sáng", + "contrast": "Tương phản", + "green": "Xanh lá", + "hue": "Sắc thái", + "name": "Bộ lọc", + "name_transforms": "Biến đổi", + "red": "Đỏ", + "reset_filters": "Đặt lại bộ lọc", + "reset_transforms": "Đặt lại chuyển đổi", + "rotate": "Xoay", + "rotate_left_and_scale": "Xoay trái và thay đổi tỷ lệ", + "rotate_right_and_scale": "Xoay phải và thay đổi tỷ lệ", + "saturation": "Độ bão hòa", + "scale": "Tỉ lệ", + "warmth": "Sự ấm áp" + }, + "empty_server": "Thêm một số cảnh quay vào máy chủ của bạn để xem các đề xuất trên trang này." } diff --git a/ui/v2.5/src/locales/zh-CN.json b/ui/v2.5/src/locales/zh-CN.json index 6ebb8d7b6..29e92b049 100644 --- a/ui/v2.5/src/locales/zh-CN.json +++ b/ui/v2.5/src/locales/zh-CN.json @@ -141,7 +141,19 @@ "reset_play_duration": "重置播放时长", "reset_resume_time": "重置恢复时间", "add_sub_groups": "添加子集合", - "remove_from_containing_group": "从集合中移除" + "remove_from_containing_group": "从集合中移除", + "sidebar": { + "close": "关闭侧边栏", + "open": "打开侧边栏", + "toggle": "切换侧边栏" + }, + "play": "播放", + "show_results": "显示结果", + "show_count_results": "显示{count}个结果", + "load": "加载", + "load_filter": "加载过滤器", + "add_stash_id": "添加Stash编号", + "create_new": "新建" }, "actions_name": "操作", "age": "年龄", @@ -187,13 +199,15 @@ "set_cover_label": "设置短片封面", "set_tag_desc": "通过覆盖或与短片中的现有标签合并,将标签附加到短片。", "set_tag_label": "设置标签", - "show_male_desc": "选择搜索时是否获取男演员信息.", - "show_male_label": "展示男演员标签", "source": "源", "mark_organized_label": "保存时标记为已整理", "mark_organized_desc": "点击保存按钮后,立即将短片标记为 \"已整理\"。", "errors": { "blacklist_duplicate": "重复黑名单项目" + }, + "performer_genders": { + "heading": "演员性别", + "description": "这些性别的演员在标记场景时会被展示。" } }, "noun_query": "查询", @@ -215,7 +229,10 @@ "verb_scrape_all": "挖掘所有", "verb_submit_fp": "提交 {fpCount, plural, one{# 指纹} other{# 指纹}}", "verb_toggle_config": "{toggle} {configuration}", - "verb_toggle_unmatched": "{toggle} 未匹配的短片" + "verb_toggle_unmatched": "{toggle} 未匹配的短片", + "verb_add_as_alias": "添加刮削到的名字作为别名", + "verb_link_existing": "链接到已存在的", + "verb_match_tag": "匹配标签" }, "config": { "about": { @@ -294,7 +311,9 @@ "password_desc": "登录 Stash 时所需的密码.留空表示关闭身份验证", "stash-box_integration": "整合 Stash-box", "username": "用户名", - "username_desc": "登录 Stash 时所需的用户名.留空表示关闭身份验证" + "username_desc": "登录 Stash 时所需的用户名.留空表示关闭身份验证", + "log_file_max_size": "最大日志尺寸", + "log_file_max_size_desc": "日志文件压缩前的最大大小(以兆字节为单位)。0MB 表示禁用此功能。需要重启。" }, "backup_directory_path": { "description": "备份SQLite 数据库文件的目录路径", @@ -410,6 +429,10 @@ "plugins_path": { "heading": "插件文件路径", "description": "插件配置文件目录" + }, + "delete_trash_path": { + "description": "删除的文件将被移动到的路径,而不是永久删除。留空将永久删除文件。", + "heading": "回收站路径" } }, "library": { @@ -446,7 +469,9 @@ "endpoint": "入口", "graphql_endpoint": "GraphQL 入口", "name": "名称", - "title": "Stash-box 入口" + "title": "Stash-box 入口", + "max_requests_per_minute": "每分钟最多可发起请求数量", + "max_requests_per_minute_description": "使用{defaultValue}的默认数值,当其数值设置为0时" }, "system": { "transcoding": "转码" @@ -572,7 +597,9 @@ "whitespace_chars": "空白字符", "whitespace_chars_desc": "这些字符在标题中会替换为空白字符" }, - "scene_tools": "短片工具" + "scene_tools": "短片工具", + "heading": "工具", + "graphql_playground": "GraphQL试验场" }, "ui": { "abbreviate_counters": { @@ -797,6 +824,18 @@ "use_stash_hosted_funscript": { "description": "启用后,funscript将直接从Stash提供到您的Handy设备,而无需使用第三方Handy服务器。要求可以从您的Handy设备访问Stash,并且如果stash配置了凭据,则会生成API密钥。", "heading": "直接提供funscripts服务" + }, + "performer_list": { + "heading": "演员列表", + "options": { + "show_links_on_grid_card": { + "heading": "在演员网格卡片上显示链接" + } + } + }, + "sfw_mode": { + "description": "如果使用 stash 存储SFW(工作场合安全)内容,请启用。它隐藏或更改了 UI 中与成人内容相关的某些方面。", + "heading": "SFW(工作场合安全)内容模式" } }, "advanced_mode": "高级模式" @@ -895,7 +934,8 @@ "label": "卷屏模式", "pan_y": "垂直卷动", "zoom": "放大" - } + }, + "disable_animation": "禁用图片之间的切换动画" }, "merge": { "destination": "目标", @@ -906,7 +946,6 @@ "destination": "目标", "source": "源" }, - "overwrite_filter_confirm": "确定要覆盖现有的已保存查询 {entityName} 吗?", "reassign_entity_title": "{count, plural, one {Reassign {singularEntity}} 其它 {Reassign {pluralEntity}}}", "reassign_files": { "destination": "重新指定至" @@ -959,7 +998,15 @@ "unsaved_changes": "更改未保存,确定离开吗?", "performers_found": "找到{count} 个演员", "clear_o_history_confirm": "真的确定要清空高潮记录吗?", - "clear_play_history_confirm": "真的确定要清空播放历史?" + "clear_play_history_confirm": "真的确定要清空播放历史?", + "set_default_filter_confirm": "你确定要设置这个过滤器为默认吗?", + "overwrite_filter_warning": "已保存的过滤器 \"{entityName}\" 将被覆盖。", + "clear_o_history_confirm_sfw": "你确定要清除点赞的历史?", + "delete_alert_to_trash": "如下的{count, plural, one {{singularEntity}} other {{pluralEntity}}} 将会被移动到回收站:", + "stashid_exists_warning": "此stash-box已存在的stash编号将会被取代。", + "studios_found": "找到{count} 个工作室", + "tags_found": "找到{count} 个标签", + "scrape_results_missing": "丢失" }, "dimensions": "大小", "director": "导演", @@ -969,7 +1016,8 @@ "list": "列表显示", "tagger": "标签工具", "unknown": "未知", - "wall": "预览墙" + "wall": "预览墙", + "label_current": "显示模式: {current}" }, "donate": "赞助", "dupe_check": { @@ -1082,8 +1130,8 @@ "syncing": "正在和服务器同步", "uploading": "上传脚本中" }, - "hasChapters": "已有章节", - "hasMarkers": "含有章节标记", + "hasChapters": "章节", + "hasMarkers": "章节标记", "height": "身高", "height_cm": "高(cm)", "help": "说明", @@ -1130,7 +1178,6 @@ "name": "名称", "new": "新增", "none": "空", - "o_counter": "高潮次数", "operations": "操作", "organized": "是否已经整理", "pagination": { @@ -1172,7 +1219,6 @@ "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": "查找所有在此数据库的演员", @@ -1185,7 +1231,8 @@ "untagged_performers": "未标签的演员", "update_performer": "更新演员资料", "update_performers": "更新演员们资料", - "updating_untagged_performers_description": "更新尚未标记的演员,会尝试搜寻没有stashid的演员并更新其元数据。" + "updating_untagged_performers_description": "更新尚未标记的演员,会尝试搜寻没有stashid的演员并更新其元数据。", + "performer_names_or_stashids_separated_by_comma": "用逗号分隔的演员名称或Stash编号" }, "performer_tags": "演员标签", "performers": "演员", @@ -1216,7 +1263,9 @@ "edit_filter": "编辑筛选器", "name": "过滤", "saved_filters": "保存过滤器", - "update_filter": "更新过滤器" + "update_filter": "更新过滤器", + "more_filter_criteria": "+{count}个更多", + "search_term": "搜索词" }, "second": "秒", "seconds": "秒", @@ -1263,7 +1312,7 @@ }, "paths": { "database_filename_empty_for_default": "数据库文件名 (留空则用默认名)", - "description": "接下来,我们需要决定哪里找到你的收藏,哪里存放 stash 数据库和产生资料文件。如果需要,这些设定可在以后再修改。", + "description": "接下来,我们需要决定哪里找到你的内容,哪里存放 stash 数据库和产生资料文件。如果需要,这些设定可在以后再修改。", "path_to_cache_directory_empty_for_default": "缓存目录的路径(默认为空)", "path_to_generated_directory_empty_for_default": "生成资料的文件夹路径 (留空则使用默认路径)", "set_up_your_paths": "设立你的路径", @@ -1274,14 +1323,17 @@ "where_can_stash_store_cache_files": "Stash可以在哪里存储缓存文件?", "where_can_stash_store_cache_files_description": "为了使HLS/DASH实时转码等功能正常运行,Stash需要一个临时文件的缓存目录。默认情况下,Stash将在包含您的配置文件的目录中创建一个cache目录。如果您想更改此设置,请输入绝对或相对(与当前工作目录)路径。如果Stash不存在,它将创建此目录。", "where_can_stash_store_its_database": "在哪里可以储存Stash的数据库?", - "where_can_stash_store_its_database_description": "Stash 使用 sqlite 数据库来存放你的收藏的元数据。默认情况下,会建立stash-go.sqlite在包含有你配置文件的目录里。如果你想改动,请输入一个绝对,或者相对(对于当前目录)的文件名。", + "where_can_stash_store_its_database_description": "Stash 使用 sqlite 数据库来存放你的内容的元数据。默认情况下,会建立stash-go.sqlite在包含有你配置文件的目录里。如果你想改动,请输入一个绝对,或者相对(对于当前目录)的文件名。", "where_can_stash_store_its_database_warning": "警告:不支持将数据库存储在运行 Stash 的不同系统上(例如,在另一台计算机上运行 Stash 服务器时将数据库存储到 NAS 上)!SQLite 不适合在网络上使用,尝试这样做很容易导致整个数据库损坏。", "where_can_stash_store_its_generated_content": "哪里可以存放Stash产生的资料?", "where_can_stash_store_its_generated_content_description": "为了可以提供缩图,预览和浏览图,Stash生成图片和视频。同时也包括将不支持的文件转码后的视频。默认情况下,Stash会建立一个generated文件夹在含有你配置文件的目录中。如果你要修改生成媒体的地方,请输入一个绝对,或者相对(对于当前工作目录)的路径。如果此目录不存在,Stash会自动建立它。", - "where_is_your_porn_located": "你的收藏在哪里?", - "where_is_your_porn_located_description": "添加含有你收藏的视频和图片的目录。Stash会在扫描时使用这些目录去寻找视频和图片。", + "where_is_your_porn_located": "你的内容存放在哪里?", + "where_is_your_porn_located_description": "添加含有你的视频和图片的目录。Stash会在扫描时使用这些目录去寻找视频和图片。", "path_to_blobs_directory_empty_for_default": "blobs目录的路径(默认为空)", - "store_blobs_in_database": "将 blobs存储到数据库" + "store_blobs_in_database": "将 blobs存储到数据库", + "sfw_content_settings": "为了SFW(工作场合安全)内容使用stash?", + "sfw_content_settings_description": "stash能被用来管理SFW(工作场合安全)内容,例如摄影、艺术、漫画等。启用此选项将调整部分界面行为,使其更适合SFW内容。", + "use_sfw_content_mode": "使用SFW(工作场合安全)模式" }, "stash_setup_wizard": "Stash 设定向导", "success": { @@ -1377,7 +1429,7 @@ "updated_at": "更新于", "url": "链接", "validation": { - "date_invalid_form": "${path} 的格式必须为 YYYY-MM-DD", + "date_invalid_form": "${path} 的格式必须为四位年份(YYYY)、四位年份-两位月份(YYYY-MM)或四位年份-两位月份-两位日期(YYYY-MM-DD)", "required": "${path} 是必填字段", "blank": "${path}不能为空", "unique": "${path}不能相同", @@ -1405,7 +1457,6 @@ }, "batch_add_studios": "批量添加工作室", "batch_update_studios": "批量更新工作室", - "studio_names_separated_by_comma": "工作室名称,用逗号分隔", "update_studio": "更新工作室", "current_page": "当前页", "no_results_found": "未查询到结果。", @@ -1425,7 +1476,8 @@ "refreshing_will_update_the_data": "刷新后将更新所有已在此stash-box实例被标记的工作室。", "create_or_tag_parent_studios": "创建缺失的上级工作室或标记已存在的上级工作室", "any_names_entered_will_be_queried": "任何输入的名字将会从远程的Stash-Box实例查询并且加入(如果找到)。只有完全符合的名字才会视为匹配。", - "studio_selection": "工作室选择" + "studio_selection": "工作室选择", + "studio_names_or_stashids_separated_by_comma": "逗号分隔的工作室名称或Stash编号" }, "subsidiary_studio_count": "子工作室数量", "urls": "链接", @@ -1522,5 +1574,23 @@ }, "eta": "预估剩余时间", "sort_name": "排序用名", - "age_on_date": "在制作时{age}岁" + "age_on_date": "在制作时{age}岁", + "login": { + "username": "用户名", + "password": "密码", + "invalid_credentials": "无效的用户名或密码", + "internal_error": "意外的内部错误。有关更多详细信息,请查看日志", + "login": "登录" + }, + "scenes_duration": "场景持续时间", + "last_o_at_sfw": "最近一次点赞在", + "o_count_sfw": "点赞", + "o_history_sfw": "点赞历史", + "odate_recorded_no_sfw": "没有已记录的点赞日期", + "stashbox_search": { + "header": "在StashBox 搜索{entityType}", + "no_results": "没有结果被找到。", + "placeholder_name_or_id": "{entityType} 名称或Stash编号……", + "select_stashbox": "选择StashBox……" + } } diff --git a/ui/v2.5/src/locales/zh-TW.json b/ui/v2.5/src/locales/zh-TW.json index 264973de3..317d66f61 100644 --- a/ui/v2.5/src/locales/zh-TW.json +++ b/ui/v2.5/src/locales/zh-TW.json @@ -141,7 +141,17 @@ "reset_resume_time": "重置恢復時間", "set_cover": "設為封面", "remove_from_containing_group": "從群組中刪除", - "add_sub_groups": "新增子分類" + "add_sub_groups": "新增子分類", + "sidebar": { + "close": "關閉側邊攔", + "open": "開啟側邊攔", + "toggle": "切換側邊欄" + }, + "show_results": "顯示結果", + "show_count_results": "顯示 {count} 筆結果", + "play": "播放", + "load": "載入", + "load_filter": "載入篩選結果" }, "actions_name": "動作", "age": "年齡", @@ -181,8 +191,6 @@ "set_cover_label": "設定短片封面", "set_tag_desc": "選擇套用標籤時,該如何處理現有標籤。", "set_tag_label": "標籤設定", - "show_male_desc": "選擇搜尋時,是否要取得男優資訊。", - "show_male_label": "顯示男優", "source": "來源", "mark_organized_desc": "點選儲存後立即將短片標為已整理。", "mark_organized_label": "儲存時標記為已整理", @@ -440,7 +448,9 @@ "endpoint": "端點", "graphql_endpoint": "GraphQL 端點", "name": "名稱", - "title": "Stash-box 端點" + "title": "Stash-box 端點", + "max_requests_per_minute": "每分鐘請求上限", + "max_requests_per_minute_description": "當設為 0 時,會套用預設值 {defaultValue}" }, "system": { "transcoding": "轉檔" @@ -566,7 +576,9 @@ "whitespace_chars": "空白字元", "whitespace_chars_desc": "這些字元將在標題中被空格取代" }, - "scene_tools": "短片工具" + "scene_tools": "短片工具", + "heading": "工具", + "graphql_playground": "GraphQL 測試環境" }, "ui": { "abbreviate_counters": { @@ -726,7 +738,8 @@ }, "enable_chromecast": "啟用 Chromecast", "show_ab_loop_controls": "顯示AB循環插件控件", - "disable_mobile_media_auto_rotate": "停用行動裝置上全螢幕媒體的自動旋轉功能" + "disable_mobile_media_auto_rotate": "停用行動裝置上全螢幕媒體的自動旋轉功能", + "show_range_markers": "顯示範圍標記" } }, "scene_wall": { @@ -790,6 +803,14 @@ "use_stash_hosted_funscript": { "description": "啟用後,funscript 將直接從 Stash 傳送至您的 Handy 裝置,而無需使用第三方 Handy 伺服器。要求可從您的 Handy 裝置存取 Stash,且如果 stash 已設定憑證,則會產生 API 金鑰。", "heading": "直接為 funscript 服務" + }, + "performer_list": { + "options": { + "show_links_on_grid_card": { + "heading": "在表演者卡片上顯示連結" + } + }, + "heading": "表演者清單" } }, "advanced_mode": "進階模式" @@ -889,7 +910,6 @@ "destination": "目的地", "source": "來源" }, - "overwrite_filter_confirm": "您確定要覆蓋現有的條件 {entityName} 嗎?", "reassign_entity_title": "{count, plural, one {重新指定{singularEntity}}}", "reassign_files": { "destination": "重新指定至" @@ -950,7 +970,9 @@ }, "clear_o_history_confirm": "您確定要清除尻尻紀錄嗎?", "clear_play_history_confirm": "您確定要清除播放紀錄嗎?", - "performers_found": "找到{count} 個演員" + "performers_found": "找到{count} 個演員", + "set_default_filter_confirm": "是否確定設定這一個過濾條件為預設?", + "overwrite_filter_warning": "篩選器 \"{entityName}\" 已存在,將被覆蓋。" }, "dimensions": "解析度", "director": "導演", @@ -960,7 +982,8 @@ "list": "條列顯示", "tagger": "標記工具", "unknown": "未知", - "wall": "預覽牆顯示" + "wall": "預覽牆顯示", + "label_current": "顯示模式:{current}" }, "donate": "贊助", "dupe_check": { @@ -1058,7 +1081,7 @@ "syncing": "與伺服器同步中", "uploading": "上傳腳本中" }, - "hasMarkers": "含有章節標記", + "hasMarkers": "章節標記", "height": "身高", "height_cm": "高度 (cm)", "help": "說明", @@ -1090,7 +1113,7 @@ "interactive_speed": "互動速度", "performer_card": { "age": "{age} {years_old}", - "age_context": "這齣戲裡面 {age} {years_old}" + "age_context": "這齣戲時 {age} {years_old}" }, "phash": "PHash", "play_count": "播放次數", @@ -1104,7 +1127,6 @@ "name": "名稱", "new": "新增", "none": "無", - "o_counter": "尻尻計數", "operations": "動作", "organized": "是否已整理", "pagination": { @@ -1146,7 +1168,6 @@ "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": "查詢所有資料庫中的演員", @@ -1190,7 +1211,9 @@ "name": "篩選", "saved_filters": "已儲存的過濾條件", "update_filter": "更新篩選", - "edit_filter": "編輯篩選器" + "edit_filter": "編輯篩選器", + "more_filter_criteria": "+{count} 更多", + "search_term": "搜尋詞組" }, "seconds": "秒", "settings": "設定", @@ -1236,16 +1259,16 @@ }, "paths": { "database_filename_empty_for_default": "資料庫檔案名稱(留空以使用預設)", - "description": "接下來,我們需要確定可以在哪裡找到你的片片,在哪裡儲存資料庫及其生成檔案等等。如果需要,您稍後可以再更改這些設定。", + "description": "接下來,我們需要確定可以在哪裡找到你的內容,在哪裡儲存資料庫及其生成檔案等等。如果需要,您稍後可以再更改這些設定。", "path_to_generated_directory_empty_for_default": "生成媒體資料夾路徑(留空以使用預設)", "set_up_your_paths": "設定你的路徑", "stash_alert": "您尚未選取任何路徑,Stash 將無法掃描你的檔案。你確定要繼續嗎?", "where_can_stash_store_its_database": "Stash 可以在哪裡儲存資料庫?", - "where_can_stash_store_its_database_description": "Stash 使用 SQLite 資料庫來儲存您片片的資料。預設情況下,Stash 將在您的設定檔路徑下以 stash-go.sqlite 這個檔案來儲存此資料庫內容。如果您想要更改此設定,請在此輸入您所想要的絕對或相對路徑(相對於目前工作目錄)。", + "where_can_stash_store_its_database_description": "Stash 使用 SQLite 資料庫來儲存您內容的資料。預設情況下,Stash 將在您的設定檔路徑下以 stash-go.sqlite 這個檔案來儲存此資料庫內容。如果您想要更改此設定,請在此輸入您所想要的絕對或相對路徑(相對於目前工作目錄)。", "where_can_stash_store_its_generated_content": "Stash 可以在哪裡儲存其生成內容?", "where_can_stash_store_its_generated_content_description": "為提供縮圖、預覽和其他預覽資料,Stash 將自動生成圖片和影片資訊。這包括不支援的檔案格式之轉檔。預設情況下,Stash 將在包含您設定檔案的資料夾中建立一個新的 generated 資料夾。如果要更改此生成媒體的儲存位置,請在此輸入絕對或相對路徑(相對於目前工作目錄)。如果該資料夾不存在,Stash 將自動建立此目錄。", - "where_is_your_porn_located": "你的片片都藏哪?", - "where_is_your_porn_located_description": "在此選擇你A片及圖片的資料夾,Stash 將在掃描影片及圖片時使用這些路徑。", + "where_is_your_porn_located": "你的內容都藏哪?", + "where_is_your_porn_located_description": "在此選擇你視訊及圖片的資料夾,Stash 將在掃描影片及圖片時使用這些路徑。", "path_to_blobs_directory_empty_for_default": "blobs 目錄的路徑 (預設為空)", "path_to_cache_directory_empty_for_default": "快取目錄的路徑 (預設為空)", "store_blobs_in_database": "將 blobs 儲存到資料庫", @@ -1388,7 +1411,7 @@ "group_count": "群組計數", "group_scene_number": "短片編號", "groups": "群組", - "hasChapters": "擁有章節", + "hasChapters": "章節", "history": "歷史紀錄", "image_index": "圖片#", "index_of_total": "第{index}個,共 {total}個", @@ -1453,7 +1476,6 @@ "update_studio": "更新工作室", "update_studios": "更新工作室", "updating_untagged_studios_description": "更新未標記的工作室將嘗試匹配任何缺乏stashid的工作室並更新metadata。", - "studio_names_separated_by_comma": "以逗號分隔的工作室名稱", "studio_selection": "工作室選擇", "studio_successfully_tagged": "工作室已成功設定標籤", "tag_status": "標籤狀態", @@ -1515,6 +1537,19 @@ "custom_fields": { "field": "欄位", "title": "自訂欄位", - "value": "值" - } + "value": "值", + "criteria_format_string_others": "{criterion} (custom field) 條件:{modifierString} {valueString} (+{others} others)", + "criteria_format_string": "{criterion} (custom field) 條件:{modifierString} {valueString}" + }, + "login": { + "login": "登入", + "username": "使用者名稱", + "password": "密碼", + "invalid_credentials": "無效的使用者名稱或是密碼", + "internal_error": "系統發生未預期的內部錯誤。詳情請參考日誌" + }, + "sort_name": "分類名稱", + "eta": "預估剩餘時間", + "age_on_date": "在{age}歲時製作", + "scenes_duration": "場景持續時間" } diff --git a/ui/v2.5/src/models/list-filter/criteria/criterion.ts b/ui/v2.5/src/models/list-filter/criteria/criterion.ts index a4d3a145c..8f30e5d17 100644 --- a/ui/v2.5/src/models/list-filter/criteria/criterion.ts +++ b/ui/v2.5/src/models/list-filter/criteria/criterion.ts @@ -78,7 +78,7 @@ export abstract class Criterion { protected cloneValues() {} - public abstract getLabel(intl: IntlShape): string; + public abstract getLabel(intl: IntlShape, sfwContentMode?: boolean): string; public getId(): string { return `${this.criterionOption.type}`; @@ -148,7 +148,7 @@ export abstract class ModifierCriterion< : ""; } - public getLabel(intl: IntlShape): string { + public getLabel(intl: IntlShape, sfwContentMode: boolean = false): string { const modifierString = ModifierCriterion.getModifierLabel( intl, this.modifier @@ -162,10 +162,14 @@ export abstract class ModifierCriterion< valueString = this.getLabelValue(intl); } + const messageID = !sfwContentMode + ? this.criterionOption.messageID + : this.criterionOption.sfwMessageID ?? this.criterionOption.messageID; + return intl.formatMessage( { id: "criterion_modifier.format_string" }, { - criterion: intl.formatMessage({ id: this.criterionOption.messageID }), + criterion: intl.formatMessage({ id: messageID }), modifierString, valueString, } @@ -257,12 +261,14 @@ interface ICriterionOptionParams { type: CriterionType; makeCriterion: MakeCriterionFn; hidden?: boolean; + sfwMessageID?: string; } export class CriterionOption { public readonly type: CriterionType; public readonly messageID: string; public readonly makeCriterionFn: MakeCriterionFn; + public readonly sfwMessageID?: string; // used for legacy criteria that are not shown in the UI public readonly hidden: boolean = false; @@ -272,6 +278,7 @@ export class CriterionOption { this.messageID = options.messageID; this.makeCriterionFn = options.makeCriterion; this.hidden = options.hidden ?? false; + this.sfwMessageID = options.sfwMessageID; } public makeCriterion(config?: ConfigDataFragment) { @@ -478,7 +485,7 @@ export class IHierarchicalLabeledIdCriterion extends ModifierCriterion ModifierCriterion + makeCriterion?: () => ModifierCriterion, + options?: { sfwMessageID?: string } ) { super({ messageID, @@ -773,15 +790,22 @@ export class MandatoryNumberCriterionOption extends ModifierCriterionOption { makeCriterion: makeCriterion ? makeCriterion : () => new NumberCriterion(this), + ...options, }); } } export function createMandatoryNumberCriterionOption( value: CriterionType, - messageID?: string + messageID?: string, + options?: { sfwMessageID?: string } ) { - return new MandatoryNumberCriterionOption(messageID ?? value, value); + return new MandatoryNumberCriterionOption( + messageID ?? value, + value, + undefined, + options + ); } export function encodeRangeValue( diff --git a/ui/v2.5/src/models/list-filter/criteria/is-missing.ts b/ui/v2.5/src/models/list-filter/criteria/is-missing.ts index f7387e558..58e3535a6 100644 --- a/ui/v2.5/src/models/list-filter/criteria/is-missing.ts +++ b/ui/v2.5/src/models/list-filter/criteria/is-missing.ts @@ -32,7 +32,7 @@ export const SceneIsMissingCriterionOption = new IsMissingCriterionOption( "date", "galleries", "studio", - "movie", + "group", "performers", "tags", "stash_id", diff --git a/ui/v2.5/src/models/list-filter/filter-options.ts b/ui/v2.5/src/models/list-filter/filter-options.ts index 32b86e786..a63394f35 100644 --- a/ui/v2.5/src/models/list-filter/filter-options.ts +++ b/ui/v2.5/src/models/list-filter/filter-options.ts @@ -4,6 +4,7 @@ import { DisplayMode } from "./types"; export interface ISortByOption { messageID: string; value: string; + sfwMessageID?: string; } export const MediaSortByOptions = [ @@ -22,7 +23,7 @@ export class ListFilterOptions { public readonly displayModeOptions: DisplayMode[] = []; public readonly criterionOptions: CriterionOption[] = []; - public static createSortBy(value: string) { + public static createSortBy(value: string): ISortByOption { return { messageID: value, value, diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts index ac9d9de1e..d28107a12 100644 --- a/ui/v2.5/src/models/list-filter/filter.ts +++ b/ui/v2.5/src/models/list-filter/filter.ts @@ -103,6 +103,20 @@ export class ListFilterModel { }); } + // returns a clone of the filter for metadata fetching + // this removes the sort, page size and page number and zoom index + public metadataInfo() { + const clone = this.clone(); + clone.sortBy = undefined; + clone.randomSeed = -1; + clone.currentPage = 1; + clone.sortDirection = DEFAULT_PARAMS.sortDirection; + clone.itemsPerPage = 0; + clone.zoomIndex = 1; + clone.displayMode = DEFAULT_PARAMS.displayMode; + return clone; + } + // returns the number of filters applied public count() { // don't include search term @@ -183,7 +197,7 @@ export class ListFilterModel { ret.disp = Number.parseInt(params.disp, 10); } if (params.q) { - ret.q = params.q.trim(); + ret.q = params.q; } if (params.p) { ret.p = Number.parseInt(params.p, 10); @@ -476,13 +490,23 @@ export class ListFilterModel { return this.setCriteria(criteria); } - public clearCriteria() { + public clearCriteria(clearSearchTerm = false) { const ret = this.clone(); + if (clearSearchTerm) { + ret.searchTerm = ""; + } ret.criteria = []; ret.currentPage = 1; return ret; } + public clearSearchTerm() { + const ret = this.clone(); + ret.searchTerm = ""; + ret.currentPage = 1; // reset to first page + return ret; + } + public setCriteria(criteria: Criterion[]) { const ret = this.clone(); ret.criteria = criteria; diff --git a/ui/v2.5/src/models/list-filter/groups.ts b/ui/v2.5/src/models/list-filter/groups.ts index c96fd8dc6..5a263b272 100644 --- a/ui/v2.5/src/models/list-filter/groups.ts +++ b/ui/v2.5/src/models/list-filter/groups.ts @@ -35,6 +35,11 @@ const sortByOptions = [ messageID: "scene_count", value: "scenes_count", }, + { + messageID: "o_count", + value: "o_counter", + sfwMessageID: "o_count_sfw", + }, ]); const displayModeOptions = [DisplayMode.Grid]; const criterionOptions = [ @@ -49,6 +54,9 @@ const criterionOptions = [ RatingCriterionOption, PerformersCriterionOption, createDateCriterionOption("date"), + createMandatoryNumberCriterionOption("o_counter", "o_count", { + sfwMessageID: "o_count_sfw", + }), ContainingGroupsCriterionOption, SubGroupsCriterionOption, createMandatoryNumberCriterionOption("containing_group_count"), diff --git a/ui/v2.5/src/models/list-filter/images.ts b/ui/v2.5/src/models/list-filter/images.ts index d8619112d..2d3db8265 100644 --- a/ui/v2.5/src/models/list-filter/images.ts +++ b/ui/v2.5/src/models/list-filter/images.ts @@ -22,15 +22,23 @@ import { import { ListFilterOptions, MediaSortByOptions } from "./filter-options"; import { DisplayMode } from "./types"; import { GalleriesCriterionOption } from "./criteria/galleries"; +import { PhashCriterionOption } from "./criteria/phash"; const defaultSortBy = "path"; -const sortByOptions = ["filesize", "file_count", "date", ...MediaSortByOptions] +const sortByOptions = [ + "filesize", + "file_count", + "date", + "resolution", + ...MediaSortByOptions, +] .map(ListFilterOptions.createSortBy) .concat([ { messageID: "o_count", value: "o_counter", + sfwMessageID: "o_count_sfw", }, ]); const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall]; @@ -40,10 +48,13 @@ const criterionOptions = [ createStringCriterionOption("details"), createStringCriterionOption("photographer"), createMandatoryStringCriterionOption("checksum", "media_info.checksum"), + PhashCriterionOption, PathCriterionOption, GalleriesCriterionOption, OrganizedCriterionOption, - createMandatoryNumberCriterionOption("o_counter", "o_count"), + createMandatoryNumberCriterionOption("o_counter", "o_count", { + sfwMessageID: "o_count_sfw", + }), ResolutionCriterionOption, OrientationCriterionOption, ImageIsMissingCriterionOption, diff --git a/ui/v2.5/src/models/list-filter/performers.ts b/ui/v2.5/src/models/list-filter/performers.ts index a8d3c9096..c0bcb3bba 100644 --- a/ui/v2.5/src/models/list-filter/performers.ts +++ b/ui/v2.5/src/models/list-filter/performers.ts @@ -31,10 +31,11 @@ const sortByOptions = [ "penis_length", "play_count", "last_played_at", - "last_o_at", + "latest_scene", "career_length", "weight", "measurements", + "scenes_duration", ] .map(ListFilterOptions.createSortBy) .concat([ @@ -53,6 +54,12 @@ const sortByOptions = [ { messageID: "o_count", value: "o_counter", + sfwMessageID: "o_count_sfw", + }, + { + messageID: "last_o_at", + value: "last_o_at", + sfwMessageID: "last_o_at_sfw", }, ]); @@ -101,7 +108,9 @@ const criterionOptions = [ createMandatoryNumberCriterionOption("image_count"), createMandatoryNumberCriterionOption("gallery_count"), createMandatoryNumberCriterionOption("play_count"), - createMandatoryNumberCriterionOption("o_counter", "o_count"), + createMandatoryNumberCriterionOption("o_counter", "o_count", { + sfwMessageID: "o_count_sfw", + }), createBooleanCriterionOption("ignore_auto_tag"), CountryCriterionOption, createNumberCriterionOption("height_cm", "height"), diff --git a/ui/v2.5/src/models/list-filter/scenes.ts b/ui/v2.5/src/models/list-filter/scenes.ts index 592b8b6fe..251e2592d 100644 --- a/ui/v2.5/src/models/list-filter/scenes.ts +++ b/ui/v2.5/src/models/list-filter/scenes.ts @@ -44,15 +44,17 @@ const sortByOptions = [ "filesize", "duration", "framerate", + "resolution", "bitrate", "last_played_at", - "last_o_at", "resume_time", "play_duration", "play_count", "interactive", "interactive_speed", "perceptual_similarity", + "performer_age", + "studio", ...MediaSortByOptions, ] .map(ListFilterOptions.createSortBy) @@ -60,6 +62,12 @@ const sortByOptions = [ { messageID: "o_count", value: "o_counter", + sfwMessageID: "o_count_sfw", + }, + { + messageID: "last_o_at", + value: "last_o_at", + sfwMessageID: "last_o_at_sfw", }, { messageID: "group_scene_number", @@ -77,6 +85,12 @@ const displayModeOptions = [ DisplayMode.Tagger, ]; +export const PerformerAgeCriterionOption = + createMandatoryNumberCriterionOption("performer_age"); + +export const DurationCriterionOption = + createDurationCriterionOption("duration"); + const criterionOptions = [ createStringCriterionOption("title"), createStringCriterionOption("code", "scene_code"), @@ -89,14 +103,16 @@ const criterionOptions = [ DuplicatedCriterionOption, OrganizedCriterionOption, RatingCriterionOption, - createMandatoryNumberCriterionOption("o_counter", "o_count"), + createMandatoryNumberCriterionOption("o_counter", "o_count", { + sfwMessageID: "o_count_sfw", + }), ResolutionCriterionOption, OrientationCriterionOption, createMandatoryNumberCriterionOption("framerate"), createMandatoryNumberCriterionOption("bitrate"), createStringCriterionOption("video_codec"), createStringCriterionOption("audio_codec"), - createDurationCriterionOption("duration"), + DurationCriterionOption, createDurationCriterionOption("resume_time"), createDurationCriterionOption("play_duration"), createMandatoryNumberCriterionOption("play_count"), @@ -108,7 +124,7 @@ const criterionOptions = [ PerformerTagsCriterionOption, PerformersCriterionOption, createMandatoryNumberCriterionOption("performer_count"), - createMandatoryNumberCriterionOption("performer_age"), + PerformerAgeCriterionOption, PerformerFavoriteCriterionOption, // StudioTagsCriterionOption, StudiosCriterionOption, @@ -117,6 +133,7 @@ const criterionOptions = [ GalleriesCriterionOption, createStringCriterionOption("url"), StashIDCriterionOption, + createMandatoryNumberCriterionOption("stash_id_count"), InteractiveCriterionOption, CaptionsCriterionOption, createMandatoryNumberCriterionOption("interactive_speed"), diff --git a/ui/v2.5/src/models/list-filter/studios.ts b/ui/v2.5/src/models/list-filter/studios.ts index a25fd9e22..42ac1b4dc 100644 --- a/ui/v2.5/src/models/list-filter/studios.ts +++ b/ui/v2.5/src/models/list-filter/studios.ts @@ -15,7 +15,14 @@ import { ListFilterOptions } from "./filter-options"; import { DisplayMode } from "./types"; const defaultSortBy = "name"; -const sortByOptions = ["name", "tag_count", "random", "rating"] +const sortByOptions = [ + "name", + "tag_count", + "random", + "rating", + "scenes_duration", + "latest_scene", +] .map(ListFilterOptions.createSortBy) .concat([ { diff --git a/ui/v2.5/src/models/list-filter/tags.ts b/ui/v2.5/src/models/list-filter/tags.ts index db3a84666..e2d4fbed4 100644 --- a/ui/v2.5/src/models/list-filter/tags.ts +++ b/ui/v2.5/src/models/list-filter/tags.ts @@ -14,9 +14,10 @@ import { ParentTagsCriterionOption, } from "./criteria/tags"; import { FavoriteTagCriterionOption } from "./criteria/favorite"; +import { StashIDCriterionOption } from "./criteria/stash-ids"; const defaultSortBy = "name"; -const sortByOptions = ["name", "random"] +const sortByOptions = ["name", "random", "scenes_duration"] .map(ListFilterOptions.createSortBy) .concat([ { @@ -58,6 +59,7 @@ const criterionOptions = [ createStringCriterionOption("aliases"), createStringCriterionOption("description"), createBooleanCriterionOption("ignore_auto_tag"), + StashIDCriterionOption, createMandatoryNumberCriterionOption("scene_count"), createMandatoryNumberCriterionOption("image_count"), createMandatoryNumberCriterionOption("gallery_count"), diff --git a/ui/v2.5/src/models/list-filter/types.ts b/ui/v2.5/src/models/list-filter/types.ts index 83ebaa010..bf5fff4d9 100644 --- a/ui/v2.5/src/models/list-filter/types.ts +++ b/ui/v2.5/src/models/list-filter/types.ts @@ -200,6 +200,7 @@ export type CriterionType = | "ignore_auto_tag" | "file_count" | "stash_id_endpoint" + | "stash_id_count" | "date" | "created_at" | "updated_at" diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index da4a64765..dd881c0b4 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -617,7 +617,9 @@ declare namespace PluginApi { const FontAwesomeBrands: typeof import("@fortawesome/free-brands-svg-icons"); const Intl: typeof import("react-intl"); const Mousetrap: typeof import("mousetrap"); + const ReactFontAwesome: typeof import("@fortawesome/react-fontawesome"); const ReactSelect: typeof import("react-select"); + const ReactSlick: typeof import("@ant-design/react-slick"); // @ts-expect-error import { MousetrapStatic } from "mousetrap"; @@ -664,6 +666,8 @@ declare namespace PluginApi { DetailImage: React.FC; ExternalLinkButtons: React.FC; ExternalLinksButton: React.FC; + FilteredGalleryList: React.FC; + FilteredSceneList: React.FC; FolderSelect: React.FC; FrontPage: React.FC; GalleryCard: React.FC; @@ -671,14 +675,29 @@ declare namespace PluginApi { "GalleryCard.Image": React.FC; "GalleryCard.Overlays": React.FC; "GalleryCard.Popovers": React.FC; + GalleryCardGrid: React.FC; + GalleryAddPanel: React.FC; GalleryIDSelect: React.FC; + GalleryImagesPanel: React.FC; + GalleryList: React.FC; + GalleryRecommendationRow: React.FC; GallerySelect: React.FC; + GridCard: React.FC; + GroupCard: React.FC; + GroupCardGrid: React.FC; GroupIDSelect: React.FC; + GroupList: React.FC; + GroupRecommendationRow: React.FC; GroupSelect: React.FC; + GroupSubGroupsPanel: React.FC; HeaderImage: React.FC; HoverPopover: React.FC; Icon: React.FC; + ImageCard: React.FC; + ImageCardGrid: React.FC; ImageInput: React.FC; + ImageList: React.FC; + ImageRecommendationRow: React.FC; LightboxLink: React.FC; LoadingIndicator: React.FC; "MainNavBar.MenuItems": React.FC; @@ -687,6 +706,7 @@ declare namespace PluginApi { NumberSetting: React.FC; PerformerAppearsWithPanel: React.FC; PerformerCard: React.FC; + PerformerCardGrid: React.FC; "PerformerCard.Details": React.FC; "PerformerCard.Image": React.FC; "PerformerCard.Overlays": React.FC; @@ -699,13 +719,16 @@ declare namespace PluginApi { PerformerHeaderImage: React.FC; PerformerIDSelect: React.FC; PerformerImagesPanel: React.FC; + PerformerList: React.FC; PerformerPage: React.FC; + PerformerRecommendationRow: React.FC; PerformerScenesPanel: React.FC; PerformerSelect: React.FC; PluginSettings: React.FC; RatingNumber: React.FC; RatingStars: React.FC; RatingSystem: React.FC; + RecommendationRow: React.FC; SceneFileInfoPanel: React.FC; SceneIDSelect: React.FC; ScenePage: React.FC; @@ -717,13 +740,29 @@ declare namespace PluginApi { "SceneCard.Image": React.FC; "SceneCard.Overlays": React.FC; "SceneCard.Popovers": React.FC; + SceneCardGrid: React.FC; + SceneList: React.FC; + SceneListOperations: React.FC; + SceneMarkerCard: React.FC; + "SceneMarkerCard.Details": React.FC; + "SceneMarkerCard.Image": React.FC; + "SceneMarkerCard.Popovers": React.FC; + SceneMarkerCardGrid: React.FC; + SceneMarkerList: React.FC; + SceneMarkerRecommendationRow: React.FC; + SceneRecommendationRow: React.FC; SelectSetting: React.FC; Setting: React.FC; SettingGroup: React.FC; SettingModal: React.FC; StringListSetting: React.FC; StringSetting: React.FC; + StudioCard: React.FC; + StudioCardGrid: React.FC; + StudioDetailsPanel: React.FC; StudioIDSelect: React.FC; + StudioList: React.FC; + StudioRecommendationRow: React.FC; StudioSelect: React.FC; SweatDrops: React.FC; TabTitleCounter: React.FC; @@ -733,7 +772,10 @@ declare namespace PluginApi { "TagCard.Overlays": React.FC; "TagCard.Popovers": React.FC; "TagCard.Title": React.FC; + TagCardGrid: React.FC; TagLink: React.FC; + TagList: React.FC; + TagRecommendationRow: React.FC; TagSelect: React.FC; TruncatedText: React.FC; }; diff --git a/ui/v2.5/src/pluginApi.tsx b/ui/v2.5/src/pluginApi.tsx index 99d4a5992..72441cec9 100644 --- a/ui/v2.5/src/pluginApi.tsx +++ b/ui/v2.5/src/pluginApi.tsx @@ -12,13 +12,16 @@ import * as Intl from "react-intl"; import * as FontAwesomeSolid from "@fortawesome/free-solid-svg-icons"; import * as FontAwesomeRegular from "@fortawesome/free-regular-svg-icons"; import * as FontAwesomeBrands from "@fortawesome/free-brands-svg-icons"; +import * as ReactFontAwesome from "@fortawesome/react-fontawesome"; import * as ReactSelect from "react-select"; +import * as ReactSlick from "@ant-design/react-slick"; import { useSpriteInfo } from "./hooks/sprite"; import { useToast } from "./hooks/Toast"; import Event from "./hooks/event"; -import { before, instead, after, components, RegisterComponent } from "./patch"; +import { after, before, components, instead, RegisterComponent } from "./patch"; import { useSettings } from "./components/Settings/context"; import { useInteractive } from "./hooks/Interactive/context"; +import InteractiveUtils from "./hooks/Interactive/utils"; import { useLightbox, useGalleryLightbox } from "./hooks/Lightbox/hooks"; // due to code splitting, some components may not have been loaded when a plugin @@ -77,7 +80,9 @@ export const PluginApi = { FontAwesomeBrands, Mousetrap, MousetrapPause, + ReactFontAwesome, ReactSelect, + ReactSlick, }, register: { // register a route to be added to the main router @@ -152,6 +157,7 @@ export const PluginApi = { }, components, utils: { + InteractiveUtils, NavUtils, StashService, loadComponents, diff --git a/ui/v2.5/src/plugins.tsx b/ui/v2.5/src/plugins.tsx index 41577a92c..00ffb9ca4 100644 --- a/ui/v2.5/src/plugins.tsx +++ b/ui/v2.5/src/plugins.tsx @@ -59,7 +59,8 @@ function sortPlugins(plugins: PluginList) { // load all plugins and their dependencies // returns true when all plugins are loaded, regardess of success or failure -function useLoadPlugins() { +// if disableCustomizations is true, skip loading plugins entirely +function useLoadPlugins(disableCustomizations?: boolean) { const { data: plugins, loading: pluginsLoading, @@ -74,6 +75,12 @@ function useLoadPlugins() { }, [plugins?.plugins, pluginsLoading, pluginsError]); const pluginJavascripts = useMemoOnce(() => { + // Skip loading plugin JS if customizations are disabled. + // Note: We check inside useMemoOnce rather than early-returning from useLoadPlugins + // to comply with React's rules of hooks - hooks must be called unconditionally. + if (disableCustomizations) { + return [[], true]; + } return [ uniq( sortedPlugins @@ -83,9 +90,12 @@ function useLoadPlugins() { ), !!sortedPlugins && !pluginsLoading && !pluginsError, ]; - }, [sortedPlugins, pluginsLoading, pluginsError]); + }, [sortedPlugins, pluginsLoading, pluginsError, disableCustomizations]); const pluginCSS = useMemoOnce(() => { + if (disableCustomizations) { + return [[], true]; + } return [ uniq( sortedPlugins @@ -95,7 +105,7 @@ function useLoadPlugins() { ), !!sortedPlugins && !pluginsLoading && !pluginsError, ]; - }, [sortedPlugins, pluginsLoading, pluginsError]); + }, [sortedPlugins, pluginsLoading, pluginsError, disableCustomizations]); const pluginJavascriptLoaded = useScript( pluginJavascripts ?? [], @@ -109,11 +119,15 @@ function useLoadPlugins() { }; } -export const PluginsLoader: React.FC> = ({ - children, -}) => { +interface IPluginsLoaderProps { + disableCustomizations?: boolean; +} + +export const PluginsLoader: React.FC< + React.PropsWithChildren +> = ({ disableCustomizations, children }) => { const Toast = useToast(); - const { loading: loaded, error } = useLoadPlugins(); + const { loading: loaded, error } = useLoadPlugins(disableCustomizations); useEffect(() => { if (error) { diff --git a/ui/v2.5/src/polyfills.ts b/ui/v2.5/src/polyfills.ts index 55914e1d9..ca5620e81 100644 --- a/ui/v2.5/src/polyfills.ts +++ b/ui/v2.5/src/polyfills.ts @@ -3,7 +3,6 @@ import { shouldPolyfill as shouldPolyfillCanonicalLocales } from "@formatjs/intl import { shouldPolyfill as shouldPolyfillLocale } from "@formatjs/intl-locale/should-polyfill"; import { shouldPolyfill as shouldPolyfillNumberformat } from "@formatjs/intl-numberformat/should-polyfill"; import { shouldPolyfill as shouldPolyfillPluralRules } from "@formatjs/intl-pluralrules/should-polyfill"; -import "intersection-observer"; // needed for older safari versions import "event-target-polyfill"; @@ -27,11 +26,6 @@ async function checkPolyfills() { await import("@formatjs/intl-pluralrules/polyfill"); await import("@formatjs/intl-pluralrules/locale-data/en"); } - - if (!("ResizeObserver" in window)) { - const ResizeObserver = await import("resize-observer-polyfill"); - window.ResizeObserver = ResizeObserver.default; - } } export const initPolyfills = async () => { diff --git a/ui/v2.5/src/sfw-mode.scss b/ui/v2.5/src/sfw-mode.scss new file mode 100644 index 000000000..5ba449433 --- /dev/null +++ b/ui/v2.5/src/sfw-mode.scss @@ -0,0 +1,86 @@ +// hide nsfw elements when in sfw-content mode +// stylelint-disable selector-class-pattern +.sfw-content-mode { + // hide adult-oriented performer fields in sort by select + .sort-by-select, + .performer-table { + [data-value="ethnicity"], + [data-value="hair_color"], + [data-value="eye_color"], + [data-value="measurements"], + [data-value="weight"], + [data-value="weight_kg"], + [data-value="penis_length"], + [data-value="penis_length_cm"], + [data-value="circumcised"], + [data-value="fake_tits"] { + display: none; + } + } + + .performer-table { + td, + th { + &.ethnicity, + &.hair_color, + &.eye_color, + &.height, + &.measurements, + &.weight_kg, + &.penis_length_cm, + &.circumcised, + &.fake_tits { + &-head, + &-data { + display: none; + } + } + } + } + + #performer-edit, + &.scrape-dialog { + [data-field="ethnicity"], + [data-field="hair_color"], + [data-field="eye_color"], + [data-field="measurements"], + [data-field="weight"], + [data-field="penis_length"], + [data-field="circumcised"], + [data-field="fake_tits"], + [data-field="tattoos"], + [data-field="piercings"] { + display: none; + } + } + + &.edit-filter-dialog { + [data-type="ethnicity"], + [data-type="hair_color"], + [data-type="eye_color"], + [data-type="measurements"], + [data-type="weight"], + [data-type="penis_length"], + [data-type="circumcised"], + [data-type="fake_tits"], + [data-type="tattoos"], + [data-type="piercings"] { + display: none; + } + } + + #performer-page { + .detail-item.ethnicity, + .detail-item.hair_color, + .detail-item.eye_color, + .detail-item.measurements, + .detail-item.weight, + .detail-item.penis_length, + .detail-item.circumcised, + .detail-item.fake_tits, + .detail-item.tattoos, + .detail-item.piercings { + display: none; + } + } +} diff --git a/ui/v2.5/src/utils/focus.ts b/ui/v2.5/src/utils/focus.ts index f1ede47f9..cf1b20a88 100644 --- a/ui/v2.5/src/utils/focus.ts +++ b/ui/v2.5/src/utils/focus.ts @@ -2,10 +2,14 @@ import { useRef, useEffect, useCallback } from "react"; const useFocus = () => { const htmlElRef = useRef(null); - const setFocus = useCallback(() => { + const setFocus = useCallback((selectAll?: boolean) => { const currentEl = htmlElRef.current; if (currentEl) { - currentEl.focus(); + if (selectAll) { + currentEl.select(); + } else { + currentEl.focus(); + } } }, []); diff --git a/ui/v2.5/src/utils/form.tsx b/ui/v2.5/src/utils/form.tsx index f518d5700..fbf239a9b 100644 --- a/ui/v2.5/src/utils/form.tsx +++ b/ui/v2.5/src/utils/form.tsx @@ -175,7 +175,7 @@ export function formikUtils( props?: IProps ) { return ( - + {title} {control} @@ -308,10 +308,15 @@ export function formikUtils( } } + interface IStringListProps extends IProps { + // defaults to true if not provided + orderable?: boolean; + } + function renderStringListField( field: Field, messageID: string = field, - props?: IProps + props?: IStringListProps ) { const value = formik.values[field] as string[]; const error = formik.errors[field] as ErrorMessage[] | ErrorMessage; @@ -325,6 +330,7 @@ export function formikUtils( setValue={(v) => formik.setFieldValue(field, v)} errors={errorMsg} errorIdx={errorIdx} + orderable={props?.orderable} /> ); @@ -362,12 +368,10 @@ export function formikUtils( field: Field, linkType: LinkType, messageID: string = field, - props?: IProps + props?: IProps, + addButton?: React.ReactNode ) { const values = formik.values[field] as GQL.StashIdInput[]; - if (!values.length) { - return; - } const title = intl.formatMessage({ id: messageID }); @@ -377,26 +381,31 @@ export function formikUtils( }; const control = ( -
          - {values.map((stashID) => { - return ( - - - - - ); - })} -
        + <> + {values.length > 0 && ( +
          + {values.map((stashID) => { + return ( + + + + + ); + })} +
        + )} + {addButton} + ); return renderField(field, title, control, props); diff --git a/ui/v2.5/src/utils/gender.ts b/ui/v2.5/src/utils/gender.ts index 29502695c..09d3eddec 100644 --- a/ui/v2.5/src/utils/gender.ts +++ b/ui/v2.5/src/utils/gender.ts @@ -1,14 +1,23 @@ import * as GQL from "../core/generated-graphql"; export const stringGenderMap = new Map([ - ["Male", GQL.GenderEnum.Male], ["Female", GQL.GenderEnum.Female], + ["Male", GQL.GenderEnum.Male], ["Transgender Male", GQL.GenderEnum.TransgenderMale], ["Transgender Female", GQL.GenderEnum.TransgenderFemale], ["Intersex", GQL.GenderEnum.Intersex], ["Non-Binary", GQL.GenderEnum.NonBinary], ]); +export const genderList = [ + GQL.GenderEnum.Female, + GQL.GenderEnum.Male, + GQL.GenderEnum.TransgenderFemale, + GQL.GenderEnum.TransgenderMale, + GQL.GenderEnum.Intersex, + GQL.GenderEnum.NonBinary, +]; + export const genderToString = (value?: GQL.GenderEnum | string | null) => { if (!value) { return undefined; diff --git a/ui/v2.5/src/utils/history.ts b/ui/v2.5/src/utils/history.ts new file mode 100644 index 000000000..6ae7b637f --- /dev/null +++ b/ui/v2.5/src/utils/history.ts @@ -0,0 +1,11 @@ +import { useHistory } from "react-router-dom"; + +type History = ReturnType; + +export function goBackOrReplace(history: History, defaultPath: string) { + if (history.length > 1) { + history.goBack(); + } else { + history.replace(defaultPath); + } +} diff --git a/ui/v2.5/src/utils/navigation.ts b/ui/v2.5/src/utils/navigation.ts index 581d079c7..17d9dfe6b 100644 --- a/ui/v2.5/src/utils/navigation.ts +++ b/ui/v2.5/src/utils/navigation.ts @@ -342,6 +342,15 @@ const makeScenesPHashMatchUrl = (phash: GQL.Maybe | undefined) => { return `/scenes?${filter.makeQueryParameters()}`; }; +const makeImagesPHashMatchUrl = (phash: GQL.Maybe | undefined) => { + if (!phash) return "#"; + const filter = new ListFilterModel(GQL.FilterMode.Images, undefined); + const criterion = new PhashCriterion(); + criterion.value = { value: phash }; + filter.criteria.push(criterion); + return `/images?${filter.makeQueryParameters()}`; +}; + const makeGalleryImagesUrl = ( gallery: Partial, extraCriteria?: ModifierCriterion[] @@ -493,6 +502,7 @@ const NavUtils = { makeTagGroupsUrl, makeScenesPHashMatchUrl, makeSceneMarkerUrl, + makeImagesPHashMatchUrl, makeGroupScenesUrl, makeChildStudiosUrl, makeGalleryImagesUrl, diff --git a/ui/v2.5/src/utils/stashIds.ts b/ui/v2.5/src/utils/stashIds.ts index 289ce9c9d..10e3835b8 100644 --- a/ui/v2.5/src/utils/stashIds.ts +++ b/ui/v2.5/src/utils/stashIds.ts @@ -1,3 +1,5 @@ +import * as GQL from "src/core/generated-graphql"; + export const getStashIDs = ( ids?: { stash_id: string; endpoint: string; updated_at: string }[] ) => @@ -6,3 +8,62 @@ export const getStashIDs = ( endpoint, updated_at, })); + +// UUID regex pattern to detect StashIDs (supports v4 and v7) +const UUID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[47][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +/** + * Separates a list of inputs into names and StashIDs based on UUID pattern matching + * @param inputs - Array of strings that could be either names or StashIDs + * @returns Object containing separate arrays for names and stashIds + */ +export const separateNamesAndStashIds = ( + inputs: string[] +): { names: string[]; stashIds: string[] } => { + const names: string[] = []; + const stashIds: string[] = []; + + inputs.forEach((input) => { + if (UUID_PATTERN.test(input)) { + stashIds.push(input); + } else { + names.push(input); + } + }); + + return { names, stashIds }; +}; + +/** + * Utility to add or update a StashID in an array. + * If a StashID with the same endpoint exists, it will be replaced. + * Otherwise, the new StashID will be appended. + */ +export const addUpdateStashID = ( + existingStashIDs: GQL.StashIdInput[], + newItem: GQL.StashIdInput, + allowMultiple: boolean = false +): GQL.StashIdInput[] => { + const existingIndex = existingStashIDs.findIndex( + (s) => s.endpoint === newItem.endpoint + ); + + if (!allowMultiple && existingIndex >= 0) { + const newStashIDs = [...existingStashIDs]; + newStashIDs[existingIndex] = newItem; + return newStashIDs; + } + + // ensure we don't add duplicates if allowMultiple is true + if ( + allowMultiple && + existingStashIDs.some( + (s) => s.endpoint === newItem.endpoint && s.stash_id === newItem.stash_id + ) + ) { + return existingStashIDs; + } + + return [...existingStashIDs, newItem]; +}; diff --git a/ui/v2.5/src/utils/text.ts b/ui/v2.5/src/utils/text.ts index dc654ae18..2c5bb4648 100644 --- a/ui/v2.5/src/utils/text.ts +++ b/ui/v2.5/src/utils/text.ts @@ -336,8 +336,10 @@ function dateTimeToString(date: Date) { const getAge = (dateString?: string | null, fromDateString?: string | null) => { if (!dateString) return 0; - const birthdate = stringToDate(dateString); - const fromDate = fromDateString ? stringToDate(fromDateString) : new Date(); + const birthdate = stringToFuzzyDate(dateString); + const fromDate = fromDateString + ? stringToFuzzyDate(fromDateString) + : new Date(); if (!birthdate || !fromDate) return 0; @@ -459,6 +461,38 @@ const formatDate = (intl: IntlShape, date?: string, utc = true) => { }); }; +const formatFuzzyDate = (intl: IntlShape, date?: string, utc = true) => { + if (!date) { + return ""; + } + + // handle year or year/month dates + const yearMatch = date.match(/^(\d{4})$/); + if (yearMatch) { + const year = parseInt(yearMatch[1], 10); + return intl.formatDate(Date.UTC(year, 0), { + year: "numeric", + timeZone: utc ? "utc" : undefined, + }); + } + + const yearMonthMatch = date.match(/^(\d{4})-(\d{2})$/); + if (yearMonthMatch) { + const year = parseInt(yearMonthMatch[1], 10); + const month = parseInt(yearMonthMatch[2], 10) - 1; + return intl.formatDate(Date.UTC(year, month), { + year: "numeric", + month: "long", + timeZone: utc ? "utc" : undefined, + }); + } + + return intl.formatDate(date, { + format: "long", + timeZone: utc ? "utc" : undefined, + }); +}; + const formatDateTime = (intl: IntlShape, dateTime?: string, utc = false) => `${formatDate(intl, dateTime, utc)} ${intl.formatTime(dateTime, { timeZone: utc ? "utc" : undefined, @@ -519,6 +553,7 @@ const TextUtils = { sanitiseURL, domainFromURL, formatDate, + formatFuzzyDate, formatDateTime, secondsAsTimeString, abbreviateCounter, diff --git a/ui/v2.5/src/utils/yup.ts b/ui/v2.5/src/utils/yup.ts index e2c4987a8..a9c4f69e1 100644 --- a/ui/v2.5/src/utils/yup.ts +++ b/ui/v2.5/src/utils/yup.ts @@ -92,45 +92,6 @@ export function yupUniqueStringList(intl: IntlShape) { }); } -export function yupUniqueAliases(intl: IntlShape, nameField: string) { - return yupRequiredStringArray(intl) - .defined() - .test({ - name: "unique", - test(value) { - const aliases = [this.parent[nameField].toLowerCase()]; - const dupes: number[] = []; - for (let i = 0; i < value.length; i++) { - const s = value[i].toLowerCase(); - if (aliases.includes(s)) { - dupes.push(i); - } else { - aliases.push(s); - } - } - if (dupes.length === 0) return true; - - const msg = yup.ValidationError.formatError( - intl.formatMessage({ id: "validation.unique" }), - { - label: this.schema.spec.label, - path: this.path, - } - ); - const errors = dupes.map( - (i) => - new yup.ValidationError( - msg, - value[i], - `${this.path}["${i}"]`, - "unique" - ) - ); - return new yup.ValidationError(errors, value, this.path, "unique"); - }, - }); -} - export function yupDateString(intl: IntlShape) { return yup .string() @@ -139,8 +100,22 @@ export function yupDateString(intl: IntlShape) { name: "date", test(value) { if (!value) return true; - if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false; - if (Number.isNaN(Date.parse(value))) return false; + // Allow YYYY, YYYY-MM, or YYYY-MM-DD formats + if (!value.match(/^\d{4}(-\d{2}(-\d{2})?)?$/)) return false; + // Validate the date components + const parts = value.split("-"); + const year = parseInt(parts[0], 10); + if (year < 1 || year > 9999) return false; + if (parts.length >= 2) { + const month = parseInt(parts[1], 10); + if (month < 1 || month > 12) return false; + } + if (parts.length === 3) { + const day = parseInt(parts[2], 10); + if (day < 1 || day > 31) return false; + // Full date - validate it parses correctly + if (Number.isNaN(Date.parse(value))) return false; + } return true; }, message: intl.formatMessage({ id: "validation.date_invalid_form" }), diff --git a/ui/v2.5/tsconfig.json b/ui/v2.5/tsconfig.json index 93c8c0b5a..4b68980fd 100644 --- a/ui/v2.5/tsconfig.json +++ b/ui/v2.5/tsconfig.json @@ -19,7 +19,7 @@ "isolatedModules": true, "noFallthroughCasesInSwitch": true, "useDefineForClassFields": true, - "types": ["vite/client"] + "types": ["vite/client", "dom-screen-wake-lock"] }, "include": ["src"] } diff --git a/ui/v2.5/yarn.lock b/ui/v2.5/yarn.lock deleted file mode 100644 index 76a398f9f..000000000 --- a/ui/v2.5/yarn.lock +++ /dev/null @@ -1,8344 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@ampproject/remapping@^2.2.0": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" - integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@ant-design/react-slick@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@ant-design/react-slick/-/react-slick-1.0.0.tgz#4696eecaa2dea0429e47ae24c267015cfd6df35c" - integrity sha512-OKxZsn8TAf8fYxP79rDXgLs9zvKMTslK6dJ4iLhDXOujUqC5zJPBRszyrcEHXcMPOm1Sgk40JgyF3yiL/Swd7w== - dependencies: - "@babel/runtime" "^7.10.4" - classnames "^2.2.5" - json2mq "^0.2.0" - resize-observer-polyfill "^1.5.1" - throttle-debounce "^5.0.0" - -"@apollo/client@^3.8.0", "@apollo/client@^3.8.10": - version "3.8.10" - resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.8.10.tgz#db6ee4378212d93c1f22b90a2aa474f6e9664b68" - integrity sha512-p/22RZ8ehHyvySnC20EHPPe0gdu8Xp6ZCiXOfdEe1ZORw5cUteD/TLc66tfKv8qu8NLIfbiWoa+6s70XnKvxqg== - dependencies: - "@graphql-typed-document-node/core" "^3.1.1" - "@wry/equality" "^0.5.6" - "@wry/trie" "^0.5.0" - graphql-tag "^2.12.6" - hoist-non-react-statics "^3.3.2" - optimism "^0.18.0" - prop-types "^15.7.2" - response-iterator "^0.2.6" - symbol-observable "^4.0.0" - ts-invariant "^0.10.3" - tslib "^2.3.0" - zen-observable-ts "^1.2.5" - -"@ardatan/relay-compiler@12.0.0": - version "12.0.0" - resolved "https://registry.yarnpkg.com/@ardatan/relay-compiler/-/relay-compiler-12.0.0.tgz#2e4cca43088e807adc63450e8cab037020e91106" - integrity sha512-9anThAaj1dQr6IGmzBMcfzOQKTa5artjuPmw8NYK/fiGEMjADbSguBY2FMDykt+QhilR3wc9VA/3yVju7JHg7Q== - dependencies: - "@babel/core" "^7.14.0" - "@babel/generator" "^7.14.0" - "@babel/parser" "^7.14.0" - "@babel/runtime" "^7.0.0" - "@babel/traverse" "^7.14.0" - "@babel/types" "^7.0.0" - babel-preset-fbjs "^3.4.0" - chalk "^4.0.0" - fb-watchman "^2.0.0" - fbjs "^3.0.0" - glob "^7.1.1" - immutable "~3.7.6" - invariant "^2.2.4" - nullthrows "^1.1.1" - relay-runtime "12.0.0" - signedsource "^1.0.0" - yargs "^15.3.1" - -"@ardatan/sync-fetch@^0.0.1": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@ardatan/sync-fetch/-/sync-fetch-0.0.1.tgz#3385d3feedceb60a896518a1db857ec1e945348f" - integrity sha512-xhlTqH0m31mnsG0tIP4ETgfSB6gXDaYYsUWTrlUV93fFQPI9dd8hE0Ot6MHLCtqgB32hwJAC3YZMWlXZw7AleA== - dependencies: - node-fetch "^2.6.1" - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.23.5": - version "7.23.5" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" - integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== - dependencies: - "@babel/highlight" "^7.23.4" - chalk "^2.4.2" - -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.23.5": - version "7.23.5" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" - integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== - -"@babel/core@^7.14.0", "@babel/core@^7.20.12", "@babel/core@^7.22.9", "@babel/core@^7.9.0": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.9.tgz#b028820718000f267870822fec434820e9b1e4d1" - integrity sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.23.5" - "@babel/generator" "^7.23.6" - "@babel/helper-compilation-targets" "^7.23.6" - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helpers" "^7.23.9" - "@babel/parser" "^7.23.9" - "@babel/template" "^7.23.9" - "@babel/traverse" "^7.23.9" - "@babel/types" "^7.23.9" - convert-source-map "^2.0.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.3" - semver "^6.3.1" - -"@babel/generator@^7.14.0", "@babel/generator@^7.18.13", "@babel/generator@^7.23.6": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" - integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== - dependencies: - "@babel/types" "^7.23.6" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/helper-annotate-as-pure@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" - integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz#acd4edfd7a566d1d51ea975dff38fd52906981bb" - integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.18.6" - "@babel/types" "^7.18.9" - -"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.23.6": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" - integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== - dependencies: - "@babel/compat-data" "^7.23.5" - "@babel/helper-validator-option" "^7.23.5" - browserslist "^4.22.2" - lru-cache "^5.1.1" - semver "^6.3.1" - -"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.0.tgz#64f49ecb0020532f19b1d014b03bccaa1ab85fb9" - integrity sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-member-expression-to-functions" "^7.21.0" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-replace-supers" "^7.20.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/helper-split-export-declaration" "^7.18.6" - -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.0.tgz#53ff78472e5ce10a52664272a239787107603ebb" - integrity sha512-N+LaFW/auRSWdx7SHD/HiARwXQju1vXTW4fKr4u5SgBUTm51OKEjKgj+cs00ggW3kEvNqwErnlwuq7Y3xBe4eg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - regexpu-core "^5.3.1" - -"@babel/helper-define-polyfill-provider@^0.3.3": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz#8612e55be5d51f0cd1f36b4a5a83924e89884b7a" - integrity sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww== - dependencies: - "@babel/helper-compilation-targets" "^7.17.7" - "@babel/helper-plugin-utils" "^7.16.7" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - semver "^6.1.2" - -"@babel/helper-environment-visitor@^7.18.9", "@babel/helper-environment-visitor@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" - integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== - -"@babel/helper-explode-assignable-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" - integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.21.0", "@babel/helper-function-name@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" - integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== - dependencies: - "@babel/template" "^7.22.15" - "@babel/types" "^7.23.0" - -"@babel/helper-hoist-variables@^7.18.6", "@babel/helper-hoist-variables@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" - integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz#319c6a940431a133897148515877d2f3269c3ba5" - integrity sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q== - dependencies: - "@babel/types" "^7.21.0" - -"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6", "@babel/helper-module-imports@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" - integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== - dependencies: - "@babel/types" "^7.22.15" - -"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" - integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== - dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-module-imports" "^7.22.15" - "@babel/helper-simple-access" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/helper-validator-identifier" "^7.22.20" - -"@babel/helper-optimise-call-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" - integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" - integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== - -"@babel/helper-remap-async-to-generator@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" - integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-wrap-function" "^7.18.9" - "@babel/types" "^7.18.9" - -"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz#243ecd2724d2071532b2c8ad2f0f9f083bcae331" - integrity sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-member-expression-to-functions" "^7.20.7" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/helper-simple-access@^7.20.2", "@babel/helper-simple-access@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" - integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-skip-transparent-expression-wrappers@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" - integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg== - dependencies: - "@babel/types" "^7.20.0" - -"@babel/helper-split-export-declaration@^7.18.6", "@babel/helper-split-export-declaration@^7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" - integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-string-parser@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" - integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== - -"@babel/helper-validator-identifier@^7.19.1", "@babel/helper-validator-identifier@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" - integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== - -"@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.23.5": - version "7.23.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" - integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== - -"@babel/helper-wrap-function@^7.18.9": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz#75e2d84d499a0ab3b31c33bcfe59d6b8a45f62e3" - integrity sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q== - dependencies: - "@babel/helper-function-name" "^7.19.0" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.5" - "@babel/types" "^7.20.5" - -"@babel/helpers@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.9.tgz#c3e20bbe7f7a7e10cb9b178384b4affdf5995c7d" - integrity sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ== - dependencies: - "@babel/template" "^7.23.9" - "@babel/traverse" "^7.23.9" - "@babel/types" "^7.23.9" - -"@babel/highlight@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" - integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== - dependencies: - "@babel/helper-validator-identifier" "^7.22.20" - chalk "^2.4.2" - js-tokens "^4.0.0" - -"@babel/parser@^7.1.0", "@babel/parser@^7.14.0", "@babel/parser@^7.16.8", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" - integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" - integrity sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz#d9c85589258539a22a901033853101a6198d4ef1" - integrity sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/plugin-proposal-optional-chaining" "^7.20.7" - -"@babel/plugin-proposal-async-generator-functions@^7.20.1": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326" - integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-remap-async-to-generator" "^7.18.9" - "@babel/plugin-syntax-async-generators" "^7.8.4" - -"@babel/plugin-proposal-class-properties@^7.0.0", "@babel/plugin-proposal-class-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" - integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-proposal-class-static-block@^7.18.6": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz#77bdd66fb7b605f3a61302d224bdfacf5547977d" - integrity sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.21.0" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - -"@babel/plugin-proposal-dynamic-import@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz#72bcf8d408799f547d759298c3c27c7e7faa4d94" - integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-proposal-export-namespace-from@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz#5f7313ab348cdb19d590145f9247540e94761203" - integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-proposal-json-strings@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz#7e8788c1811c393aff762817e7dbf1ebd0c05f0b" - integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-json-strings" "^7.8.3" - -"@babel/plugin-proposal-logical-assignment-operators@^7.18.9": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz#dfbcaa8f7b4d37b51e8bfb46d94a5aea2bb89d83" - integrity sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" - integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-proposal-numeric-separator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75" - integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-proposal-object-rest-spread@^7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.20.2": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" - integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== - dependencies: - "@babel/compat-data" "^7.20.5" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.20.7" - -"@babel/plugin-proposal-optional-catch-binding@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz#f9400d0e6a3ea93ba9ef70b09e72dd6da638a2cb" - integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-proposal-optional-chaining@^7.18.9", "@babel/plugin-proposal-optional-chaining@^7.20.7": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz#886f5c8978deb7d30f678b2e24346b287234d3ea" - integrity sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-proposal-private-methods@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" - integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-proposal-private-property-in-object@^7.18.6": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz#19496bd9883dd83c23c7d7fc45dcd9ad02dfa1dc" - integrity sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-create-class-features-plugin" "^7.21.0" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - -"@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e" - integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.0.0", "@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz#774d825256f2379d06139be0c723c4dd444f3ca1" - integrity sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-syntax-import-assertions@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz#bb50e0d4bea0957235390641209394e87bdb9cc4" - integrity sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ== - dependencies: - "@babel/helper-plugin-utils" "^7.19.0" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.17.12", "@babel/plugin-syntax-jsx@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" - integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-arrow-functions@^7.0.0", "@babel/plugin-transform-arrow-functions@^7.18.6": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz#bea332b0e8b2dab3dafe55a163d8227531ab0551" - integrity sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-async-to-generator@^7.18.6": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz#dfee18623c8cb31deb796aa3ca84dda9cea94354" - integrity sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q== - dependencies: - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-remap-async-to-generator" "^7.18.9" - -"@babel/plugin-transform-block-scoped-functions@^7.0.0", "@babel/plugin-transform-block-scoped-functions@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz#9187bf4ba302635b9d70d986ad70f038726216a8" - integrity sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.20.2": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz#e737b91037e5186ee16b76e7ae093358a5634f02" - integrity sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-classes@^7.0.0", "@babel/plugin-transform-classes@^7.20.2": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz#f469d0b07a4c5a7dbb21afad9e27e57b47031665" - integrity sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-replace-supers" "^7.20.7" - "@babel/helper-split-export-declaration" "^7.18.6" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.0.0", "@babel/plugin-transform-computed-properties@^7.18.9": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz#704cc2fd155d1c996551db8276d55b9d46e4d0aa" - integrity sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/template" "^7.20.7" - -"@babel/plugin-transform-destructuring@^7.0.0", "@babel/plugin-transform-destructuring@^7.20.2": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz#8bda578f71620c7de7c93af590154ba331415454" - integrity sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz#b286b3e7aae6c7b861e45bed0a2fafd6b1a4fef8" - integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-duplicate-keys@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz#687f15ee3cdad6d85191eb2a372c4528eaa0ae0e" - integrity sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-exponentiation-operator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz#421c705f4521888c65e91fdd1af951bfefd4dacd" - integrity sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-flow-strip-types@^7.0.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.19.0.tgz#e9e8606633287488216028719638cbbb2f2dde8f" - integrity sha512-sgeMlNaQVbCSpgLSKP4ZZKfsJVnFnNQlUSk6gPYzR/q7tzCgQF2t8RBKAP6cKJeZdveei7Q7Jm527xepI8lNLg== - dependencies: - "@babel/helper-plugin-utils" "^7.19.0" - "@babel/plugin-syntax-flow" "^7.18.6" - -"@babel/plugin-transform-for-of@^7.0.0", "@babel/plugin-transform-for-of@^7.18.8": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz#964108c9988de1a60b4be2354a7d7e245f36e86e" - integrity sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-function-name@^7.0.0", "@babel/plugin-transform-function-name@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz#cc354f8234e62968946c61a46d6365440fc764e0" - integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ== - dependencies: - "@babel/helper-compilation-targets" "^7.18.9" - "@babel/helper-function-name" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-literals@^7.0.0", "@babel/plugin-transform-literals@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz#72796fdbef80e56fba3c6a699d54f0de557444bc" - integrity sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-member-expression-literals@^7.0.0", "@babel/plugin-transform-member-expression-literals@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz#ac9fdc1a118620ac49b7e7a5d2dc177a1bfee88e" - integrity sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-modules-amd@^7.19.6": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz#3daccca8e4cc309f03c3a0c4b41dc4b26f55214a" - integrity sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g== - dependencies: - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.19.6": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.20.11.tgz#8cb23010869bf7669fd4b3098598b6b2be6dc607" - integrity sha512-S8e1f7WQ7cimJQ51JkAaDrEtohVEitXjgCGAS2N8S31Y42E+kWwfSz83LYz57QdBm7q9diARVqanIaH2oVgQnw== - dependencies: - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-simple-access" "^7.20.2" - -"@babel/plugin-transform-modules-systemjs@^7.19.6": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz#467ec6bba6b6a50634eea61c9c232654d8a4696e" - integrity sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw== - dependencies: - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-validator-identifier" "^7.19.1" - -"@babel/plugin-transform-modules-umd@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz#81d3832d6034b75b54e62821ba58f28ed0aab4b9" - integrity sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ== - dependencies: - "@babel/helper-module-transforms" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.19.1": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz#626298dd62ea51d452c3be58b285d23195ba69a8" - integrity sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.20.5" - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-new-target@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz#d128f376ae200477f37c4ddfcc722a8a1b3246a8" - integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-object-super@^7.0.0", "@babel/plugin-transform-object-super@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" - integrity sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-replace-supers" "^7.18.6" - -"@babel/plugin-transform-parameters@^7.0.0", "@babel/plugin-transform-parameters@^7.20.1", "@babel/plugin-transform-parameters@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz#0ee349e9d1bc96e78e3b37a7af423a4078a7083f" - integrity sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-property-literals@^7.0.0", "@babel/plugin-transform-property-literals@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz#e22498903a483448e94e032e9bbb9c5ccbfc93a3" - integrity sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-react-display-name@^7.0.0": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz#8b1125f919ef36ebdfff061d664e266c666b9415" - integrity sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-react-jsx-self@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.18.6.tgz#3849401bab7ae8ffa1e3e5687c94a753fc75bda7" - integrity sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-react-jsx-source@^7.19.6": - version "7.19.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.19.6.tgz#88578ae8331e5887e8ce28e4c9dc83fb29da0b86" - integrity sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ== - dependencies: - "@babel/helper-plugin-utils" "^7.19.0" - -"@babel/plugin-transform-react-jsx@^7.0.0": - version "7.20.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.20.13.tgz#f950f0b0c36377503d29a712f16287cedf886cbb" - integrity sha512-MmTZx/bkUrfJhhYAYt3Urjm+h8DQGrPrnKQ94jLo7NLuOU+T89a7IByhKmrb8SKhrIYIQ0FN0CHMbnFRen4qNw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-jsx" "^7.18.6" - "@babel/types" "^7.20.7" - -"@babel/plugin-transform-regenerator@^7.18.6": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz#57cda588c7ffb7f4f8483cc83bdcea02a907f04d" - integrity sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - regenerator-transform "^0.15.1" - -"@babel/plugin-transform-reserved-words@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz#b1abd8ebf8edaa5f7fe6bbb8d2133d23b6a6f76a" - integrity sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-shorthand-properties@^7.0.0", "@babel/plugin-transform-shorthand-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz#6d6df7983d67b195289be24909e3f12a8f664dc9" - integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-spread@^7.0.0", "@babel/plugin-transform-spread@^7.19.0": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz#c2d83e0b99d3bf83e07b11995ee24bf7ca09401e" - integrity sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - -"@babel/plugin-transform-sticky-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz#c6706eb2b1524028e317720339583ad0f444adcc" - integrity sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-template-literals@^7.0.0", "@babel/plugin-transform-template-literals@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz#04ec6f10acdaa81846689d63fae117dd9c243a5e" - integrity sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-typeof-symbol@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz#c8cea68263e45addcd6afc9091429f80925762c0" - integrity sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-unicode-escapes@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz#1ecfb0eda83d09bbcb77c09970c2dd55832aa246" - integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-unicode-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz#194317225d8c201bbae103364ffe9e2cea36cdca" - integrity sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/preset-env@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.20.2.tgz#9b1642aa47bb9f43a86f9630011780dab7f86506" - integrity sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg== - dependencies: - "@babel/compat-data" "^7.20.1" - "@babel/helper-compilation-targets" "^7.20.0" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-validator-option" "^7.18.6" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9" - "@babel/plugin-proposal-async-generator-functions" "^7.20.1" - "@babel/plugin-proposal-class-properties" "^7.18.6" - "@babel/plugin-proposal-class-static-block" "^7.18.6" - "@babel/plugin-proposal-dynamic-import" "^7.18.6" - "@babel/plugin-proposal-export-namespace-from" "^7.18.9" - "@babel/plugin-proposal-json-strings" "^7.18.6" - "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" - "@babel/plugin-proposal-numeric-separator" "^7.18.6" - "@babel/plugin-proposal-object-rest-spread" "^7.20.2" - "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" - "@babel/plugin-proposal-optional-chaining" "^7.18.9" - "@babel/plugin-proposal-private-methods" "^7.18.6" - "@babel/plugin-proposal-private-property-in-object" "^7.18.6" - "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.20.0" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-transform-arrow-functions" "^7.18.6" - "@babel/plugin-transform-async-to-generator" "^7.18.6" - "@babel/plugin-transform-block-scoped-functions" "^7.18.6" - "@babel/plugin-transform-block-scoping" "^7.20.2" - "@babel/plugin-transform-classes" "^7.20.2" - "@babel/plugin-transform-computed-properties" "^7.18.9" - "@babel/plugin-transform-destructuring" "^7.20.2" - "@babel/plugin-transform-dotall-regex" "^7.18.6" - "@babel/plugin-transform-duplicate-keys" "^7.18.9" - "@babel/plugin-transform-exponentiation-operator" "^7.18.6" - "@babel/plugin-transform-for-of" "^7.18.8" - "@babel/plugin-transform-function-name" "^7.18.9" - "@babel/plugin-transform-literals" "^7.18.9" - "@babel/plugin-transform-member-expression-literals" "^7.18.6" - "@babel/plugin-transform-modules-amd" "^7.19.6" - "@babel/plugin-transform-modules-commonjs" "^7.19.6" - "@babel/plugin-transform-modules-systemjs" "^7.19.6" - "@babel/plugin-transform-modules-umd" "^7.18.6" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.19.1" - "@babel/plugin-transform-new-target" "^7.18.6" - "@babel/plugin-transform-object-super" "^7.18.6" - "@babel/plugin-transform-parameters" "^7.20.1" - "@babel/plugin-transform-property-literals" "^7.18.6" - "@babel/plugin-transform-regenerator" "^7.18.6" - "@babel/plugin-transform-reserved-words" "^7.18.6" - "@babel/plugin-transform-shorthand-properties" "^7.18.6" - "@babel/plugin-transform-spread" "^7.19.0" - "@babel/plugin-transform-sticky-regex" "^7.18.6" - "@babel/plugin-transform-template-literals" "^7.18.9" - "@babel/plugin-transform-typeof-symbol" "^7.18.9" - "@babel/plugin-transform-unicode-escapes" "^7.18.10" - "@babel/plugin-transform-unicode-regex" "^7.18.6" - "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.20.2" - babel-plugin-polyfill-corejs2 "^0.3.3" - babel-plugin-polyfill-corejs3 "^0.6.0" - babel-plugin-polyfill-regenerator "^0.4.1" - core-js-compat "^3.25.1" - semver "^6.3.0" - -"@babel/preset-modules@^0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" - integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/regjsgen@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" - integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== - -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" - integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== - dependencies: - regenerator-runtime "^0.14.0" - -"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.22.15", "@babel/template@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a" - integrity sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA== - dependencies: - "@babel/code-frame" "^7.23.5" - "@babel/parser" "^7.23.9" - "@babel/types" "^7.23.9" - -"@babel/traverse@^7.14.0", "@babel/traverse@^7.16.8", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" - integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== - dependencies: - "@babel/code-frame" "^7.23.5" - "@babel/generator" "^7.23.6" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.23.9" - "@babel/types" "^7.23.9" - debug "^4.3.1" - globals "^11.1.0" - -"@babel/types@^7.0.0", "@babel/types@^7.16.8", "@babel/types@^7.18.13", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.23.9", "@babel/types@^7.3.0", "@babel/types@^7.4.4", "@babel/types@^7.9.5": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002" - integrity sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q== - dependencies: - "@babel/helper-string-parser" "^7.23.4" - "@babel/helper-validator-identifier" "^7.22.20" - to-fast-properties "^2.0.0" - -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== - dependencies: - "@jridgewell/trace-mapping" "0.3.9" - -"@csstools/css-parser-algorithms@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.0.tgz#0cc3a656dc2d638370ecf6f98358973bfbd00141" - integrity sha512-dTKSIHHWc0zPvcS5cqGP+/TPFUJB0ekJ9dGKvMAFoNuBFhDPBt9OMGNZiIA5vTiNdGHHBeScYPXIGBMnVOahsA== - -"@csstools/css-tokenizer@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz#07ae11a0a06365d7ec686549db7b729bc036528e" - integrity sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA== - -"@csstools/media-query-list-parser@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.2.tgz#6ef642b728d30c1009bfbba3211c7e4c11302728" - integrity sha512-M8cFGGwl866o6++vIY7j1AKuq9v57cf+dGepScwCcbut9ypJNr4Cj+LLTWligYUZ0uyhEoJDKt5lvyBfh2L3ZQ== - -"@csstools/selector-specificity@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-3.0.0.tgz#798622546b63847e82389e473fd67f2707d82247" - integrity sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g== - -"@emotion/babel-plugin@^11.10.5": - version "11.10.5" - resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz#65fa6e1790ddc9e23cc22658a4c5dea423c55c3c" - integrity sha512-xE7/hyLHJac7D2Ve9dKroBBZqBT7WuPQmWcq7HSGb84sUuP4mlOWoB8dvVfD9yk5DHkU1m6RW7xSoDtnQHNQeA== - dependencies: - "@babel/helper-module-imports" "^7.16.7" - "@babel/plugin-syntax-jsx" "^7.17.12" - "@babel/runtime" "^7.18.3" - "@emotion/hash" "^0.9.0" - "@emotion/memoize" "^0.8.0" - "@emotion/serialize" "^1.1.1" - babel-plugin-macros "^3.1.0" - convert-source-map "^1.5.0" - escape-string-regexp "^4.0.0" - find-root "^1.1.0" - source-map "^0.5.7" - stylis "4.1.3" - -"@emotion/cache@^11.10.5", "@emotion/cache@^11.4.0": - version "11.10.5" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.5.tgz#c142da9351f94e47527ed458f7bbbbe40bb13c12" - integrity sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA== - dependencies: - "@emotion/memoize" "^0.8.0" - "@emotion/sheet" "^1.2.1" - "@emotion/utils" "^1.2.0" - "@emotion/weak-memoize" "^0.3.0" - stylis "4.1.3" - -"@emotion/hash@^0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.0.tgz#c5153d50401ee3c027a57a177bc269b16d889cb7" - integrity sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ== - -"@emotion/memoize@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f" - integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA== - -"@emotion/react@^11.8.1": - version "11.10.5" - resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.5.tgz#95fff612a5de1efa9c0d535384d3cfa115fe175d" - integrity sha512-TZs6235tCJ/7iF6/rvTaOH4oxQg2gMAcdHemjwLKIjKz4rRuYe1HJ2TQJKnAcRAfOUDdU8XoDadCe1rl72iv8A== - dependencies: - "@babel/runtime" "^7.18.3" - "@emotion/babel-plugin" "^11.10.5" - "@emotion/cache" "^11.10.5" - "@emotion/serialize" "^1.1.1" - "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0" - "@emotion/utils" "^1.2.0" - "@emotion/weak-memoize" "^0.3.0" - hoist-non-react-statics "^3.3.1" - -"@emotion/serialize@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.1.tgz#0595701b1902feded8a96d293b26be3f5c1a5cf0" - integrity sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA== - dependencies: - "@emotion/hash" "^0.9.0" - "@emotion/memoize" "^0.8.0" - "@emotion/unitless" "^0.8.0" - "@emotion/utils" "^1.2.0" - csstype "^3.0.2" - -"@emotion/sheet@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.1.tgz#0767e0305230e894897cadb6c8df2c51e61a6c2c" - integrity sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA== - -"@emotion/unitless@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.0.tgz#a4a36e9cbdc6903737cd20d38033241e1b8833db" - integrity sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw== - -"@emotion/use-insertion-effect-with-fallbacks@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz#ffadaec35dbb7885bd54de3fa267ab2f860294df" - integrity sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A== - -"@emotion/utils@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561" - integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw== - -"@emotion/weak-memoize@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb" - integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg== - -"@esbuild/android-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" - integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ== - -"@esbuild/android-arm@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" - integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw== - -"@esbuild/android-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" - integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg== - -"@esbuild/darwin-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" - integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA== - -"@esbuild/darwin-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" - integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== - -"@esbuild/freebsd-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" - integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw== - -"@esbuild/freebsd-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" - integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ== - -"@esbuild/linux-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" - integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA== - -"@esbuild/linux-arm@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" - integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg== - -"@esbuild/linux-ia32@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" - integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA== - -"@esbuild/linux-loong64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" - integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg== - -"@esbuild/linux-mips64el@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" - integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ== - -"@esbuild/linux-ppc64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" - integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA== - -"@esbuild/linux-riscv64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" - integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A== - -"@esbuild/linux-s390x@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" - integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ== - -"@esbuild/linux-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" - integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w== - -"@esbuild/netbsd-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" - integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A== - -"@esbuild/openbsd-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" - integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg== - -"@esbuild/sunos-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" - integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ== - -"@esbuild/win32-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" - integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg== - -"@esbuild/win32-ia32@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" - integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g== - -"@esbuild/win32-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" - integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== - -"@eslint/eslintrc@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" - integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.4.0" - globals "^13.19.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - -"@floating-ui/core@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.2.1.tgz#074182a1d277f94569c50a6b456e62585d463c8e" - integrity sha512-LSqwPZkK3rYfD7GKoIeExXOyYx6Q1O4iqZWwIehDNuv3Dv425FIAE8PRwtAx1imEolFTHgBEcoFHm9MDnYgPCg== - -"@floating-ui/dom@^1.0.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.2.1.tgz#8f93906e1a3b9f606ce78afb058e874344dcbe07" - integrity sha512-Rt45SmRiV8eU+xXSB9t0uMYiQ/ZWGE/jumse2o3i5RGlyvcbqOF4q+1qBnzLE2kZ5JGhq0iMkcGXUKbFe7MpTA== - dependencies: - "@floating-ui/core" "^1.2.1" - -"@formatjs/ecma402-abstract@1.14.3": - version "1.14.3" - resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.3.tgz#6428f243538a11126180d121ce8d4b2f17465738" - integrity sha512-SlsbRC/RX+/zg4AApWIFNDdkLtFbkq3LNoZWXZCE/nHVKqoIJyaoQyge/I0Y38vLxowUn9KTtXgusLD91+orbg== - dependencies: - "@formatjs/intl-localematcher" "0.2.32" - tslib "^2.4.0" - -"@formatjs/ecma402-abstract@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.4.0.tgz#ac6c17a8fffac43c6d68c849a7b732626d32654c" - integrity sha512-Mv027hcLFjE45K8UJ8PjRpdDGfR0aManEFj1KzoN8zXNveHGEygpZGfFf/FTTMl+QEVSrPAUlyxaCApvmv47AQ== - dependencies: - tslib "^2.0.1" - -"@formatjs/ecma402-abstract@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.5.0.tgz#759c8f11ff45e96f8fb58741e7fbdb41096d5ddd" - integrity sha512-wXv36yo+mfWllweN0Fq7sUs7PUiNopn7I0JpLTe3hGu6ZMR4CV7LqK1llhB18pndwpKoafQKb1et2DCJAOW20Q== - dependencies: - tslib "^2.0.1" - -"@formatjs/fast-memoize@1.2.8": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-1.2.8.tgz#425a69f783005f69e11f9e38a7f87f8822d330c6" - integrity sha512-PemNUObyoIZcqdQ1ixTPugzAzhEj7j6AHIyrq/qR6x5BFTvOQeXHYsVZUqBEFduAIscUaDfou+U+xTqOiunJ3Q== - dependencies: - tslib "^2.4.0" - -"@formatjs/icu-messageformat-parser@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.2.0.tgz#9221f7f4dbaf634a84e459a49017a872e708dcfa" - integrity sha512-NT/jKI9nvqNIsosTm+Cxv3BHutB1RIDFa4rAa2b664Od4sBnXtK7afXvAqNa3XDFxljKTij9Cp+kRMJbXozUww== - dependencies: - "@formatjs/ecma402-abstract" "1.14.3" - "@formatjs/icu-skeleton-parser" "1.3.18" - tslib "^2.4.0" - -"@formatjs/icu-skeleton-parser@1.3.18": - version "1.3.18" - resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.18.tgz#7aed3d60e718c8ad6b0e64820be44daa1e29eeeb" - integrity sha512-ND1ZkZfmLPcHjAH1sVpkpQxA+QYfOX3py3SjKWMUVGDow18gZ0WPqz3F+pJLYQMpS2LnnQ5zYR2jPVYTbRwMpg== - dependencies: - "@formatjs/ecma402-abstract" "1.14.3" - tslib "^2.4.0" - -"@formatjs/intl-displaynames@6.2.4": - version "6.2.4" - resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-6.2.4.tgz#e2cc5f5828074f3263e44247491f2698fa4c22dd" - integrity sha512-CmTbSjnmAZHNGuA9vBkWoDHvrcrRauDb0OWc6nk2dAPtesQdadr49Q9N18fr8IV7n3rblgKiYaFVjg68UkRxNg== - dependencies: - "@formatjs/ecma402-abstract" "1.14.3" - "@formatjs/intl-localematcher" "0.2.32" - tslib "^2.4.0" - -"@formatjs/intl-getcanonicallocales@2.0.5", "@formatjs/intl-getcanonicallocales@^2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-2.0.5.tgz#d405cf5221f49531e62ecfde50acdfb62fcc4854" - integrity sha512-YOk+Fa5gpPq5bdpm8JDAY5bkfCkR+NENZKQbLHeqhm8JchHcclPwZ9FU48gYGg3CW6Wi/cTCOvmOrzsIhlkr0w== - dependencies: - tslib "^2.4.0" - -"@formatjs/intl-listformat@7.1.7": - version "7.1.7" - resolved "https://registry.yarnpkg.com/@formatjs/intl-listformat/-/intl-listformat-7.1.7.tgz#b46fec1038ef9ca062d1e7b9b3412c2a14dca18f" - integrity sha512-Zzf5ruPpfJnrAA2hGgf/6pMgQ3tx9oJVhpqycFDavHl3eEzrwdHddGqGdSNwhd0bB4NAFttZNQdmKDldc5iDZw== - dependencies: - "@formatjs/ecma402-abstract" "1.14.3" - "@formatjs/intl-localematcher" "0.2.32" - tslib "^2.4.0" - -"@formatjs/intl-locale@^3.0.11": - version "3.0.11" - resolved "https://registry.yarnpkg.com/@formatjs/intl-locale/-/intl-locale-3.0.11.tgz#6b3bee5692fab3c70a0ce9c642c2a2bec3700aa4" - integrity sha512-gLEX9kzebBjIVCkXMMN+VFMUV2aj0vhmrP+nke2muxUSJ3fLs/DJjlkv+s59rAL3nNaGdvphqKLhQsul0mmhAw== - dependencies: - "@formatjs/ecma402-abstract" "1.14.3" - "@formatjs/intl-getcanonicallocales" "2.0.5" - tslib "^2.4.0" - -"@formatjs/intl-localematcher@0.2.32": - version "0.2.32" - resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz#00d4d307cd7d514b298e15a11a369b86c8933ec1" - integrity sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ== - dependencies: - tslib "^2.4.0" - -"@formatjs/intl-numberformat@^5.5.2": - version "5.7.6" - resolved "https://registry.yarnpkg.com/@formatjs/intl-numberformat/-/intl-numberformat-5.7.6.tgz#630206bb0acefd2d508ccf4f82367c6875cad611" - integrity sha512-ZlZfYtvbVHYZY5OG3RXizoCwxKxEKOrzEe2YOw9wbzoxF3PmFn0SAgojCFGLyNXkkR6xVxlylhbuOPf1dkIVNg== - dependencies: - "@formatjs/ecma402-abstract" "1.4.0" - tslib "^2.0.1" - -"@formatjs/intl-numberformat@^8.3.3": - version "8.3.3" - resolved "https://registry.yarnpkg.com/@formatjs/intl-numberformat/-/intl-numberformat-8.3.3.tgz#bfba1a90a22a28479dae4cf8cc16fb81ff659a36" - integrity sha512-11mSFZb5RsCVZMVaHbRcDYNxQ+tsstReL62AJcTCBZdvAZMqECOEsDkJODZ90nf/ClKqp0/KxwVlshxprn22Nw== - dependencies: - "@formatjs/ecma402-abstract" "1.14.3" - "@formatjs/intl-localematcher" "0.2.32" - tslib "^2.4.0" - -"@formatjs/intl-pluralrules@^5.1.8": - version "5.1.8" - resolved "https://registry.yarnpkg.com/@formatjs/intl-pluralrules/-/intl-pluralrules-5.1.8.tgz#11eeca3cde088fd68d258a09b0791b327a8eb019" - integrity sha512-uevO916EWoeuueqeNzHjnUzpfWZzXFJibC/sEvPR/ZiZH5btWuOLeJLdb1To4nMH8ZJQlmAf8SDpFf+eWvz5lQ== - dependencies: - "@formatjs/ecma402-abstract" "1.14.3" - "@formatjs/intl-localematcher" "0.2.32" - tslib "^2.4.0" - -"@formatjs/intl@2.6.5": - version "2.6.5" - resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-2.6.5.tgz#349dc624a06978b143135201dcf63d40ba3cd816" - integrity sha512-kNH221hsdbTAMdsF6JAxSFV4N/9p5azXZvCLQBxl10Q4D2caPODLtne98gRhinIJ8Hv3djBabPdJG32OQaHuMA== - dependencies: - "@formatjs/ecma402-abstract" "1.14.3" - "@formatjs/fast-memoize" "1.2.8" - "@formatjs/icu-messageformat-parser" "2.2.0" - "@formatjs/intl-displaynames" "6.2.4" - "@formatjs/intl-listformat" "7.1.7" - intl-messageformat "10.3.0" - tslib "^2.4.0" - -"@formatjs/ts-transformer@^2.6.0": - version "2.13.0" - resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-2.13.0.tgz#df47b35cdd209269d282a411f1646e0498aa8fdc" - integrity sha512-mu7sHXZk1NWZrQ3eUqugpSYo8x5/tXkrI4uIbFqCEC0eNgQaIcoKgVeDFgDAcgG+cEme2atAUYSFF+DFWC4org== - dependencies: - intl-messageformat-parser "6.1.2" - tslib "^2.0.1" - typescript "^4.0" - -"@fortawesome/fontawesome-common-types@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz#51f734e64511dbc3674cd347044d02f4dd26e86b" - integrity sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg== - -"@fortawesome/fontawesome-svg-core@^6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.3.0.tgz#b6a17d48d231ac1fad93e43fca7271676bf316cf" - integrity sha512-uz9YifyKlixV6AcKlOX8WNdtF7l6nakGyLYxYaCa823bEBqyj/U2ssqtctO38itNEwXb8/lMzjdoJ+aaJuOdrw== - dependencies: - "@fortawesome/fontawesome-common-types" "6.3.0" - -"@fortawesome/free-brands-svg-icons@^6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.3.0.tgz#436e5fcba4f4f0902edcceaec5c4ff887ba7328f" - integrity sha512-xI0c+a8xnKItAXCN8rZgCNCJQiVAd2Y7p9e2ND6zN3J3ekneu96qrePieJ7yA7073C1JxxoM3vH1RU7rYsaj8w== - dependencies: - "@fortawesome/fontawesome-common-types" "6.3.0" - -"@fortawesome/free-regular-svg-icons@^6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.3.0.tgz#286f87f777e6c96af59151e86647c81083029ee2" - integrity sha512-cZnwiVHZ51SVzWHOaNCIA+u9wevZjCuAGSvSYpNlm6A4H4Vhwh8481Bf/5rwheIC3fFKlgXxLKaw8Xeroz8Ntg== - dependencies: - "@fortawesome/fontawesome-common-types" "6.3.0" - -"@fortawesome/free-solid-svg-icons@^6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.3.0.tgz#d3bd33ae18bb15fdfc3ca136e2fea05f32768a65" - integrity sha512-x5tMwzF2lTH8pyv8yeZRodItP2IVlzzmBuD1M7BjawWgg9XAvktqJJ91Qjgoaf8qJpHQ8FEU9VxRfOkLhh86QA== - dependencies: - "@fortawesome/fontawesome-common-types" "6.3.0" - -"@fortawesome/react-fontawesome@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz#d90dd8a9211830b4e3c08e94b63a0ba7291ddcf4" - integrity sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw== - dependencies: - prop-types "^15.8.1" - -"@graphql-codegen/cli@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@graphql-codegen/cli/-/cli-5.0.0.tgz#761dcf08cfee88bbdd9cdf8097b2343445ec6f0a" - integrity sha512-A7J7+be/a6e+/ul2KI5sfJlpoqeqwX8EzktaKCeduyVKgOLA6W5t+NUGf6QumBDXU8PEOqXk3o3F+RAwCWOiqA== - dependencies: - "@babel/generator" "^7.18.13" - "@babel/template" "^7.18.10" - "@babel/types" "^7.18.13" - "@graphql-codegen/core" "^4.0.0" - "@graphql-codegen/plugin-helpers" "^5.0.1" - "@graphql-tools/apollo-engine-loader" "^8.0.0" - "@graphql-tools/code-file-loader" "^8.0.0" - "@graphql-tools/git-loader" "^8.0.0" - "@graphql-tools/github-loader" "^8.0.0" - "@graphql-tools/graphql-file-loader" "^8.0.0" - "@graphql-tools/json-file-loader" "^8.0.0" - "@graphql-tools/load" "^8.0.0" - "@graphql-tools/prisma-loader" "^8.0.0" - "@graphql-tools/url-loader" "^8.0.0" - "@graphql-tools/utils" "^10.0.0" - "@whatwg-node/fetch" "^0.8.0" - chalk "^4.1.0" - cosmiconfig "^8.1.3" - debounce "^1.2.0" - detect-indent "^6.0.0" - graphql-config "^5.0.2" - inquirer "^8.0.0" - is-glob "^4.0.1" - jiti "^1.17.1" - json-to-pretty-yaml "^1.2.2" - listr2 "^4.0.5" - log-symbols "^4.0.0" - micromatch "^4.0.5" - shell-quote "^1.7.3" - string-env-interpolation "^1.0.1" - ts-log "^2.2.3" - tslib "^2.4.0" - yaml "^2.3.1" - yargs "^17.0.0" - -"@graphql-codegen/core@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@graphql-codegen/core/-/core-4.0.0.tgz#b29c911746a532a675e33720acb4eb2119823e01" - integrity sha512-JAGRn49lEtSsZVxeIlFVIRxts2lWObR+OQo7V2LHDJ7ohYYw3ilv7nJ8pf8P4GTg/w6ptcYdSdVVdkI8kUHB/Q== - dependencies: - "@graphql-codegen/plugin-helpers" "^5.0.0" - "@graphql-tools/schema" "^10.0.0" - "@graphql-tools/utils" "^10.0.0" - tslib "~2.5.0" - -"@graphql-codegen/plugin-helpers@^2.7.2": - version "2.7.2" - resolved "https://registry.yarnpkg.com/@graphql-codegen/plugin-helpers/-/plugin-helpers-2.7.2.tgz#6544f739d725441c826a8af6a49519f588ff9bed" - integrity sha512-kln2AZ12uii6U59OQXdjLk5nOlh1pHis1R98cDZGFnfaiAbX9V3fxcZ1MMJkB7qFUymTALzyjZoXXdyVmPMfRg== - dependencies: - "@graphql-tools/utils" "^8.8.0" - change-case-all "1.0.14" - common-tags "1.8.2" - import-from "4.0.0" - lodash "~4.17.0" - tslib "~2.4.0" - -"@graphql-codegen/plugin-helpers@^3.0.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@graphql-codegen/plugin-helpers/-/plugin-helpers-3.1.2.tgz#69a2e91178f478ea6849846ade0a59a844d34389" - integrity sha512-emOQiHyIliVOIjKVKdsI5MXj312zmRDwmHpyUTZMjfpvxq/UVAHUJIVdVf+lnjjrI+LXBTgMlTWTgHQfmICxjg== - dependencies: - "@graphql-tools/utils" "^9.0.0" - change-case-all "1.0.15" - common-tags "1.8.2" - import-from "4.0.0" - lodash "~4.17.0" - tslib "~2.4.0" - -"@graphql-codegen/plugin-helpers@^5.0.0", "@graphql-codegen/plugin-helpers@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@graphql-codegen/plugin-helpers/-/plugin-helpers-5.0.1.tgz#e2429fcfba3f078d5aa18aa062d46c922bbb0d55" - integrity sha512-6L5sb9D8wptZhnhLLBcheSPU7Tg//DGWgc5tQBWX46KYTOTQHGqDpv50FxAJJOyFVJrveN9otWk9UT9/yfY4ww== - dependencies: - "@graphql-tools/utils" "^10.0.0" - change-case-all "1.0.15" - common-tags "1.8.2" - import-from "4.0.0" - lodash "~4.17.0" - tslib "~2.5.0" - -"@graphql-codegen/schema-ast@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@graphql-codegen/schema-ast/-/schema-ast-4.0.0.tgz#5d60996c87b64f81847da8fcb2d8ef50ede89755" - integrity sha512-WIzkJFa9Gz28FITAPILbt+7A8+yzOyd1NxgwFh7ie+EmO9a5zQK6UQ3U/BviirguXCYnn+AR4dXsoDrSrtRA1g== - dependencies: - "@graphql-codegen/plugin-helpers" "^5.0.0" - "@graphql-tools/utils" "^10.0.0" - tslib "~2.5.0" - -"@graphql-codegen/time@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@graphql-codegen/time/-/time-5.0.0.tgz#832754bcc3dfa62a5e9c612fc3c8a0fe852ea533" - integrity sha512-RI9b3Wm2kci046atAqYfldHtVETi8/mewtfaRJyQn/xBCWwvTEJUL3gr/IxQbEgHRmZEadsWKtIyn7p/OOYQmg== - dependencies: - "@graphql-codegen/plugin-helpers" "^5.0.0" - moment "~2.29.1" - -"@graphql-codegen/typescript-operations@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@graphql-codegen/typescript-operations/-/typescript-operations-4.0.1.tgz#930af3e2d2ae8ff06de696291be28fe7046a2fef" - integrity sha512-GpUWWdBVUec/Zqo23aFLBMrXYxN2irypHqDcKjN78JclDPdreasAEPcIpMfqf4MClvpmvDLy4ql+djVAwmkjbw== - dependencies: - "@graphql-codegen/plugin-helpers" "^5.0.0" - "@graphql-codegen/typescript" "^4.0.1" - "@graphql-codegen/visitor-plugin-common" "4.0.1" - auto-bind "~4.0.0" - tslib "~2.5.0" - -"@graphql-codegen/typescript-react-apollo@^4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@graphql-codegen/typescript-react-apollo/-/typescript-react-apollo-4.1.0.tgz#d900c5d6c259f9b986f917ee280d6bd539f7ecc4" - integrity sha512-G7l4ECoilGnW1zJeqgsFQEVup9ME3w3811ZxHP5yvTra3ZNsbZO4WbYBOPKyS5uc4swsTAxj70a28hNF7kdVcw== - dependencies: - "@graphql-codegen/plugin-helpers" "^3.0.0" - "@graphql-codegen/visitor-plugin-common" "2.13.1" - auto-bind "~4.0.0" - change-case-all "1.0.15" - tslib "~2.6.0" - -"@graphql-codegen/typescript@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@graphql-codegen/typescript/-/typescript-4.0.1.tgz#7481d68f59bea802dd10e278dce73c8a1552b2a4" - integrity sha512-3YziQ21dCVdnHb+Us1uDb3pA6eG5Chjv0uTK+bt9dXeMlwYBU8MbtzvQTo4qvzWVC1AxSOKj0rgfNu1xCXqJyA== - dependencies: - "@graphql-codegen/plugin-helpers" "^5.0.0" - "@graphql-codegen/schema-ast" "^4.0.0" - "@graphql-codegen/visitor-plugin-common" "4.0.1" - auto-bind "~4.0.0" - tslib "~2.5.0" - -"@graphql-codegen/visitor-plugin-common@2.13.1": - version "2.13.1" - resolved "https://registry.yarnpkg.com/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.13.1.tgz#2228660f6692bcdb96b1f6d91a0661624266b76b" - integrity sha512-mD9ufZhDGhyrSaWQGrU1Q1c5f01TeWtSWy/cDwXYjJcHIj1Y/DG2x0tOflEfCvh5WcnmHNIw4lzDsg1W7iFJEg== - dependencies: - "@graphql-codegen/plugin-helpers" "^2.7.2" - "@graphql-tools/optimize" "^1.3.0" - "@graphql-tools/relay-operation-optimizer" "^6.5.0" - "@graphql-tools/utils" "^8.8.0" - auto-bind "~4.0.0" - change-case-all "1.0.14" - dependency-graph "^0.11.0" - graphql-tag "^2.11.0" - parse-filepath "^1.0.2" - tslib "~2.4.0" - -"@graphql-codegen/visitor-plugin-common@4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-4.0.1.tgz#64e293728b3c186f6767141e41fcdb310e50d367" - integrity sha512-Bi/1z0nHg4QMsAqAJhds+ForyLtk7A3HQOlkrZNm3xEkY7lcBzPtiOTLBtvziwopBsXUxqeSwVjOOFPLS5Yw1Q== - dependencies: - "@graphql-codegen/plugin-helpers" "^5.0.0" - "@graphql-tools/optimize" "^2.0.0" - "@graphql-tools/relay-operation-optimizer" "^7.0.0" - "@graphql-tools/utils" "^10.0.0" - auto-bind "~4.0.0" - change-case-all "1.0.15" - dependency-graph "^0.11.0" - graphql-tag "^2.11.0" - parse-filepath "^1.0.2" - tslib "~2.5.0" - -"@graphql-tools/apollo-engine-loader@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-8.0.0.tgz#ac1f351cbe41508411784f25757f5557b0f27489" - integrity sha512-axQTbN5+Yxs1rJ6cWQBOfw3AEeC+fvIuZSfJLPLLvFJLj4pUm9fhxey/g6oQZAAQJqKPfw+tLDUQvnfvRK8Kmg== - dependencies: - "@ardatan/sync-fetch" "^0.0.1" - "@graphql-tools/utils" "^10.0.0" - "@whatwg-node/fetch" "^0.9.0" - tslib "^2.4.0" - -"@graphql-tools/batch-execute@^9.0.1": - version "9.0.2" - resolved "https://registry.yarnpkg.com/@graphql-tools/batch-execute/-/batch-execute-9.0.2.tgz#5ac3257501e7941fad40661bb5e1110d6312f58b" - integrity sha512-Y2uwdZI6ZnatopD/SYfZ1eGuQFI7OU2KGZ2/B/7G9ISmgMl5K+ZZWz/PfIEXeiHirIDhyk54s4uka5rj2xwKqQ== - dependencies: - "@graphql-tools/utils" "^10.0.5" - dataloader "^2.2.2" - tslib "^2.4.0" - value-or-promise "^1.0.12" - -"@graphql-tools/code-file-loader@^8.0.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@graphql-tools/code-file-loader/-/code-file-loader-8.1.0.tgz#1092594f02f2c54fc1dd8b997921ccb8db642272" - integrity sha512-HKWW/B2z15ves8N9+xnVbGmFEVGyHEK80a4ghrjeTa6nwNZaKDVfq5CoYFfF0xpfjtH6gOVUExo2XCOEz4B8mQ== - dependencies: - "@graphql-tools/graphql-tag-pluck" "8.2.0" - "@graphql-tools/utils" "^10.0.13" - globby "^11.0.3" - tslib "^2.4.0" - unixify "^1.0.0" - -"@graphql-tools/delegate@^10.0.0", "@graphql-tools/delegate@^10.0.3": - version "10.0.3" - resolved "https://registry.yarnpkg.com/@graphql-tools/delegate/-/delegate-10.0.3.tgz#2d0e133da94ca92c24e0c7360414e5592321cf2d" - integrity sha512-Jor9oazZ07zuWkykD3OOhT/2XD74Zm6Ar0ENZMk75MDD51wB2UWUIMljtHxbJhV5A6UBC2v8x6iY0xdCGiIlyw== - dependencies: - "@graphql-tools/batch-execute" "^9.0.1" - "@graphql-tools/executor" "^1.0.0" - "@graphql-tools/schema" "^10.0.0" - "@graphql-tools/utils" "^10.0.5" - dataloader "^2.2.2" - tslib "^2.5.0" - -"@graphql-tools/executor-graphql-ws@^1.0.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-1.1.0.tgz#7727159ebaa9df4dc793d0d02e74dd1ca4a7cc60" - integrity sha512-yM67SzwE8rYRpm4z4AuGtABlOp9mXXVy6sxXnTJRoYIdZrmDbKVfIY+CpZUJCqS0FX3xf2+GoHlsj7Qswaxgcg== - dependencies: - "@graphql-tools/utils" "^10.0.2" - "@types/ws" "^8.0.0" - graphql-ws "^5.14.0" - isomorphic-ws "^5.0.0" - tslib "^2.4.0" - ws "^8.13.0" - -"@graphql-tools/executor-http@^1.0.0", "@graphql-tools/executor-http@^1.0.5": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@graphql-tools/executor-http/-/executor-http-1.0.7.tgz#c358f91d4f88e49b9be7408a517f77a3079b2d91" - integrity sha512-/MoRYzQS50Tz5mxRfq3ZmeZ2SOins9wGZAGetsJ55F3PxL0PmHdSGlCq12KzffZDbwHV5YMlwigBsSGWq4y9Iw== - dependencies: - "@graphql-tools/utils" "^10.0.2" - "@repeaterjs/repeater" "^3.0.4" - "@whatwg-node/fetch" "^0.9.0" - extract-files "^11.0.0" - meros "^1.2.1" - tslib "^2.4.0" - value-or-promise "^1.0.12" - -"@graphql-tools/executor-legacy-ws@^1.0.0": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.0.5.tgz#07de9d6e0e49febbcb87d6558bbeebf3940ff25a" - integrity sha512-w54AZ7zkNuvpyV09FH+eGHnnAmaxhBVHg4Yh2ICcsMfRg0brkLt77PlbjBuxZ4HY8XZnKJaYWf+tKazQZtkQtg== - dependencies: - "@graphql-tools/utils" "^10.0.0" - "@types/ws" "^8.0.0" - isomorphic-ws "^5.0.0" - tslib "^2.4.0" - ws "^8.15.0" - -"@graphql-tools/executor@^1.0.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@graphql-tools/executor/-/executor-1.2.0.tgz#6c45f4add765769d9820c4c4405b76957ba39c79" - integrity sha512-SKlIcMA71Dha5JnEWlw4XxcaJ+YupuXg0QCZgl2TOLFz4SkGCwU/geAsJvUJFwK2RbVLpQv/UMq67lOaBuwDtg== - dependencies: - "@graphql-tools/utils" "^10.0.0" - "@graphql-typed-document-node/core" "3.2.0" - "@repeaterjs/repeater" "^3.0.4" - tslib "^2.4.0" - value-or-promise "^1.0.12" - -"@graphql-tools/git-loader@^8.0.0": - version "8.0.4" - resolved "https://registry.yarnpkg.com/@graphql-tools/git-loader/-/git-loader-8.0.4.tgz#663a42e28f1705ba29c0e41ac2f89e7436751608" - integrity sha512-fBmKtnOVqzMT2N8L6nggM4skPq3y2t0eBITZJXCOuxeIlIRAeCOdjNLPKgyGb0rezIyGsn55DKMua5101VN0Sg== - dependencies: - "@graphql-tools/graphql-tag-pluck" "8.2.0" - "@graphql-tools/utils" "^10.0.13" - is-glob "4.0.3" - micromatch "^4.0.4" - tslib "^2.4.0" - unixify "^1.0.0" - -"@graphql-tools/github-loader@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@graphql-tools/github-loader/-/github-loader-8.0.0.tgz#683195800618364701cfea9bc6f88674486f053b" - integrity sha512-VuroArWKcG4yaOWzV0r19ElVIV6iH6UKDQn1MXemND0xu5TzrFme0kf3U9o0YwNo0kUYEk9CyFM0BYg4he17FA== - dependencies: - "@ardatan/sync-fetch" "^0.0.1" - "@graphql-tools/executor-http" "^1.0.0" - "@graphql-tools/graphql-tag-pluck" "^8.0.0" - "@graphql-tools/utils" "^10.0.0" - "@whatwg-node/fetch" "^0.9.0" - tslib "^2.4.0" - value-or-promise "^1.0.12" - -"@graphql-tools/graphql-file-loader@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.0.0.tgz#a2026405bce86d974000455647511bf65df4f211" - integrity sha512-wRXj9Z1IFL3+zJG1HWEY0S4TXal7+s1vVhbZva96MSp0kbb/3JBF7j0cnJ44Eq0ClccMgGCDFqPFXty4JlpaPg== - dependencies: - "@graphql-tools/import" "7.0.0" - "@graphql-tools/utils" "^10.0.0" - globby "^11.0.3" - tslib "^2.4.0" - unixify "^1.0.0" - -"@graphql-tools/graphql-tag-pluck@8.2.0", "@graphql-tools/graphql-tag-pluck@^8.0.0": - version "8.2.0" - resolved "https://registry.yarnpkg.com/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.2.0.tgz#958e07d3bdd94c2a7ac958364b7383c17d009ce2" - integrity sha512-aGIuHxyrJB+LlUfXrH73NVlQTA6LkFbLKQzHojFuwXZJpf7wPkxceN2yp7VjMedARkLJg589IoXgZeMb1EztGQ== - dependencies: - "@babel/core" "^7.22.9" - "@babel/parser" "^7.16.8" - "@babel/plugin-syntax-import-assertions" "^7.20.0" - "@babel/traverse" "^7.16.8" - "@babel/types" "^7.16.8" - "@graphql-tools/utils" "^10.0.13" - tslib "^2.4.0" - -"@graphql-tools/import@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@graphql-tools/import/-/import-7.0.0.tgz#a6a91a90a707d5f46bad0fd3fde2f407b548b2be" - integrity sha512-NVZiTO8o1GZs6OXzNfjB+5CtQtqsZZpQOq+Uu0w57kdUkT4RlQKlwhT8T81arEsbV55KpzkpFsOZP7J1wdmhBw== - dependencies: - "@graphql-tools/utils" "^10.0.0" - resolve-from "5.0.0" - tslib "^2.4.0" - -"@graphql-tools/json-file-loader@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@graphql-tools/json-file-loader/-/json-file-loader-8.0.0.tgz#9b1b62902f766ef3f1c9cd1c192813ea4f48109c" - integrity sha512-ki6EF/mobBWJjAAC84xNrFMhNfnUFD6Y0rQMGXekrUgY0NdeYXHU0ZUgHzC9O5+55FslqUmAUHABePDHTyZsLg== - dependencies: - "@graphql-tools/utils" "^10.0.0" - globby "^11.0.3" - tslib "^2.4.0" - unixify "^1.0.0" - -"@graphql-tools/load@^8.0.0": - version "8.0.1" - resolved "https://registry.yarnpkg.com/@graphql-tools/load/-/load-8.0.1.tgz#498f2230448601cb87894b8a93df7867daef69ea" - integrity sha512-qSMsKngJhDqRbuWyo3NvakEFqFL6+eSjy8ooJ1o5qYD26N7dqXkKzIMycQsX7rBK19hOuINAUSaRcVWH6hTccw== - dependencies: - "@graphql-tools/schema" "^10.0.0" - "@graphql-tools/utils" "^10.0.11" - p-limit "3.1.0" - tslib "^2.4.0" - -"@graphql-tools/merge@^9.0.0", "@graphql-tools/merge@^9.0.1": - version "9.0.1" - resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-9.0.1.tgz#693f15da152339284469b1ce5c6827e3ae350a29" - integrity sha512-hIEExWO9fjA6vzsVjJ3s0cCQ+Q/BEeMVJZtMXd7nbaVefVy0YDyYlEkeoYYNV3NVVvu1G9lr6DM1Qd0DGo9Caw== - dependencies: - "@graphql-tools/utils" "^10.0.10" - tslib "^2.4.0" - -"@graphql-tools/optimize@^1.3.0": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@graphql-tools/optimize/-/optimize-1.3.1.tgz#29407991478dbbedc3e7deb8c44f46acb4e9278b" - integrity sha512-5j5CZSRGWVobt4bgRRg7zhjPiSimk+/zIuColih8E8DxuFOaJ+t0qu7eZS5KXWBkjcd4BPNuhUPpNlEmHPqVRQ== - dependencies: - tslib "^2.4.0" - -"@graphql-tools/optimize@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@graphql-tools/optimize/-/optimize-2.0.0.tgz#7a9779d180824511248a50c5a241eff6e7a2d906" - integrity sha512-nhdT+CRGDZ+bk68ic+Jw1OZ99YCDIKYA5AlVAnBHJvMawSx9YQqQAIj4refNc1/LRieGiuWvhbG3jvPVYho0Dg== - dependencies: - tslib "^2.4.0" - -"@graphql-tools/prisma-loader@^8.0.0": - version "8.0.2" - resolved "https://registry.yarnpkg.com/@graphql-tools/prisma-loader/-/prisma-loader-8.0.2.tgz#3a7126ec2389a7aa7846bd0e441629ac5a1934fc" - integrity sha512-8d28bIB0bZ9Bj0UOz9sHagVPW+6AHeqvGljjERtwCnWl8OCQw2c2pNboYXISLYUG5ub76r4lDciLLTU+Ks7Q0w== - dependencies: - "@graphql-tools/url-loader" "^8.0.0" - "@graphql-tools/utils" "^10.0.8" - "@types/js-yaml" "^4.0.0" - "@types/json-stable-stringify" "^1.0.32" - "@whatwg-node/fetch" "^0.9.0" - chalk "^4.1.0" - debug "^4.3.1" - dotenv "^16.0.0" - graphql-request "^6.0.0" - http-proxy-agent "^7.0.0" - https-proxy-agent "^7.0.0" - jose "^5.0.0" - js-yaml "^4.0.0" - json-stable-stringify "^1.0.1" - lodash "^4.17.20" - scuid "^1.1.0" - tslib "^2.4.0" - yaml-ast-parser "^0.0.43" - -"@graphql-tools/relay-operation-optimizer@^6.5.0": - version "6.5.17" - resolved "https://registry.yarnpkg.com/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-6.5.17.tgz#4e4e2675d696a2a31f106b09ed436c43f7976f37" - integrity sha512-hHPEX6ccRF3+9kfVz0A3In//Dej7QrHOLGZEokBmPDMDqn9CS7qUjpjyGzclbOX0tRBtLfuFUZ68ABSac3P1nA== - dependencies: - "@ardatan/relay-compiler" "12.0.0" - "@graphql-tools/utils" "9.2.1" - tslib "^2.4.0" - -"@graphql-tools/relay-operation-optimizer@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.0.tgz#24367666af87bc5a81748de5e8e9b3c523fd4207" - integrity sha512-UNlJi5y3JylhVWU4MBpL0Hun4Q7IoJwv9xYtmAz+CgRa066szzY7dcuPfxrA7cIGgG/Q6TVsKsYaiF4OHPs1Fw== - dependencies: - "@ardatan/relay-compiler" "12.0.0" - "@graphql-tools/utils" "^10.0.0" - tslib "^2.4.0" - -"@graphql-tools/schema@^10.0.0": - version "10.0.2" - resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-10.0.2.tgz#21bc2ee25a65fb4890d2e5f9f22ef1f733aa81da" - integrity sha512-TbPsIZnWyDCLhgPGnDjt4hosiNU2mF/rNtSk5BVaXWnZqvKJ6gzJV4fcHcvhRIwtscDMW2/YTnK6dLVnk8pc4w== - dependencies: - "@graphql-tools/merge" "^9.0.1" - "@graphql-tools/utils" "^10.0.10" - tslib "^2.4.0" - value-or-promise "^1.0.12" - -"@graphql-tools/url-loader@^8.0.0": - version "8.0.1" - resolved "https://registry.yarnpkg.com/@graphql-tools/url-loader/-/url-loader-8.0.1.tgz#91247247d253c538c4c28376ca74d944fa8cfb82" - integrity sha512-B2k8KQEkEQmfV1zhurT5GLoXo8jbXP+YQHUayhCSxKYlRV7j/1Fhp1b21PDM8LXIDGlDRXaZ0FbWKOs7eYXDuQ== - dependencies: - "@ardatan/sync-fetch" "^0.0.1" - "@graphql-tools/delegate" "^10.0.0" - "@graphql-tools/executor-graphql-ws" "^1.0.0" - "@graphql-tools/executor-http" "^1.0.5" - "@graphql-tools/executor-legacy-ws" "^1.0.0" - "@graphql-tools/utils" "^10.0.0" - "@graphql-tools/wrap" "^10.0.0" - "@types/ws" "^8.0.0" - "@whatwg-node/fetch" "^0.9.0" - isomorphic-ws "^5.0.0" - tslib "^2.4.0" - value-or-promise "^1.0.11" - ws "^8.12.0" - -"@graphql-tools/utils@9.2.1", "@graphql-tools/utils@^9.0.0": - version "9.2.1" - resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-9.2.1.tgz#1b3df0ef166cfa3eae706e3518b17d5922721c57" - integrity sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A== - dependencies: - "@graphql-typed-document-node/core" "^3.1.1" - tslib "^2.4.0" - -"@graphql-tools/utils@^10.0.0", "@graphql-tools/utils@^10.0.10", "@graphql-tools/utils@^10.0.11", "@graphql-tools/utils@^10.0.13", "@graphql-tools/utils@^10.0.2", "@graphql-tools/utils@^10.0.5", "@graphql-tools/utils@^10.0.8": - version "10.0.13" - resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-10.0.13.tgz#d0ab7a4dd02a8405f5ef62dd140b7579ba69f8cb" - integrity sha512-fMILwGr5Dm2zefNItjQ6C2rauigklv69LIwppccICuGTnGaOp3DspLt/6Lxj72cbg5d9z60Sr+Egco3CJKLsNg== - dependencies: - "@graphql-typed-document-node/core" "^3.1.1" - cross-inspect "1.0.0" - dset "^3.1.2" - tslib "^2.4.0" - -"@graphql-tools/utils@^8.8.0": - version "8.13.1" - resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-8.13.1.tgz#b247607e400365c2cd87ff54654d4ad25a7ac491" - integrity sha512-qIh9yYpdUFmctVqovwMdheVNJqFh+DQNWIhX87FJStfXYnmweBUDATok9fWPleKeFwxnW8IapKmY8m8toJEkAw== - dependencies: - tslib "^2.4.0" - -"@graphql-tools/wrap@^10.0.0": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@graphql-tools/wrap/-/wrap-10.0.1.tgz#9e3d27d2723962c26c4377d5d7ab0d3038bf728c" - integrity sha512-Cw6hVrKGM2OKBXeuAGltgy4tzuqQE0Nt7t/uAqnuokSXZhMHXJUb124Bnvxc2gPZn5chfJSDafDe4Cp8ZAVJgg== - dependencies: - "@graphql-tools/delegate" "^10.0.3" - "@graphql-tools/schema" "^10.0.0" - "@graphql-tools/utils" "^10.0.0" - tslib "^2.4.0" - value-or-promise "^1.0.12" - -"@graphql-typed-document-node/core@3.2.0", "@graphql-typed-document-node/core@^3.1.1", "@graphql-typed-document-node/core@^3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" - integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== - -"@humanwhocodes/config-array@^0.11.8": - version "0.11.8" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" - integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== - dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" - minimatch "^3.0.5" - -"@humanwhocodes/module-importer@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" - integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== - -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== - -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== - dependencies: - "@jridgewell/set-array" "^1.0.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== - -"@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== - -"@jridgewell/source-map@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" - integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13": - version "1.4.14" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.17" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" - integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== - dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" - -"@juggle/resize-observer@^3.3.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" - integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== - -"@kamilkisiela/fast-url-parser@^1.1.4": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@kamilkisiela/fast-url-parser/-/fast-url-parser-1.1.4.tgz#9d68877a489107411b953c54ea65d0658b515809" - integrity sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew== - -"@mapbox/hast-util-table-cell-style@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.2.0.tgz#1003f59d54fae6f638cb5646f52110fb3da95b4d" - integrity sha512-gqaTIGC8My3LVSnU38IwjHVKJC94HSonjvFHDk8/aSrApL8v4uWgm8zJkK7MJIIbHuNOr/+Mv2KkQKcxs6LEZA== - dependencies: - unist-util-visit "^1.4.1" - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@peculiar/asn1-schema@^2.1.6", "@peculiar/asn1-schema@^2.3.0": - version "2.3.3" - resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.3.tgz#21418e1f3819e0b353ceff0c2dad8ccb61acd777" - integrity sha512-6GptMYDMyWBHTUKndHaDsRZUO/XMSgIns2krxcm2L7SEExRHwawFvSwNBhqNPR9HJwv3MruAiF1bhN0we6j6GQ== - dependencies: - asn1js "^3.0.5" - pvtsutils "^1.3.2" - tslib "^2.4.0" - -"@peculiar/json-schema@^1.1.12": - version "1.1.12" - resolved "https://registry.yarnpkg.com/@peculiar/json-schema/-/json-schema-1.1.12.tgz#fe61e85259e3b5ba5ad566cb62ca75b3d3cd5339" - integrity sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w== - dependencies: - tslib "^2.0.0" - -"@peculiar/webcrypto@^1.4.0": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.4.1.tgz#821493bd5ad0f05939bd5f53b28536f68158360a" - integrity sha512-eK4C6WTNYxoI7JOabMoZICiyqRRtJB220bh0Mbj5RwRycleZf9BPyZoxsTvpP0FpmVS2aS13NKOuh5/tN3sIRw== - dependencies: - "@peculiar/asn1-schema" "^2.3.0" - "@peculiar/json-schema" "^1.1.12" - pvtsutils "^1.3.2" - tslib "^2.4.1" - webcrypto-core "^1.7.4" - -"@popperjs/core@^2.11.6", "@popperjs/core@^2.9.2": - version "2.11.6" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" - integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== - -"@react-hook/latest@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@react-hook/latest/-/latest-1.0.3.tgz#c2d1d0b0af8b69ec6e2b3a2412ba0768ac82db80" - integrity sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg== - -"@react-hook/passive-layout-effect@^1.2.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz#c06dac2d011f36d61259aa1c6df4f0d5e28bc55e" - integrity sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg== - -"@react-hook/resize-observer@^1.2.6": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@react-hook/resize-observer/-/resize-observer-1.2.6.tgz#9a8cf4c5abb09becd60d1d65f6bf10eec211e291" - integrity sha512-DlBXtLSW0DqYYTW3Ft1/GQFZlTdKY5VAFIC4+km6IK5NiPPDFchGbEJm1j6pSgMqPRHbUQgHJX7RaR76ic1LWA== - dependencies: - "@juggle/resize-observer" "^3.3.1" - "@react-hook/latest" "^1.0.2" - "@react-hook/passive-layout-effect" "^1.2.0" - -"@repeaterjs/repeater@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@repeaterjs/repeater/-/repeater-3.0.4.tgz#a04d63f4d1bf5540a41b01a921c9a7fddc3bd1ca" - integrity sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA== - -"@restart/context@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@restart/context/-/context-2.1.4.tgz#a99d87c299a34c28bd85bb489cb07bfd23149c02" - integrity sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q== - -"@restart/hooks@^0.4.7": - version "0.4.9" - resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.9.tgz#ad858fb39d99e252cccce19416adc18fc3f18fcb" - integrity sha512-3BekqcwB6Umeya+16XPooARn4qEPW6vNvwYnlofIYe6h9qG1/VeD7UvShCWx11eFz5ELYmwIEshz+MkPX3wjcQ== - dependencies: - dequal "^2.0.2" - -"@silvermine/videojs-airplay@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@silvermine/videojs-airplay/-/videojs-airplay-1.2.0.tgz#3ca6464c8e94c97bb9f35c2926f59a5aa662cebd" - integrity sha512-4KzHoM/wJaq3au8IBLxnz8+btAB/M/2AdMoAoOdZRpp9DGG8SGU/UPw2w+CRMknvfbGtvlhlNrNiiXVWA6Cn0A== - -"@silvermine/videojs-chromecast@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@silvermine/videojs-chromecast/-/videojs-chromecast-1.4.1.tgz#a5763eb85dfd4bc8f47a1b8b150d5dadbb8ea658" - integrity sha512-tXeikkWoNC3WIl2WSkIag1CLMbGsgn+26LM4LoB4qx0TQ8mkg7pgpldCTkvXxLVaDluQ/uEm2uxrgrMmjOc6sw== - dependencies: - webcomponents.js "git+https://git@github.com/webcomponents/webcomponentsjs.git#v0.7.24" - -"@tsconfig/node10@^1.0.7": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" - integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" - integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== - -"@tweenjs/tween.js@~18.6.4": - version "18.6.4" - resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-18.6.4.tgz#40a3d0a93647124872dec8e0fd1bd5926695b6ca" - integrity sha512-lB9lMjuqjtuJrx7/kOkqQBtllspPIN+96OvTCeJ2j5FEzinoAXTdAMFnDAQT1KVPRlnYfBrqxtqP66vDM40xxQ== - -"@types/apollo-upload-client@^18.0.0": - version "18.0.0" - resolved "https://registry.yarnpkg.com/@types/apollo-upload-client/-/apollo-upload-client-18.0.0.tgz#1f7e2ff0c0e6508bc1bd19b2340339c7abd117c3" - integrity sha512-cMgITNemktxasqvp6jiPj15dv84n3FTMvMoYBP1+xonDS+0l6JygIJrj2LJh85rShRzTOOkrElrAsCXXARa3KA== - dependencies: - "@apollo/client" "^3.8.0" - "@types/extract-files" "*" - graphql "14 - 16" - -"@types/babel__core@^7.1.7": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" - integrity sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ== - dependencies: - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" - -"@types/babel__generator@*": - version "7.6.4" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" - integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== - dependencies: - "@babel/types" "^7.0.0" - -"@types/babel__template@*": - version "7.4.1" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" - integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - -"@types/babel__traverse@*": - version "7.18.3" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.3.tgz#dfc508a85781e5698d5b33443416b6268c4b3e8d" - integrity sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w== - dependencies: - "@babel/types" "^7.3.0" - -"@types/cookie@^0.3.3": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" - integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== - -"@types/crypto-js@^4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea" - integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ== - -"@types/extract-files@*": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@types/extract-files/-/extract-files-13.0.1.tgz#3ec057a3fa25f778245a76a17271d23b71ee31d7" - integrity sha512-/fRbzc2lAd7jDJSSnxWiUyXWjdUZZ4HbISLJzVgt1AvrdOa7U49YRPcvuCUywkmURZ7uwJOheDjx19itbQ5KvA== - -"@types/fs-extra@^9.0.1": - version "9.0.13" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" - integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA== - dependencies: - "@types/node" "*" - -"@types/history@^4.7.11": - version "4.7.11" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" - integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== - -"@types/hoist-non-react-statics@^3.3.1": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" - integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== - dependencies: - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - -"@types/invariant@^2.2.33": - version "2.2.35" - resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.35.tgz#cd3ebf581a6557452735688d8daba6cf0bd5a3be" - integrity sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg== - -"@types/js-yaml@^4.0.0": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" - integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== - -"@types/json-schema@^7.0.5", "@types/json-schema@^7.0.9": - version "7.0.11" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" - integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== - -"@types/json-stable-stringify@^1.0.32": - version "1.0.34" - resolved "https://registry.yarnpkg.com/@types/json-stable-stringify/-/json-stable-stringify-1.0.34.tgz#c0fb25e4d957e0ee2e497c1f553d7f8bb668fd75" - integrity sha512-s2cfwagOQAS8o06TcwKfr9Wx11dNGbH2E9vJz1cqV+a/LOyhWNLUNd6JSRYNzvB4d29UuJX2M0Dj9vE1T8fRXw== - -"@types/json5@^0.0.29": - version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" - integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== - -"@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.191" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" - integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== - -"@types/mdast@^3.0.0": - version "3.0.10" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" - integrity sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA== - dependencies: - "@types/unist" "*" - -"@types/minimist@^1.2.0", "@types/minimist@^1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" - integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== - -"@types/mousetrap@^1.6.11": - version "1.6.11" - resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.11.tgz#ef9620160fdcefcb85bccda8aaa3e84d7429376d" - integrity sha512-F0oAily9Q9QQpv9JKxKn0zMKfOo36KHCW7myYsmUyf2t0g+sBTbG3UleTPoguHdE1z3GLFr3p7/wiOio52QFjQ== - -"@types/node@*", "@types/node@^18.13.0": - version "18.13.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850" - integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg== - -"@types/normalize-package-data@^2.4.0": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" - integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== - -"@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== - -"@types/prop-types@*", "@types/prop-types@^15.7.3": - version "15.7.5" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" - integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== - -"@types/react-datepicker@^4.10.0": - version "4.10.0" - resolved "https://registry.yarnpkg.com/@types/react-datepicker/-/react-datepicker-4.10.0.tgz#fcb0e6a7787491bf2f37fbda2b537062608a0056" - integrity sha512-Cq+ks20vBIU6XN67TbkCHu8M7V46Y6vJrKE2n+8q/GfueJyWWTIKeC3Z7cz/d+qxGDq/VCrqA929R0U4lNuztg== - dependencies: - "@popperjs/core" "^2.9.2" - "@types/react" "*" - date-fns "^2.0.1" - react-popper "^2.2.5" - -"@types/react-dom@^17.0.19": - version "17.0.19" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.19.tgz#36feef3aa35d045cacd5ed60fe0eef5272f19492" - integrity sha512-PiYG40pnQRdPHnlf7tZnp0aQ6q9tspYr72vD61saO6zFCybLfMqwUCN0va1/P+86DXn18ZWeW30Bk7xlC5eEAQ== - dependencies: - "@types/react" "^17" - -"@types/react-helmet@^6.1.6": - version "6.1.6" - resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.6.tgz#7d1afd8cbf099616894e8240e9ef70e3c6d7506d" - integrity sha512-ZKcoOdW/Tg+kiUbkFCBtvDw0k3nD4HJ/h/B9yWxN4uDO8OkRksWTO+EL+z/Qu3aHTeTll3Ro0Cc/8UhwBCMG5A== - dependencies: - "@types/react" "*" - -"@types/react-router-bootstrap@^0.24.5": - version "0.24.5" - resolved "https://registry.yarnpkg.com/@types/react-router-bootstrap/-/react-router-bootstrap-0.24.5.tgz#9257ba3dfb01cda201aac9fa05cde3eb09ea5b27" - integrity sha512-GRx/8xF/skw4/Pmm6d+xbExi8gobCLOe8Eoz9kXPQGbYo7p5Wbi61tjpOF5AbfJ5XMN+fIzweToTi56odj/LOQ== - dependencies: - "@types/react" "*" - "@types/react-router-dom" "*" - -"@types/react-router-dom@*": - version "5.3.3" - resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" - integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router" "*" - -"@types/react-router-hash-link@^2.4.5": - version "2.4.5" - resolved "https://registry.yarnpkg.com/@types/react-router-hash-link/-/react-router-hash-link-2.4.5.tgz#41dcb55279351fedc9062115bb35db921d1d69f6" - integrity sha512-YsiD8xCWtRBebzPqG6kXjDQCI35LCN9MhV/MbgYF8y0trOp7VSUNmSj8HdIGyH99WCfSOLZB2pIwUMN/IwIDQg== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router-dom" "*" - -"@types/react-router@*": - version "5.1.20" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" - integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - -"@types/react-transition-group@^4.4.0", "@types/react-transition-group@^4.4.1": - version "4.4.5" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416" - integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA== - dependencies: - "@types/react" "*" - -"@types/react@*", "@types/react@16 || 17 || 18", "@types/react@>=16.14.8", "@types/react@>=16.9.11", "@types/react@^17", "@types/react@^17.0.53": - version "17.0.53" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.53.tgz#10d4d5999b8af3d6bc6a9369d7eb953da82442ab" - integrity sha512-1yIpQR2zdYu1Z/dc1OxC+MA6GR240u3gcnP4l6mvj/PJiVaqHsQPmWttsvHsfnhfPbU2FuGmo0wSITPygjBmsw== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/scheduler@*": - version "0.16.2" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" - integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== - -"@types/schema-utils@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@types/schema-utils/-/schema-utils-2.4.0.tgz#9983012045d541dcee053e685a27c9c87c840fcd" - integrity sha512-454hrj5gz/FXcUE20ygfEiN4DxZ1sprUo0V1gqIqkNZ/CzoEzAZEll2uxMsuyz6BYjiQan4Aa65xbTemfzW9hQ== - dependencies: - schema-utils "*" - -"@types/semver@^7.3.12": - version "7.3.13" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" - integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== - -"@types/stats.js@*": - version "0.17.0" - resolved "https://registry.yarnpkg.com/@types/stats.js/-/stats.js-0.17.0.tgz#0ed81d48e03b590c24da85540c1d952077a9fe20" - integrity sha512-9w+a7bR8PeB0dCT/HBULU2fMqf6BAzvKbxFboYhmDtDkKPiyXYbjoe2auwsXlEFI7CFNMF1dCv3dFH5Poy9R1w== - -"@types/three@^0.154.0": - version "0.154.0" - resolved "https://registry.yarnpkg.com/@types/three/-/three-0.154.0.tgz#91f4384930ed050a14d7f13c09d5785cc167a064" - integrity sha512-IioqpGhch6FdLDh4zazRn3rXHj6Vn2nVOziJdXVbJFi9CaI65LtP9qqUtpzbsHK2Ezlox8NtsLNHSw3AQzucjA== - dependencies: - "@tweenjs/tween.js" "~18.6.4" - "@types/stats.js" "*" - "@types/webxr" "*" - fflate "~0.6.9" - lil-gui "~0.17.0" - meshoptimizer "~0.18.1" - -"@types/ua-parser-js@^0.7.36": - version "0.7.36" - resolved "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz" - integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ== - -"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" - integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== - -"@types/video.js@*", "@types/video.js@^7.3.51": - version "7.3.51" - resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.51.tgz#ce69e02681ed6ed8abe61bb3802dd032a74d63e8" - integrity sha512-xLlt/ZfCuWYBvG2MRn018RvaEplcK6dI63aOiVUeeAWFyjx3Br1hL749ndFgbrvNdY4m9FoHG1FQ/PB6IpfSAQ== - -"@types/videojs-mobile-ui@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@types/videojs-mobile-ui/-/videojs-mobile-ui-0.8.0.tgz#0fb82810155f3dee9620ea411c8b9bd17e1ac645" - integrity sha512-Q8p7ezQLZzf8pnvYd8GZ/6tcg2oX0269Q94dDoqNnq2QMmqWp1sj8npU3gGnTaLkYvvdrO8UjBOIzX68RkQLew== - dependencies: - "@types/video.js" "*" - -"@types/videojs-seek-buttons@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/videojs-seek-buttons/-/videojs-seek-buttons-2.1.0.tgz#2c59007ad6f4d6f86df810b6e84daeb8c2e62fc4" - integrity sha512-vaTCELmPea/cgkf82P8RaeFBBZnd4nnuOiMaciSeMKiWzbflSvcvjnHfvKcBmJEi3hnSQ12bVXlmUvvOrgmpjQ== - dependencies: - "@types/video.js" "*" - -"@types/warning@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52" - integrity sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA== - -"@types/webxr@*": - version "0.5.2" - resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.2.tgz#5d9627b0ffe223aa3b166de7112ac8a9460dc54f" - integrity sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw== - -"@types/ws@^8.0.0": - version "8.5.4" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5" - integrity sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg== - dependencies: - "@types/node" "*" - -"@typescript-eslint/eslint-plugin@^5.52.0": - version "5.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.52.0.tgz#5fb0d43574c2411f16ea80f5fc335b8eaa7b28a8" - integrity sha512-lHazYdvYVsBokwCdKOppvYJKaJ4S41CgKBcPvyd0xjZNbvQdhn/pnJlGtQksQ/NhInzdaeaSarlBjDXHuclEbg== - dependencies: - "@typescript-eslint/scope-manager" "5.52.0" - "@typescript-eslint/type-utils" "5.52.0" - "@typescript-eslint/utils" "5.52.0" - debug "^4.3.4" - grapheme-splitter "^1.0.4" - ignore "^5.2.0" - natural-compare-lite "^1.4.0" - regexpp "^3.2.0" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/parser@^5.52.0": - version "5.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.52.0.tgz#73c136df6c0133f1d7870de7131ccf356f5be5a4" - integrity sha512-e2KiLQOZRo4Y0D/b+3y08i3jsekoSkOYStROYmPUnGMEoA0h+k2qOH5H6tcjIc68WDvGwH+PaOrP1XRzLJ6QlA== - dependencies: - "@typescript-eslint/scope-manager" "5.52.0" - "@typescript-eslint/types" "5.52.0" - "@typescript-eslint/typescript-estree" "5.52.0" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@5.52.0": - version "5.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.52.0.tgz#a993d89a0556ea16811db48eabd7c5b72dcb83d1" - integrity sha512-AR7sxxfBKiNV0FWBSARxM8DmNxrwgnYMPwmpkC1Pl1n+eT8/I2NAUPuwDy/FmDcC6F8pBfmOcaxcxRHspgOBMw== - dependencies: - "@typescript-eslint/types" "5.52.0" - "@typescript-eslint/visitor-keys" "5.52.0" - -"@typescript-eslint/type-utils@5.52.0": - version "5.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.52.0.tgz#9fd28cd02e6f21f5109e35496df41893f33167aa" - integrity sha512-tEKuUHfDOv852QGlpPtB3lHOoig5pyFQN/cUiZtpw99D93nEBjexRLre5sQZlkMoHry/lZr8qDAt2oAHLKA6Jw== - dependencies: - "@typescript-eslint/typescript-estree" "5.52.0" - "@typescript-eslint/utils" "5.52.0" - debug "^4.3.4" - tsutils "^3.21.0" - -"@typescript-eslint/types@5.52.0": - version "5.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.52.0.tgz#19e9abc6afb5bd37a1a9bea877a1a836c0b3241b" - integrity sha512-oV7XU4CHYfBhk78fS7tkum+/Dpgsfi91IIDy7fjCyq2k6KB63M6gMC0YIvy+iABzmXThCRI6xpCEyVObBdWSDQ== - -"@typescript-eslint/typescript-estree@5.52.0": - version "5.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.52.0.tgz#6408cb3c2ccc01c03c278cb201cf07e73347dfca" - integrity sha512-WeWnjanyEwt6+fVrSR0MYgEpUAuROxuAH516WPjUblIrClzYJj0kBbjdnbQXLpgAN8qbEuGywiQsXUVDiAoEuQ== - dependencies: - "@typescript-eslint/types" "5.52.0" - "@typescript-eslint/visitor-keys" "5.52.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/utils@5.52.0": - version "5.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.52.0.tgz#b260bb5a8f6b00a0ed51db66bdba4ed5e4845a72" - integrity sha512-As3lChhrbwWQLNk2HC8Ree96hldKIqk98EYvypd3It8Q1f8d5zWyIoaZEp2va5667M4ZyE7X8UUR+azXrFl+NA== - dependencies: - "@types/json-schema" "^7.0.9" - "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.52.0" - "@typescript-eslint/types" "5.52.0" - "@typescript-eslint/typescript-estree" "5.52.0" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" - semver "^7.3.7" - -"@typescript-eslint/visitor-keys@5.52.0": - version "5.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.52.0.tgz#e38c971259f44f80cfe49d97dbffa38e3e75030f" - integrity sha512-qMwpw6SU5VHCPr99y274xhbm+PRViK/NATY6qzt+Et7+mThGuFSl/ompj2/hrBlRP/kq+BFdgagnOSgw9TB0eA== - dependencies: - "@typescript-eslint/types" "5.52.0" - eslint-visitor-keys "^3.3.0" - -"@videojs/http-streaming@2.16.2": - version "2.16.2" - resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-2.16.2.tgz#a9be925b4e368a41dbd67d49c4f566715169b84b" - integrity sha512-etPTUdCFu7gUWc+1XcbiPr+lrhOcBu3rV5OL1M+3PDW89zskScAkkcdqYzP4pFodBPye/ydamQoTDScOnElw5A== - dependencies: - "@babel/runtime" "^7.12.5" - "@videojs/vhs-utils" "3.0.5" - aes-decrypter "3.1.3" - global "^4.4.0" - m3u8-parser "4.8.0" - mpd-parser "^0.22.1" - mux.js "6.0.1" - video.js "^6 || ^7" - -"@videojs/vhs-utils@3.0.5", "@videojs/vhs-utils@^3.0.4", "@videojs/vhs-utils@^3.0.5": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz#665ba70d78258ba1ab977364e2fe9f4d4799c46c" - integrity sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw== - dependencies: - "@babel/runtime" "^7.12.5" - global "^4.4.0" - url-toolkit "^2.2.1" - -"@videojs/xhr@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@videojs/xhr/-/xhr-2.6.0.tgz#cd897e0ad54faf497961bcce3fa16dc15a26bb80" - integrity sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q== - dependencies: - "@babel/runtime" "^7.5.5" - global "~4.4.0" - is-function "^1.0.1" - -"@vitejs/plugin-legacy@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-legacy/-/plugin-legacy-4.0.1.tgz#122e334ac0b8dba2cbd44cc15209e67c9a014463" - integrity sha512-/ZV63NagI1c9TB5E4ijGmycY//fNm/2L02nsnXXxACwYaF9W+/OyVlgIW24jYUIS+g0yQRtn+N5hzBc8RLNhGA== - dependencies: - "@babel/core" "^7.20.12" - "@babel/preset-env" "^7.20.2" - browserslist "^4.21.4" - core-js "^3.27.2" - magic-string "^0.27.0" - regenerator-runtime "^0.13.11" - systemjs "^6.13.0" - -"@vitejs/plugin-react@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz#d1091f535eab8b83d6e74034d01e27d73c773240" - integrity sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g== - dependencies: - "@babel/core" "^7.20.12" - "@babel/plugin-transform-react-jsx-self" "^7.18.6" - "@babel/plugin-transform-react-jsx-source" "^7.19.6" - magic-string "^0.27.0" - react-refresh "^0.14.0" - -"@whatwg-node/events@^0.0.2": - version "0.0.2" - resolved "https://registry.yarnpkg.com/@whatwg-node/events/-/events-0.0.2.tgz#7b7107268d2982fc7b7aff5ee6803c64018f84dd" - integrity sha512-WKj/lI4QjnLuPrim0cfO7i+HsDSXHxNv1y0CrJhdntuO3hxWZmnXCwNDnwOvry11OjRin6cgWNF+j/9Pn8TN4w== - -"@whatwg-node/events@^0.1.0": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@whatwg-node/events/-/events-0.1.1.tgz#0ca718508249419587e130da26d40e29d99b5356" - integrity sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w== - -"@whatwg-node/fetch@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@whatwg-node/fetch/-/fetch-0.8.1.tgz#ee3c94746132f217e17f78f9e073bb342043d630" - integrity sha512-Fkd1qQHK2tAWxKlC85h9L86Lgbq3BzxMnHSnTsnzNZMMzn6Xi+HlN8/LJ90LxorhSqD54td+Q864LgwUaYDj1Q== - dependencies: - "@peculiar/webcrypto" "^1.4.0" - "@whatwg-node/node-fetch" "^0.3.0" - busboy "^1.6.0" - urlpattern-polyfill "^6.0.2" - web-streams-polyfill "^3.2.1" - -"@whatwg-node/fetch@^0.9.0": - version "0.9.16" - resolved "https://registry.yarnpkg.com/@whatwg-node/fetch/-/fetch-0.9.16.tgz#c833eb714f41f5d2caf1a345bed7a05f56db7b16" - integrity sha512-mqasZiUNquRe3ea9+aCAuo81BR6vq5opUKprPilIHTnrg8a21Z1T1OrI+KiMFX8OmwO5HUJe/vro47lpj2JPWQ== - dependencies: - "@whatwg-node/node-fetch" "^0.5.5" - urlpattern-polyfill "^10.0.0" - -"@whatwg-node/node-fetch@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@whatwg-node/node-fetch/-/node-fetch-0.3.0.tgz#7c7e90d03fa09d0ddebff29add6f16d923327d58" - integrity sha512-mPM8WnuHiI/3kFxDeE0SQQXAElbz4onqmm64fEGCwYEcBes2UsvIDI8HwQIqaXCH42A9ajJUPv4WsYoN/9oG6w== - dependencies: - "@whatwg-node/events" "^0.0.2" - busboy "^1.6.0" - fast-querystring "^1.1.1" - fast-url-parser "^1.1.3" - tslib "^2.3.1" - -"@whatwg-node/node-fetch@^0.5.5": - version "0.5.5" - resolved "https://registry.yarnpkg.com/@whatwg-node/node-fetch/-/node-fetch-0.5.5.tgz#40c45e5f5f4185fa3391ff75f619287e0f225c7f" - integrity sha512-LhE0Oo95+dOrrzrJncrpCaR3VHSjJ5Gvkl5g9WVfkPKSKkxCbMeOsRQ+v9LrU9lRvXBJn8JicXqSufKFEpyRbQ== - dependencies: - "@kamilkisiela/fast-url-parser" "^1.1.4" - "@whatwg-node/events" "^0.1.0" - busboy "^1.6.0" - fast-querystring "^1.1.1" - tslib "^2.3.1" - -"@wry/caches@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@wry/caches/-/caches-1.0.1.tgz#8641fd3b6e09230b86ce8b93558d44cf1ece7e52" - integrity sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA== - dependencies: - tslib "^2.3.0" - -"@wry/context@^0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.7.0.tgz#be88e22c0ddf62aeb0ae9f95c3d90932c619a5c8" - integrity sha512-LcDAiYWRtwAoSOArfk7cuYvFXytxfVrdX7yxoUmK7pPITLk5jYh2F8knCwS7LjgYL8u1eidPlKKV6Ikqq0ODqQ== - dependencies: - tslib "^2.3.0" - -"@wry/equality@^0.5.6": - version "0.5.7" - resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.5.7.tgz#72ec1a73760943d439d56b7b1e9985aec5d497bb" - integrity sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw== - dependencies: - tslib "^2.3.0" - -"@wry/trie@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.4.3.tgz#077d52c22365871bf3ffcbab8e95cb8bc5689af4" - integrity sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w== - dependencies: - tslib "^2.3.0" - -"@wry/trie@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.5.0.tgz#11e783f3a53f6e4cd1d42d2d1323f5bc3fa99c94" - integrity sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA== - dependencies: - tslib "^2.3.0" - -"@xmldom/xmldom@^0.8.3": - version "0.8.6" - resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.6.tgz#8a1524eb5bd5e965c1e3735476f0262469f71440" - integrity sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg== - -acorn-jsx@^5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn-walk@^8.1.1: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== - -acorn@^8.4.1, acorn@^8.5.0, acorn@^8.8.0: - version "8.8.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== - -aes-decrypter@3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/aes-decrypter/-/aes-decrypter-3.1.3.tgz#65ff5f2175324d80c41083b0e135d1464b12ac35" - integrity sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A== - dependencies: - "@babel/runtime" "^7.12.5" - "@videojs/vhs-utils" "^3.0.5" - global "^4.4.0" - pkcs7 "^1.0.4" - -agent-base@^7.0.2, agent-base@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.0.tgz#536802b76bc0b34aa50195eb2442276d613e3434" - integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== - dependencies: - debug "^4.3.4" - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv-formats@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" - integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== - dependencies: - ajv "^8.0.0" - -ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv-keywords@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" - integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== - dependencies: - fast-deep-equal "^3.1.3" - -ajv@^6.10.0, ajv@^6.12.4: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^8.0.0, ajv@^8.0.1, ajv@^8.8.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - -ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -apollo-upload-client@^18.0.1: - version "18.0.1" - resolved "https://registry.yarnpkg.com/apollo-upload-client/-/apollo-upload-client-18.0.1.tgz#e3811f2f5a36bffef23954f796daf331be748dcb" - integrity sha512-OQvZg1rK05VNI79D658FUmMdoI2oB/KJKb6QGMa2Si25QXOaAvLMBFUEwJct7wf+19U8vk9ILhidBOU1ZWv6QA== - dependencies: - extract-files "^13.0.0" - -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -aria-query@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" - integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== - dependencies: - deep-equal "^2.0.5" - -array-includes@^3.1.5, array-includes@^3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" - integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - get-intrinsic "^1.1.3" - is-string "^1.0.7" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -array.prototype.flat@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" - integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - es-shim-unscopables "^1.0.0" - -array.prototype.flatmap@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" - integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - es-shim-unscopables "^1.0.0" - -array.prototype.tosorted@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz#ccf44738aa2b5ac56578ffda97c03fd3e23dd532" - integrity sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - es-shim-unscopables "^1.0.0" - get-intrinsic "^1.1.3" - -arrify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== - -asap@~2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== - -asn1js@^3.0.1, asn1js@^3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.5.tgz#5ea36820443dbefb51cc7f88a2ebb5b462114f38" - integrity sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ== - dependencies: - pvtsutils "^1.3.2" - pvutils "^1.1.3" - tslib "^2.4.0" - -ast-types-flow@^0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" - integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== - -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -auto-bind@~4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-4.0.0.tgz#e3589fc6c2da8f7ca43ba9f84fa52a744fc997fb" - integrity sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ== - -available-typed-arrays@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" - integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== - -axe-core@^4.6.2: - version "4.6.3" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece" - integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg== - -axobject-query@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" - integrity sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg== - dependencies: - deep-equal "^2.0.5" - -b64-to-blob@^1.2.19: - version "1.2.19" - resolved "https://registry.yarnpkg.com/b64-to-blob/-/b64-to-blob-1.2.19.tgz#157d85fdc8811665b9a35d29ffbc6a522ba28fbe" - integrity sha512-L3nSu8GgF4iEyNYakCQSfL2F5GI5aCXcot9mNTf+4N0/BMhpxqqHyOb6jIR24iq2xLjQZLG8FOt3gnUcV+9NVg== - -babel-plugin-macros@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" - integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== - dependencies: - "@babel/runtime" "^7.12.5" - cosmiconfig "^7.0.0" - resolve "^1.19.0" - -babel-plugin-polyfill-corejs2@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz#5d1bd3836d0a19e1b84bbf2d9640ccb6f951c122" - integrity sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q== - dependencies: - "@babel/compat-data" "^7.17.7" - "@babel/helper-define-polyfill-provider" "^0.3.3" - semver "^6.1.1" - -babel-plugin-polyfill-corejs3@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz#56ad88237137eade485a71b52f72dbed57c6230a" - integrity sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.3" - core-js-compat "^3.25.1" - -babel-plugin-polyfill-regenerator@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz#390f91c38d90473592ed43351e801a9d3e0fd747" - integrity sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.3" - -babel-plugin-react-intl@^7.0.0: - version "7.9.4" - resolved "https://registry.yarnpkg.com/babel-plugin-react-intl/-/babel-plugin-react-intl-7.9.4.tgz#1fc9ab50470d41b934df50d8f436578ee1732cb0" - integrity sha512-cMKrHEXrw43yT4M89Wbgq8A8N8lffSquj1Piwov/HVukR7jwOw8gf9btXNsQhT27ccyqEwy+M286JQYy0jby2g== - dependencies: - "@babel/core" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/types" "^7.9.5" - "@formatjs/ts-transformer" "^2.6.0" - "@types/babel__core" "^7.1.7" - "@types/fs-extra" "^9.0.1" - "@types/schema-utils" "^2.4.0" - fs-extra "^9.0.0" - intl-messageformat-parser "^5.3.7" - schema-utils "^2.6.6" - -babel-plugin-syntax-trailing-function-commas@^7.0.0-beta.0: - version "7.0.0-beta.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz#aa213c1435e2bffeb6fca842287ef534ad05d5cf" - integrity sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ== - -babel-preset-fbjs@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/babel-preset-fbjs/-/babel-preset-fbjs-3.4.0.tgz#38a14e5a7a3b285a3f3a86552d650dca5cf6111c" - integrity sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow== - dependencies: - "@babel/plugin-proposal-class-properties" "^7.0.0" - "@babel/plugin-proposal-object-rest-spread" "^7.0.0" - "@babel/plugin-syntax-class-properties" "^7.0.0" - "@babel/plugin-syntax-flow" "^7.0.0" - "@babel/plugin-syntax-jsx" "^7.0.0" - "@babel/plugin-syntax-object-rest-spread" "^7.0.0" - "@babel/plugin-transform-arrow-functions" "^7.0.0" - "@babel/plugin-transform-block-scoped-functions" "^7.0.0" - "@babel/plugin-transform-block-scoping" "^7.0.0" - "@babel/plugin-transform-classes" "^7.0.0" - "@babel/plugin-transform-computed-properties" "^7.0.0" - "@babel/plugin-transform-destructuring" "^7.0.0" - "@babel/plugin-transform-flow-strip-types" "^7.0.0" - "@babel/plugin-transform-for-of" "^7.0.0" - "@babel/plugin-transform-function-name" "^7.0.0" - "@babel/plugin-transform-literals" "^7.0.0" - "@babel/plugin-transform-member-expression-literals" "^7.0.0" - "@babel/plugin-transform-modules-commonjs" "^7.0.0" - "@babel/plugin-transform-object-super" "^7.0.0" - "@babel/plugin-transform-parameters" "^7.0.0" - "@babel/plugin-transform-property-literals" "^7.0.0" - "@babel/plugin-transform-react-display-name" "^7.0.0" - "@babel/plugin-transform-react-jsx" "^7.0.0" - "@babel/plugin-transform-shorthand-properties" "^7.0.0" - "@babel/plugin-transform-spread" "^7.0.0" - "@babel/plugin-transform-template-literals" "^7.0.0" - babel-plugin-syntax-trailing-function-commas "^7.0.0-beta.0" - -bail@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" - integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -balanced-match@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" - integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== - -base64-blob@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/base64-blob/-/base64-blob-1.4.1.tgz#f8dfc16c22b24ee499e2782719bcce800132c18a" - integrity sha512-n5Ov4cPTbLBTX1PiFbaB5AmK7LMigO9HWh5Lzx+Kcx/yx1MppeeLYtAH8aLv1m++WNoHQnr+xbGSqcZinopwlw== - dependencies: - b64-to-blob "^1.2.19" - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -bcp-47-match@^1.0.0, bcp-47-match@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/bcp-47-match/-/bcp-47-match-1.0.3.tgz#cb8d03071389a10aff2062b862d6575ffd7cd7ef" - integrity sha512-LggQ4YTdjWQSKELZF5JwchnBa1u0pIQSZf5lSdOHEdbVP55h0qICA/FUp3+W99q0xqxYa1ZQizTUH87gecII5w== - -bcp-47-normalize@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/bcp-47-normalize/-/bcp-47-normalize-1.1.1.tgz#d2c76218d132f223c44e4a06a7224be3030f8ec3" - integrity sha512-jWZ1Jdu3cs0EZdfCkS0UE9Gg01PtxnChjEBySeB+Zo6nkqtFfnvtoQQgP1qU1Oo4qgJgxhTI6Sf9y/pZIhPs0A== - dependencies: - bcp-47 "^1.0.0" - bcp-47-match "^1.0.0" - -bcp-47@^1.0.0: - version "1.0.8" - resolved "https://registry.yarnpkg.com/bcp-47/-/bcp-47-1.0.8.tgz#bf63ae4269faabe7c100deac0811121a48b6a561" - integrity sha512-Y9y1QNBBtYtv7hcmoX0tR+tUNSFZGZ6OL6vKPObq8BbOhkCoyayF6ogfLTgAli/KuAEbsYHYUNq2AQuY6IuLag== - dependencies: - is-alphabetical "^1.0.0" - is-alphanumerical "^1.0.0" - is-decimal "^1.0.0" - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -bl@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -bootstrap@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.2.tgz#8e0cd61611728a5bf65a3a2b8d6ff6c77d5d7479" - integrity sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^3.0.2, braces@^3.0.3, braces@~3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -browserslist@^4.21.4, browserslist@^4.21.5, browserslist@^4.22.2: - version "4.22.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.3.tgz#299d11b7e947a6b843981392721169e27d60c5a6" - integrity sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A== - dependencies: - caniuse-lite "^1.0.30001580" - electron-to-chromium "^1.4.648" - node-releases "^2.0.14" - update-browserslist-db "^1.0.13" - -bser@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" - integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== - dependencies: - node-int64 "^0.4.0" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -busboy@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" - integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== - dependencies: - streamsearch "^1.1.0" - -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camel-case@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" - integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== - dependencies: - pascal-case "^3.1.2" - tslib "^2.0.3" - -camelcase-keys@^6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" - integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== - dependencies: - camelcase "^5.3.1" - map-obj "^4.0.0" - quick-lru "^4.0.1" - -camelcase-keys@^7.0.0: - version "7.0.2" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-7.0.2.tgz#d048d8c69448745bb0de6fc4c1c52a30dfbe7252" - integrity sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg== - dependencies: - camelcase "^6.3.0" - map-obj "^4.1.0" - quick-lru "^5.1.1" - type-fest "^1.2.1" - -camelcase@^5.0.0, camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -camelcase@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - -caniuse-lite@^1.0.30001580: - version "1.0.30001580" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001580.tgz#e3c76bc6fe020d9007647044278954ff8cd17d1e" - integrity sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA== - -capital-case@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" - integrity sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - upper-case-first "^2.0.2" - -cardboard-vr-display@^1.0.19: - version "1.0.19" - resolved "https://registry.yarnpkg.com/cardboard-vr-display/-/cardboard-vr-display-1.0.19.tgz#81dcde1804b329b8228b757ac00e1fd2afa9d748" - integrity sha512-+MjcnWKAkb95p68elqZLDPzoiF/dGncQilLGvPBM5ZorABp/ao3lCs7nnRcYBckmuNkg1V/5rdGDKoUaCVsHzQ== - dependencies: - gl-preserve-state "^1.0.0" - nosleep.js "^0.7.0" - webvr-polyfill-dpdb "^1.0.17" - -ccount@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" - integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== - -chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -change-case-all@1.0.14: - version "1.0.14" - resolved "https://registry.yarnpkg.com/change-case-all/-/change-case-all-1.0.14.tgz#bac04da08ad143278d0ac3dda7eccd39280bfba1" - integrity sha512-CWVm2uT7dmSHdO/z1CXT/n47mWonyypzBbuCy5tN7uMg22BsfkhwT6oHmFCAk+gL1LOOxhdbB9SZz3J1KTY3gA== - dependencies: - change-case "^4.1.2" - is-lower-case "^2.0.2" - is-upper-case "^2.0.2" - lower-case "^2.0.2" - lower-case-first "^2.0.2" - sponge-case "^1.0.1" - swap-case "^2.0.2" - title-case "^3.0.3" - upper-case "^2.0.2" - upper-case-first "^2.0.2" - -change-case-all@1.0.15: - version "1.0.15" - resolved "https://registry.yarnpkg.com/change-case-all/-/change-case-all-1.0.15.tgz#de29393167fc101d646cd76b0ef23e27d09756ad" - integrity sha512-3+GIFhk3sNuvFAJKU46o26OdzudQlPNBCu1ZQi3cMeMHhty1bhDxu2WrEilVNYaGvqUtR1VSigFcJOiS13dRhQ== - dependencies: - change-case "^4.1.2" - is-lower-case "^2.0.2" - is-upper-case "^2.0.2" - lower-case "^2.0.2" - lower-case-first "^2.0.2" - sponge-case "^1.0.1" - swap-case "^2.0.2" - title-case "^3.0.3" - upper-case "^2.0.2" - upper-case-first "^2.0.2" - -change-case@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/change-case/-/change-case-4.1.2.tgz#fedfc5f136045e2398c0410ee441f95704641e12" - integrity sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A== - dependencies: - camel-case "^4.1.2" - capital-case "^1.0.4" - constant-case "^3.0.4" - dot-case "^3.0.4" - header-case "^2.0.4" - no-case "^3.0.4" - param-case "^3.0.4" - pascal-case "^3.1.2" - path-case "^3.0.4" - sentence-case "^3.0.4" - snake-case "^3.0.4" - tslib "^2.0.3" - -character-entities-legacy@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" - integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== - -character-entities@^1.0.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" - integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== - -character-reference-invalid@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" - integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== - -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -"chokidar@>=3.0.0 <4.0.0": - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1, classnames@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" - integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-spinners@^2.5.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.7.0.tgz#f815fd30b5f9eaac02db604c7a231ed7cb2f797a" - integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw== - -cli-truncate@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" - integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== - dependencies: - slice-ansi "^3.0.0" - string-width "^4.2.0" - -cli-width@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" - integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== - -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" - -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.1" - wrap-ansi "^7.0.0" - -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== - -codem-isoboxer@0.3.6: - version "0.3.6" - resolved "https://registry.yarnpkg.com/codem-isoboxer/-/codem-isoboxer-0.3.6.tgz#867f670459b881d44f39168d5ff2a8f14c16151d" - integrity sha512-LuO8/7LW6XuR5ERn1yavXAfodGRhuY2yP60JTZIw5yNYMCE5lUVbk3NFUCJxjnphQH+Xemp5hOGb1LgUXm00Xw== - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colord@^2.9.3: - version "2.9.3" - resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" - integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== - -colorette@^2.0.16: - version "2.0.19" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" - integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== - -comma-separated-tokens@^1.0.0: - version "1.0.8" - resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" - integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== - -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -common-tags@1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" - integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -confusing-browser-globals@^1.0.10: - version "1.0.11" - resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" - integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== - -constant-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1" - integrity sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - upper-case "^2.0.2" - -convert-source-map@^1.5.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" - integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== - -convert-source-map@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" - integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== - -cookie@^0.4.0: - version "0.4.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" - integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== - -core-js-compat@^3.25.1: - version "3.28.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.28.0.tgz#c08456d854608a7264530a2afa281fadf20ecee6" - integrity sha512-myzPgE7QodMg4nnd3K1TDoES/nADRStM8Gpz0D6nhkwbmwEnE0ZGJgoWsvQ722FR8D7xS0n0LV556RcEicjTyg== - dependencies: - browserslist "^4.21.5" - -core-js@^3.27.2: - version "3.28.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.28.0.tgz#ed8b9e99c273879fdfff0edfc77ee709a5800e4a" - integrity sha512-GiZn9D4Z/rSYvTeg1ljAIsEqFm0LaN9gVtwDCrKL80zHtS31p9BAjmTxVqTQDMpwlMolJZOFntUG2uwyj7DAqw== - -cosmiconfig@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" - integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.2.1" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.10.0" - -cosmiconfig@^8.1.0, cosmiconfig@^8.1.3, cosmiconfig@^8.2.0: - version "8.3.6" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" - integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== - dependencies: - import-fresh "^3.3.0" - js-yaml "^4.1.0" - parse-json "^5.2.0" - path-type "^4.0.0" - -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - -cross-fetch@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== - dependencies: - node-fetch "2.6.7" - -cross-inspect@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/cross-inspect/-/cross-inspect-1.0.0.tgz#5fda1af759a148594d2d58394a9e21364f6849af" - integrity sha512-4PFfn4b5ZN6FMNGSZlyb7wUhuN8wvj8t/VQHZdM4JsDcruGJ8L2kf9zao98QIrBPFCpdk27qst/AGTl7pL3ypQ== - dependencies: - tslib "^2.4.0" - -cross-spawn@^7.0.2: - version "7.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" - integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -crypto-js@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" - integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== - -css-functions-list@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.1.0.tgz#cf5b09f835ad91a00e5959bcfc627cd498e1321b" - integrity sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w== - -css-tree@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" - integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== - dependencies: - mdn-data "2.0.30" - source-map-js "^1.0.1" - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -csstype@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" - integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== - -damerau-levenshtein@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" - integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== - -dashjs@^4.2.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/dashjs/-/dashjs-4.6.0.tgz#124c8371e192f1218746ce60b6aa0f175d4dcda4" - integrity sha512-0PDoSBM9PXb+Io0pRnw2CmO7aV9W8FC/BqBRNhLxzM3/e5Kfj7BLy0OWkkSB58ULg6Md6r+6jkGOTUhut/35rg== - dependencies: - bcp-47-match "^1.0.3" - bcp-47-normalize "^1.1.1" - codem-isoboxer "0.3.6" - es6-promise "^4.2.8" - fast-deep-equal "2.0.1" - html-entities "^1.2.1" - imsc "^1.0.2" - localforage "^1.7.1" - path-browserify "^1.0.1" - ua-parser-js "^1.0.2" - -dataloader@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.2.2.tgz#216dc509b5abe39d43a9b9d97e6e5e473dfbe3e0" - integrity sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g== - -date-fns@^2.0.1, date-fns@^2.24.0: - version "2.29.3" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" - integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== - -debounce@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" - integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== - -debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -debug@^3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -decamelize-keys@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" - integrity sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg== - dependencies: - decamelize "^1.1.0" - map-obj "^1.0.0" - -decamelize@^1.1.0, decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== - -decamelize@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-5.0.1.tgz#db11a92e58c741ef339fb0a2868d8a06a9a7b1e9" - integrity sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA== - -deep-equal@^2.0.5: - version "2.2.0" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.0.tgz#5caeace9c781028b9ff459f33b779346637c43e6" - integrity sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw== - dependencies: - call-bind "^1.0.2" - es-get-iterator "^1.1.2" - get-intrinsic "^1.1.3" - is-arguments "^1.1.1" - is-array-buffer "^3.0.1" - is-date-object "^1.0.5" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - isarray "^2.0.5" - object-is "^1.1.5" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.4.3" - side-channel "^1.0.4" - which-boxed-primitive "^1.0.2" - which-collection "^1.0.1" - which-typed-array "^1.1.9" - -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - -deepmerge@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" - integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== - -defaults@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" - integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== - dependencies: - clone "^1.0.2" - -define-properties@^1.1.3, define-properties@^1.1.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" - integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== - dependencies: - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -dependency-graph@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27" - integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg== - -dequal@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== - -detect-indent@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" - integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== - -diacritics@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1" - integrity sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA== - -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -doctrine@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== - dependencies: - esutils "^2.0.2" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" - integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== - dependencies: - "@babel/runtime" "^7.8.7" - csstype "^3.0.2" - -dom-walk@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" - integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w== - -dot-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" - integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - -dotenv@^16.0.0: - version "16.0.3" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" - integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== - -dset@^3.1.2: - version "3.1.4" - resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.4.tgz#f8eaf5f023f068a036d08cd07dc9ffb7d0065248" - integrity sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA== - -electron-to-chromium@^1.4.648: - version "1.4.648" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.648.tgz#c7b46c9010752c37bb4322739d6d2dd82354fbe4" - integrity sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.19.0, es-abstract@^1.20.4: - version "1.21.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.1.tgz#e6105a099967c08377830a0c9cb589d570dd86c6" - integrity sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg== - dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - es-set-tostringtag "^2.0.1" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - function.prototype.name "^1.1.5" - get-intrinsic "^1.1.3" - get-symbol-description "^1.0.0" - globalthis "^1.0.3" - gopd "^1.0.1" - has "^1.0.3" - has-property-descriptors "^1.0.0" - has-proto "^1.0.1" - has-symbols "^1.0.3" - internal-slot "^1.0.4" - is-array-buffer "^3.0.1" - is-callable "^1.2.7" - is-negative-zero "^2.0.2" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - is-string "^1.0.7" - is-typed-array "^1.1.10" - is-weakref "^1.0.2" - object-inspect "^1.12.2" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.4.3" - safe-regex-test "^1.0.0" - string.prototype.trimend "^1.0.6" - string.prototype.trimstart "^1.0.6" - typed-array-length "^1.0.4" - unbox-primitive "^1.0.2" - which-typed-array "^1.1.9" - -es-get-iterator@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" - integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" - has-symbols "^1.0.3" - is-arguments "^1.1.1" - is-map "^2.0.2" - is-set "^2.0.2" - is-string "^1.0.7" - isarray "^2.0.5" - stop-iteration-iterator "^1.0.0" - -es-set-tostringtag@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" - integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== - dependencies: - get-intrinsic "^1.1.3" - has "^1.0.3" - has-tostringtag "^1.0.0" - -es-shim-unscopables@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" - integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== - dependencies: - has "^1.0.3" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -es6-promise@^4.2.8: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -esbuild@^0.18.10: - version "0.18.20" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6" - integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA== - optionalDependencies: - "@esbuild/android-arm" "0.18.20" - "@esbuild/android-arm64" "0.18.20" - "@esbuild/android-x64" "0.18.20" - "@esbuild/darwin-arm64" "0.18.20" - "@esbuild/darwin-x64" "0.18.20" - "@esbuild/freebsd-arm64" "0.18.20" - "@esbuild/freebsd-x64" "0.18.20" - "@esbuild/linux-arm" "0.18.20" - "@esbuild/linux-arm64" "0.18.20" - "@esbuild/linux-ia32" "0.18.20" - "@esbuild/linux-loong64" "0.18.20" - "@esbuild/linux-mips64el" "0.18.20" - "@esbuild/linux-ppc64" "0.18.20" - "@esbuild/linux-riscv64" "0.18.20" - "@esbuild/linux-s390x" "0.18.20" - "@esbuild/linux-x64" "0.18.20" - "@esbuild/netbsd-x64" "0.18.20" - "@esbuild/openbsd-x64" "0.18.20" - "@esbuild/sunos-x64" "0.18.20" - "@esbuild/win32-arm64" "0.18.20" - "@esbuild/win32-ia32" "0.18.20" - "@esbuild/win32-x64" "0.18.20" - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-config-airbnb-base@^15.0.0: - version "15.0.0" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz#6b09add90ac79c2f8d723a2580e07f3925afd236" - integrity sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig== - dependencies: - confusing-browser-globals "^1.0.10" - object.assign "^4.1.2" - object.entries "^1.1.5" - semver "^6.3.0" - -eslint-config-airbnb-typescript@^17.0.0: - version "17.0.0" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.0.0.tgz#360dbcf810b26bbcf2ff716198465775f1c49a07" - integrity sha512-elNiuzD0kPAPTXjFWg+lE24nMdHMtuxgYoD30OyMD6yrW1AhFZPAg27VX7d3tzOErw+dgJTNWfRSDqEcXb4V0g== - dependencies: - eslint-config-airbnb-base "^15.0.0" - -eslint-config-airbnb@^19.0.4: - version "19.0.4" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz#84d4c3490ad70a0ffa571138ebcdea6ab085fdc3" - integrity sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew== - dependencies: - eslint-config-airbnb-base "^15.0.0" - object.assign "^4.1.2" - object.entries "^1.1.5" - -eslint-config-prettier@^8.6.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz#dec1d29ab728f4fa63061774e1672ac4e363d207" - integrity sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA== - -eslint-import-resolver-node@^0.3.7: - version "0.3.7" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz#83b375187d412324a1963d84fa664377a23eb4d7" - integrity sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA== - dependencies: - debug "^3.2.7" - is-core-module "^2.11.0" - resolve "^1.22.1" - -eslint-module-utils@^2.7.4: - version "2.7.4" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974" - integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== - dependencies: - debug "^3.2.7" - -eslint-plugin-import@^2.27.5: - version "2.27.5" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65" - integrity sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow== - dependencies: - array-includes "^3.1.6" - array.prototype.flat "^1.3.1" - array.prototype.flatmap "^1.3.1" - debug "^3.2.7" - doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.7" - eslint-module-utils "^2.7.4" - has "^1.0.3" - is-core-module "^2.11.0" - is-glob "^4.0.3" - minimatch "^3.1.2" - object.values "^1.1.6" - resolve "^1.22.1" - semver "^6.3.0" - tsconfig-paths "^3.14.1" - -eslint-plugin-jsx-a11y@^6.7.1: - version "6.7.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz#fca5e02d115f48c9a597a6894d5bcec2f7a76976" - integrity sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA== - dependencies: - "@babel/runtime" "^7.20.7" - aria-query "^5.1.3" - array-includes "^3.1.6" - array.prototype.flatmap "^1.3.1" - ast-types-flow "^0.0.7" - axe-core "^4.6.2" - axobject-query "^3.1.1" - damerau-levenshtein "^1.0.8" - emoji-regex "^9.2.2" - has "^1.0.3" - jsx-ast-utils "^3.3.3" - language-tags "=1.0.5" - minimatch "^3.1.2" - object.entries "^1.1.6" - object.fromentries "^2.0.6" - semver "^6.3.0" - -eslint-plugin-react-hooks@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" - integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== - -eslint-plugin-react@^7.32.2: - version "7.32.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz#e71f21c7c265ebce01bcbc9d0955170c55571f10" - integrity sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg== - dependencies: - array-includes "^3.1.6" - array.prototype.flatmap "^1.3.1" - array.prototype.tosorted "^1.1.1" - doctrine "^2.1.0" - estraverse "^5.3.0" - jsx-ast-utils "^2.4.1 || ^3.0.0" - minimatch "^3.1.2" - object.entries "^1.1.6" - object.fromentries "^2.0.6" - object.hasown "^1.1.2" - object.values "^1.1.6" - prop-types "^15.8.1" - resolve "^2.0.0-next.4" - semver "^6.3.0" - string.prototype.matchall "^4.0.8" - -eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -eslint-scope@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" - integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint-visitor-keys@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" - integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== - -eslint@^8.34.0: - version "8.34.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.34.0.tgz#fe0ab0ef478104c1f9ebc5537e303d25a8fb22d6" - integrity sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg== - dependencies: - "@eslint/eslintrc" "^1.4.1" - "@humanwhocodes/config-array" "^0.11.8" - "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.4.0" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.2" - globals "^13.19.0" - grapheme-splitter "^1.0.4" - ignore "^5.2.0" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-sdsl "^4.1.4" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - -espree@^9.4.0: - version "9.4.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.1.tgz#51d6092615567a2c2cff7833445e37c28c0065bd" - integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg== - dependencies: - acorn "^8.8.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.3.0" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.1.tgz#ddb8e1e2666750113b78c15f59e977564f52b116" - integrity sha512-3ZggxvMv5EEY1ssUVyHSVt0oPreyBfbUi1XikJVfjFiBeBDLdrb0IWoDiEwqT/2sUQi0TGaWtFhOGDD8RTpXgQ== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -event-target-polyfill@^0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/event-target-polyfill/-/event-target-polyfill-0.0.4.tgz#060ee66e85aaedc76b6fa66079782dcc11cba496" - integrity sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ== - -extend@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -extract-files@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-11.0.0.tgz#b72d428712f787eef1f5193aff8ab5351ca8469a" - integrity sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ== - -extract-files@^13.0.0: - version "13.0.0" - resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-13.0.0.tgz#9065073dedbcfb5e2ae8a90988cf609834b217ec" - integrity sha512-FXD+2Tsr8Iqtm3QZy1Zmwscca7Jx3mMC5Crr+sEP1I303Jy1CYMuYCm7hRTplFNg3XdUavErkxnTzpaqdSoi6g== - dependencies: - is-plain-obj "^4.1.0" - -extract-react-intl-messages@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/extract-react-intl-messages/-/extract-react-intl-messages-4.1.1.tgz#cd01d99053bb053ecc8410ccdccb9ac56daae91c" - integrity sha512-dPogci5X7HVtV7VbUxajH/1YgfNRaW2VtEiVidZ/31Tq8314uzOtzVMNo0IrAPD2E+H1wHoPiu/j565TZsyIZg== - dependencies: - "@babel/core" "^7.9.0" - babel-plugin-react-intl "^7.0.0" - flat "^5.0.0" - glob "^7.1.6" - js-yaml "^3.13.1" - load-json-file "^6.2.0" - lodash.merge "^4.6.2" - lodash.mergewith "^4.6.2" - lodash.pick "^4.4.0" - meow "^6.1.0" - mkdirp "^1.0.3" - pify "^5.0.0" - read-babelrc-up "^1.1.0" - sort-keys "^4.0.0" - write-json-file "^4.3.0" - -fast-decode-uri-component@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" - integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== - -fast-deep-equal@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-glob@^3.2.9, fast-glob@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.0.tgz#7c40cb491e1e2ed5664749e87bfb516dbe8727c0" - integrity sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - -fast-querystring@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/fast-querystring/-/fast-querystring-1.1.1.tgz#f4c56ef56b1a954880cfd8c01b83f9e1a3d3fda2" - integrity sha512-qR2r+e3HvhEFmpdHMv//U8FnFlnYjaC6QKDuaXALDkw2kvHO8WDjxH+f/rHGR4Me4pnk8p9JAkRNTjYHAKRn2Q== - dependencies: - fast-decode-uri-component "^1.0.1" - -fast-url-parser@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" - integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ== - dependencies: - punycode "^1.3.2" - -fastest-levenshtein@^1.0.16: - version "1.0.16" - resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" - integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== - -fastq@^1.6.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" - integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== - dependencies: - reusify "^1.0.4" - -fb-watchman@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" - integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== - dependencies: - bser "2.1.1" - -fbjs-css-vars@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8" - integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== - -fbjs@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-3.0.4.tgz#e1871c6bd3083bac71ff2da868ad5067d37716c6" - integrity sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ== - dependencies: - cross-fetch "^3.1.5" - fbjs-css-vars "^1.0.0" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - setimmediate "^1.0.5" - ua-parser-js "^0.7.30" - -fflate@~0.6.9: - version "0.6.10" - resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.6.10.tgz#5f40f9659205936a2d18abf88b2e7781662b6d43" - integrity sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg== - -figures@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -find-root@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" - integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== - -find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -flag-icons@^6.6.6: - version "6.6.6" - resolved "https://registry.yarnpkg.com/flag-icons/-/flag-icons-6.6.6.tgz#9ddff81e1126778ca6a5a1e0e2cfac2e865f2cb7" - integrity sha512-4lHDKxldnQ7q617pf9Dx9nAetT+9zcMpUexbRrc9kjLw9KJgZ83zA5Dky3Vv7ZDzUjAiZ46x/cy5P0HnEnqA2A== - -flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== - dependencies: - flatted "^3.1.0" - rimraf "^3.0.2" - -flat@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - -flatted@^3.1.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== - -flexbin@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/flexbin/-/flexbin-0.2.0.tgz#0126306d3d595fcb7dfcb87149b9c9599ff8f4e9" - integrity sha512-dgCeT6/oVljr0eao0f7Eg2VXutK/+rp02J6Nkw22uTTFE4HSC7zfYRzjuy2/r0dhr/sUBRMJM2tMyOCi+HeU+A== - -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - -formik@^2.4.5: - version "2.4.5" - resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.5.tgz#f899b5b7a6f103a8fabb679823e8fafc7e0ee1b4" - integrity sha512-Gxlht0TD3vVdzMDHwkiNZqJ7Mvg77xQNfmBRrNtvzcHZs72TJppSTDKHpImCMJZwcWPBJ8jSQQ95GJzXFf1nAQ== - dependencies: - "@types/hoist-non-react-statics" "^3.3.1" - deepmerge "^2.1.1" - hoist-non-react-statics "^3.3.0" - lodash "^4.17.21" - lodash-es "^4.17.21" - react-fast-compare "^2.0.1" - tiny-warning "^1.0.2" - tslib "^2.0.0" - -fs-extra@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" - integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-extra@^9.0.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - -function-bind@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -function.prototype.name@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" - integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.0" - functions-have-names "^1.2.2" - -functions-have-names@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" - integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== - -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-caller-file@^2.0.1, get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" - integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.3" - -get-symbol-description@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" - integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" - -gl-preserve-state@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gl-preserve-state/-/gl-preserve-state-1.0.0.tgz#4ef710d62873f1470ed015c6546c37dacddd4198" - integrity sha512-zQZ25l3haD4hvgJZ6C9+s0ebdkW9y+7U2qxvGu1uWOJh8a4RU+jURIKEQhf8elIlFpMH6CrAY2tH0mYrRjet3Q== - -glob-parent@^5.1.2, glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob@^7.1.1, glob@^7.1.3, glob@^7.1.6: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-modules@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - -global-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" - integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== - dependencies: - ini "^1.3.5" - kind-of "^6.0.2" - which "^1.3.1" - -global@^4.3.1, global@^4.3.2, global@^4.4.0, global@~4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406" - integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w== - dependencies: - min-document "^2.19.0" - process "^0.11.10" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globals@^13.19.0: - version "13.20.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" - integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== - dependencies: - type-fest "^0.20.2" - -globalthis@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" - integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== - dependencies: - define-properties "^1.1.3" - -globby@^11.0.3, globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -globjoin@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" - integrity sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg== - -globrex@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" - integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== - -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - -graceful-fs@^4.1.15, graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== - -grapheme-splitter@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== - -graphql-config@^5.0.2: - version "5.0.3" - resolved "https://registry.yarnpkg.com/graphql-config/-/graphql-config-5.0.3.tgz#d9aa2954cf47a927f9cb83cdc4e42ae55d0b321e" - integrity sha512-BNGZaoxIBkv9yy6Y7omvsaBUHOzfFcII3UN++tpH8MGOKFPFkCPZuwx09ggANMt8FgyWP1Od8SWPmrUEZca4NQ== - dependencies: - "@graphql-tools/graphql-file-loader" "^8.0.0" - "@graphql-tools/json-file-loader" "^8.0.0" - "@graphql-tools/load" "^8.0.0" - "@graphql-tools/merge" "^9.0.0" - "@graphql-tools/url-loader" "^8.0.0" - "@graphql-tools/utils" "^10.0.0" - cosmiconfig "^8.1.0" - jiti "^1.18.2" - minimatch "^4.2.3" - string-env-interpolation "^1.0.1" - tslib "^2.4.0" - -graphql-request@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-6.1.0.tgz#f4eb2107967af3c7a5907eb3131c671eac89be4f" - integrity sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw== - dependencies: - "@graphql-typed-document-node/core" "^3.2.0" - cross-fetch "^3.1.5" - -graphql-tag@^2.11.0, graphql-tag@^2.12.6: - version "2.12.6" - resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" - integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg== - dependencies: - tslib "^2.1.0" - -graphql-ws@^5.14.0, graphql-ws@^5.14.3: - version "5.14.3" - resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.14.3.tgz#fb1fba011a0ae9c4e86d831cae2ec27955168b9a" - integrity sha512-F/i2xNIVbaEF2xWggID0X/UZQa2V8kqKDPO8hwmu53bVOcTL7uNkxnexeEgSCVxYBQUTUNEI8+e4LO1FOhKPKQ== - -"graphql@14 - 16", graphql@^16.8.1: - version "16.8.1" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" - integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== - -hard-rejection@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" - integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== - -has-bigints@^1.0.1, has-bigints@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== - dependencies: - get-intrinsic "^1.1.1" - -has-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" - integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== - -has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== - dependencies: - has-symbols "^1.0.2" - -has@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" - integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== - -hast-to-hyperscript@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz#9b67fd188e4c81e8ad66f803855334173920218d" - integrity sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA== - dependencies: - "@types/unist" "^2.0.3" - comma-separated-tokens "^1.0.0" - property-information "^5.3.0" - space-separated-tokens "^1.0.0" - style-to-object "^0.3.0" - unist-util-is "^4.0.0" - web-namespaces "^1.0.0" - -header-case@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063" - integrity sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q== - dependencies: - capital-case "^1.0.4" - tslib "^2.0.3" - -history@^4.9.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" - integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== - dependencies: - "@babel/runtime" "^7.1.2" - loose-envify "^1.2.0" - resolve-pathname "^3.0.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - value-equal "^1.0.1" - -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - -hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - -hosted-git-info@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" - integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== - dependencies: - lru-cache "^6.0.0" - -html-entities@^1.2.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc" - integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA== - -html-tags@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" - integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== - -http-proxy-agent@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz#e9096c5afd071a3fce56e6252bb321583c124673" - integrity sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ== - dependencies: - agent-base "^7.1.0" - debug "^4.3.4" - -https-proxy-agent@^7.0.0: - version "7.0.2" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz#e2645b846b90e96c6e6f347fb5b2e41f1590b09b" - integrity sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA== - dependencies: - agent-base "^7.0.2" - debug "4" - -i18n-iso-countries@^7.5.0: - version "7.5.0" - resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-7.5.0.tgz#74fedd72619526a195cfb2e768fe1d82eed2123f" - integrity sha512-PtfKJNWLVhhU0KBX/8asmywjAcuyQk07mmmMwxFJcddTNBJJ1yvpY2qxVmyxbtVF+9+6eg9phgpv83XPUKU5CA== - dependencies: - diacritics "1.3.0" - -iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore@^5.2.0, ignore@^5.2.4: - version "5.2.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" - integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== - -immediate@~3.0.5: - version "3.0.6" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" - integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== - -immutable@^4.0.0: - version "4.2.4" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.4.tgz#83260d50889526b4b531a5e293709a77f7c55a2a" - integrity sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w== - -immutable@~3.7.6: - version "3.7.6" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b" - integrity sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw== - -import-fresh@^3.0.0, import-fresh@^3.2.1, import-fresh@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-from@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/import-from/-/import-from-4.0.0.tgz#2710b8d66817d232e16f4166e319248d3d5492e2" - integrity sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ== - -import-lazy@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" - integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== - -imsc@^1.0.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/imsc/-/imsc-1.1.3.tgz#e96a60a50d4000dd7b44097272768b9fd6a4891d" - integrity sha512-IY0hMkVTNoqoYwKEp5UvNNKp/A5jeJUOrIO7judgOyhHT+xC6PA4VBOMAOhdtAYbMRHx9DTgI8p6Z6jhYQPFDA== - dependencies: - sax "1.2.1" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -indent-string@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5" - integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg== - -individual@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/individual/-/individual-2.0.0.tgz#833b097dad23294e76117a98fb38e0d9ad61bb97" - integrity sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.3, inherits@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ini@^1.3.5: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -inline-style-parser@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" - integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== - -inquirer@^8.0.0: - version "8.2.5" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.5.tgz#d8654a7542c35a9b9e069d27e2df4858784d54f8" - integrity sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ== - dependencies: - ansi-escapes "^4.2.1" - chalk "^4.1.1" - cli-cursor "^3.1.0" - cli-width "^3.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.21" - mute-stream "0.0.8" - ora "^5.4.1" - run-async "^2.4.0" - rxjs "^7.5.5" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" - wrap-ansi "^7.0.0" - -internal-slot@^1.0.3, internal-slot@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" - integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== - dependencies: - get-intrinsic "^1.2.0" - has "^1.0.3" - side-channel "^1.0.4" - -intersection-observer@^0.12.2: - version "0.12.2" - resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.12.2.tgz#4a45349cc0cd91916682b1f44c28d7ec737dc375" - integrity sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg== - -intl-messageformat-parser@6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-6.1.2.tgz#28c65f3689f538e66c7cf628881548d6a82ff3c2" - integrity sha512-4GQDEPhl/ZMNDKwMsLqyw1LG2IAWjmLJXdmnRcHKeLQzpgtNYZI6lVw1279pqIkRk2MfKb9aDsVFzm565azK5A== - dependencies: - "@formatjs/ecma402-abstract" "1.5.0" - tslib "^2.0.1" - -intl-messageformat-parser@^5.3.7: - version "5.5.1" - resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-5.5.1.tgz#f09a692755813e6220081e3374df3fb1698bd0c6" - integrity sha512-TvB3LqF2VtP6yI6HXlRT5TxX98HKha6hCcrg9dwlPwNaedVNuQA9KgBdtWKgiyakyCTYHQ+KJeFEstNKfZr64w== - dependencies: - "@formatjs/intl-numberformat" "^5.5.2" - -intl-messageformat@10.3.0: - version "10.3.0" - resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.3.0.tgz#6a3a30882bf94dfa7014cc642c66abdafd942c0e" - integrity sha512-FKeBZKH9T2Ue4RUXCuwY/hEaRHU8cgICevlGKog0qSBuz/amtRKNBLetBLmRxiHeEkF7JBBckC+56GIwshlRwA== - dependencies: - "@formatjs/ecma402-abstract" "1.14.3" - "@formatjs/fast-memoize" "1.2.8" - "@formatjs/icu-messageformat-parser" "2.2.0" - tslib "^2.4.0" - -invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - -is-absolute@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" - integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== - dependencies: - is-relative "^1.0.0" - is-windows "^1.0.1" - -is-alphabetical@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" - integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== - -is-alphanumerical@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf" - integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A== - dependencies: - is-alphabetical "^1.0.0" - is-decimal "^1.0.0" - -is-arguments@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-array-buffer@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.1.tgz#deb1db4fcae48308d54ef2442706c0393997052a" - integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" - is-typed-array "^1.1.10" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-buffer@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" - integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== - -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - -is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.5.0, is-core-module@^2.9.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" - integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== - dependencies: - has "^1.0.3" - -is-date-object@^1.0.1, is-date-object@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - -is-decimal@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" - integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-function@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.2.tgz#4f097f30abf6efadac9833b17ca5dc03f8144e08" - integrity sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ== - -is-glob@4.0.3, is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-hexadecimal@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" - integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== - -is-interactive@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" - integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== - -is-lower-case@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-lower-case/-/is-lower-case-2.0.2.tgz#1c0884d3012c841556243483aa5d522f47396d2a" - integrity sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ== - dependencies: - tslib "^2.0.3" - -is-map@^2.0.1, is-map@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" - integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== - -is-negative-zero@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" - integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== - -is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== - dependencies: - has-tostringtag "^1.0.0" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-path-inside@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -is-plain-obj@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" - integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== - -is-plain-obj@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - -is-plain-obj@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" - integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== - -is-plain-object@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" - integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== - -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-relative@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" - integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== - dependencies: - is-unc-path "^1.0.0" - -is-set@^2.0.1, is-set@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" - integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== - -is-shared-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" - integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== - dependencies: - call-bind "^1.0.2" - -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - -is-typed-array@^1.1.10, is-typed-array@^1.1.9: - version "1.1.10" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" - integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== - dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.0" - -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== - -is-unc-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" - integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== - dependencies: - unc-path-regex "^0.1.2" - -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== - -is-upper-case@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-upper-case/-/is-upper-case-2.0.2.tgz#f1105ced1fe4de906a5f39553e7d3803fd804649" - integrity sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ== - dependencies: - tslib "^2.0.3" - -is-weakmap@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" - integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== - -is-weakref@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== - dependencies: - call-bind "^1.0.2" - -is-weakset@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" - integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" - -is-windows@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== - -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -isomorphic-ws@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" - integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== - -jiti@^1.17.1, jiti@^1.18.2: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" - integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== - -jose@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/jose/-/jose-5.2.0.tgz#d0ffd7f7e31253f633eefb190a930cd14a916995" - integrity sha512-oW3PCnvyrcm1HMvGTzqjxxfnEs9EoFOFWi2HsEGhlFVOXxTE3K9GKWVMFoFw06yPUqwpvEWic1BmtUZBI/tIjw== - -js-sdsl@^4.1.4: - version "4.3.0" - resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" - integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -js-yaml@^4.0.0, js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== - -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== - -json-stable-stringify@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz#e06f23128e0bbe342dc996ed5a19e28b57b580e0" - integrity sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g== - dependencies: - jsonify "^0.0.1" - -json-to-pretty-yaml@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz#f4cd0bd0a5e8fe1df25aaf5ba118b099fd992d5b" - integrity sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A== - dependencies: - remedial "^1.0.7" - remove-trailing-spaces "^1.0.6" - -json2mq@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/json2mq/-/json2mq-0.2.0.tgz#b637bd3ba9eabe122c83e9720483aeb10d2c904a" - integrity sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA== - dependencies: - string-convert "^0.2.0" - -json5@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" - integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== - dependencies: - minimist "^1.2.0" - -json5@^2.1.2, json5@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -jsonify@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" - integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== - -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea" - integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw== - dependencies: - array-includes "^3.1.5" - object.assign "^4.1.3" - -keycode@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff" - integrity sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg== - -kind-of@^6.0.2, kind-of@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -known-css-properties@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.27.0.tgz#82a9358dda5fe7f7bd12b5e7142c0a205393c0c5" - integrity sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg== - -language-subtag-registry@~0.3.2: - version "0.3.22" - resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" - integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== - -language-tags@=1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" - integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ== - dependencies: - language-subtag-registry "~0.3.2" - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -lie@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" - integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== - dependencies: - immediate "~3.0.5" - -lil-gui@~0.17.0: - version "0.17.0" - resolved "https://registry.yarnpkg.com/lil-gui/-/lil-gui-0.17.0.tgz#b41ae55d0023fcd9185f7395a218db0f58189663" - integrity sha512-MVBHmgY+uEbmJNApAaPbtvNh1RCAeMnKym82SBjtp5rODTYKWtM+MXHCifLe2H2Ti1HuBGBtK/5SyG4ShQ3pUQ== - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -listr2@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-4.0.5.tgz#9dcc50221583e8b4c71c43f9c7dfd0ef546b75d5" - integrity sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA== - dependencies: - cli-truncate "^2.1.0" - colorette "^2.0.16" - log-update "^4.0.0" - p-map "^4.0.0" - rfdc "^1.3.0" - rxjs "^7.5.5" - through "^2.3.8" - wrap-ansi "^7.0.0" - -load-json-file@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1" - integrity sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ== - dependencies: - graceful-fs "^4.1.15" - parse-json "^5.0.0" - strip-bom "^4.0.0" - type-fest "^0.6.0" - -localforage@^1.10.0, localforage@^1.7.1: - version "1.10.0" - resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" - integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== - dependencies: - lie "3.1.1" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -lodash-es@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" - integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash.mergewith@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" - integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== - -lodash.pick@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" - integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== - -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== - -lodash@^4.17.20, lodash@^4.17.21, lodash@~4.17.0: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -log-symbols@^4.0.0, log-symbols@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - -log-update@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" - integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== - dependencies: - ansi-escapes "^4.3.0" - cli-cursor "^3.1.0" - slice-ansi "^4.0.0" - wrap-ansi "^6.2.0" - -longest-streak@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" - integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== - -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lower-case-first@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/lower-case-first/-/lower-case-first-2.0.2.tgz#64c2324a2250bf7c37c5901e76a5b5309301160b" - integrity sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg== - dependencies: - tslib "^2.0.3" - -lower-case@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" - integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== - dependencies: - tslib "^2.0.3" - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -m3u8-parser@4.8.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.8.0.tgz#4a2d591fdf6f2579d12a327081198df8af83083d" - integrity sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA== - dependencies: - "@babel/runtime" "^7.12.5" - "@videojs/vhs-utils" "^3.0.5" - global "^4.4.0" - -magic-string@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" - integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.13" - -make-dir@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -map-cache@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg== - -map-obj@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - integrity sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg== - -map-obj@^4.0.0, map-obj@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" - integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== - -markdown-table@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b" - integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A== - dependencies: - repeat-string "^1.0.0" - -mathml-tag-names@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" - integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== - -mdast-util-definitions@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2" - integrity sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ== - dependencies: - unist-util-visit "^2.0.0" - -mdast-util-find-and-replace@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-1.1.1.tgz#b7db1e873f96f66588c321f1363069abf607d1b5" - integrity sha512-9cKl33Y21lyckGzpSmEQnIDjEfeeWelN5s1kUW1LwdB0Fkuq2u+4GdqcGEygYxJE8GVqCl0741bYXHgamfWAZA== - dependencies: - escape-string-regexp "^4.0.0" - unist-util-is "^4.0.0" - unist-util-visit-parents "^3.0.0" - -mdast-util-from-markdown@^0.8.0: - version "0.8.5" - resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz#d1ef2ca42bc377ecb0463a987910dae89bd9a28c" - integrity sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-string "^2.0.0" - micromark "~2.11.0" - parse-entities "^2.0.0" - unist-util-stringify-position "^2.0.0" - -mdast-util-gfm-autolink-literal@^0.1.0: - version "0.1.3" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-0.1.3.tgz#9c4ff399c5ddd2ece40bd3b13e5447d84e385fb7" - integrity sha512-GjmLjWrXg1wqMIO9+ZsRik/s7PLwTaeCHVB7vRxUwLntZc8mzmTsLVr6HW1yLokcnhfURsn5zmSVdi3/xWWu1A== - dependencies: - ccount "^1.0.0" - mdast-util-find-and-replace "^1.1.0" - micromark "^2.11.3" - -mdast-util-gfm-strikethrough@^0.2.0: - version "0.2.3" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-0.2.3.tgz#45eea337b7fff0755a291844fbea79996c322890" - integrity sha512-5OQLXpt6qdbttcDG/UxYY7Yjj3e8P7X16LzvpX8pIQPYJ/C2Z1qFGMmcw+1PZMUM3Z8wt8NRfYTvCni93mgsgA== - dependencies: - mdast-util-to-markdown "^0.6.0" - -mdast-util-gfm-table@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-0.1.6.tgz#af05aeadc8e5ee004eeddfb324b2ad8c029b6ecf" - integrity sha512-j4yDxQ66AJSBwGkbpFEp9uG/LS1tZV3P33fN1gkyRB2LoRL+RR3f76m0HPHaby6F4Z5xr9Fv1URmATlRRUIpRQ== - dependencies: - markdown-table "^2.0.0" - mdast-util-to-markdown "~0.6.0" - -mdast-util-gfm-task-list-item@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-0.1.6.tgz#70c885e6b9f543ddd7e6b41f9703ee55b084af10" - integrity sha512-/d51FFIfPsSmCIRNp7E6pozM9z1GYPIkSy1urQ8s/o4TC22BZ7DqfHFWiqBD23bc7J3vV1Fc9O4QIHBlfuit8A== - dependencies: - mdast-util-to-markdown "~0.6.0" - -mdast-util-gfm@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-0.1.2.tgz#8ecddafe57d266540f6881f5c57ff19725bd351c" - integrity sha512-NNkhDx/qYcuOWB7xHUGWZYVXvjPFFd6afg6/e2g+SV4r9q5XUcCbV4Wfa3DLYIiD+xAEZc6K4MGaE/m0KDcPwQ== - dependencies: - mdast-util-gfm-autolink-literal "^0.1.0" - mdast-util-gfm-strikethrough "^0.2.0" - mdast-util-gfm-table "^0.1.0" - mdast-util-gfm-task-list-item "^0.1.0" - mdast-util-to-markdown "^0.6.1" - -mdast-util-to-hast@^10.2.0: - version "10.2.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.2.0.tgz#61875526a017d8857b71abc9333942700b2d3604" - integrity sha512-JoPBfJ3gBnHZ18icCwHR50orC9kNH81tiR1gs01D8Q5YpV6adHNO9nKNuFBCJQ941/32PT1a63UF/DitmS3amQ== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - mdast-util-definitions "^4.0.0" - mdurl "^1.0.0" - unist-builder "^2.0.0" - unist-util-generated "^1.0.0" - unist-util-position "^3.0.0" - unist-util-visit "^2.0.0" - -mdast-util-to-markdown@^0.6.0, mdast-util-to-markdown@^0.6.1, mdast-util-to-markdown@~0.6.0: - version "0.6.5" - resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz#b33f67ca820d69e6cc527a93d4039249b504bebe" - integrity sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ== - dependencies: - "@types/unist" "^2.0.0" - longest-streak "^2.0.0" - mdast-util-to-string "^2.0.0" - parse-entities "^2.0.0" - repeat-string "^1.0.0" - zwitch "^1.0.0" - -mdast-util-to-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz#b8cfe6a713e1091cb5b728fc48885a4767f8b97b" - integrity sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w== - -mdn-data@2.0.30: - version "2.0.30" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" - integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== - -mdurl@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" - integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== - -memoize-one@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" - integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== - -meow@^10.1.5: - version "10.1.5" - resolved "https://registry.yarnpkg.com/meow/-/meow-10.1.5.tgz#be52a1d87b5f5698602b0f32875ee5940904aa7f" - integrity sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw== - dependencies: - "@types/minimist" "^1.2.2" - camelcase-keys "^7.0.0" - decamelize "^5.0.0" - decamelize-keys "^1.1.0" - hard-rejection "^2.1.0" - minimist-options "4.1.0" - normalize-package-data "^3.0.2" - read-pkg-up "^8.0.0" - redent "^4.0.0" - trim-newlines "^4.0.2" - type-fest "^1.2.2" - yargs-parser "^20.2.9" - -meow@^6.1.0: - version "6.1.1" - resolved "https://registry.yarnpkg.com/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467" - integrity sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg== - dependencies: - "@types/minimist" "^1.2.0" - camelcase-keys "^6.2.2" - decamelize-keys "^1.1.0" - hard-rejection "^2.1.0" - minimist-options "^4.0.2" - normalize-package-data "^2.5.0" - read-pkg-up "^7.0.1" - redent "^3.0.0" - trim-newlines "^3.0.0" - type-fest "^0.13.1" - yargs-parser "^18.1.3" - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -meros@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/meros/-/meros-1.2.1.tgz#056f7a76e8571d0aaf3c7afcbe7eb6407ff7329e" - integrity sha512-R2f/jxYqCAGI19KhAvaxSOxALBMkaXWH2a7rOyqQw+ZmizX5bKkEYWLzdhC+U82ZVVPVp6MCXe3EkVligh+12g== - -meshoptimizer@~0.18.1: - version "0.18.1" - resolved "https://registry.yarnpkg.com/meshoptimizer/-/meshoptimizer-0.18.1.tgz#cdb90907f30a7b5b1190facd3b7ee6b7087797d8" - integrity sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw== - -micromark-extension-gfm-autolink-literal@~0.5.0: - version "0.5.7" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-0.5.7.tgz#53866c1f0c7ef940ae7ca1f72c6faef8fed9f204" - integrity sha512-ePiDGH0/lhcngCe8FtH4ARFoxKTUelMp4L7Gg2pujYD5CSMb9PbblnyL+AAMud/SNMyusbS2XDSiPIRcQoNFAw== - dependencies: - micromark "~2.11.3" - -micromark-extension-gfm-strikethrough@~0.6.5: - version "0.6.5" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-0.6.5.tgz#96cb83356ff87bf31670eefb7ad7bba73e6514d1" - integrity sha512-PpOKlgokpQRwUesRwWEp+fHjGGkZEejj83k9gU5iXCbDG+XBA92BqnRKYJdfqfkrRcZRgGuPuXb7DaK/DmxOhw== - dependencies: - micromark "~2.11.0" - -micromark-extension-gfm-table@~0.4.0: - version "0.4.3" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-0.4.3.tgz#4d49f1ce0ca84996c853880b9446698947f1802b" - integrity sha512-hVGvESPq0fk6ALWtomcwmgLvH8ZSVpcPjzi0AjPclB9FsVRgMtGZkUcpE0zgjOCFAznKepF4z3hX8z6e3HODdA== - dependencies: - micromark "~2.11.0" - -micromark-extension-gfm-tagfilter@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-0.3.0.tgz#d9f26a65adee984c9ccdd7e182220493562841ad" - integrity sha512-9GU0xBatryXifL//FJH+tAZ6i240xQuFrSL7mYi8f4oZSbc+NvXjkrHemeYP0+L4ZUT+Ptz3b95zhUZnMtoi/Q== - -micromark-extension-gfm-task-list-item@~0.3.0: - version "0.3.3" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-0.3.3.tgz#d90c755f2533ed55a718129cee11257f136283b8" - integrity sha512-0zvM5iSLKrc/NQl84pZSjGo66aTGd57C1idmlWmE87lkMcXrTxg1uXa/nXomxJytoje9trP0NDLvw4bZ/Z/XCQ== - dependencies: - micromark "~2.11.0" - -micromark-extension-gfm@^0.3.0: - version "0.3.3" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-0.3.3.tgz#36d1a4c089ca8bdfd978c9bd2bf1a0cb24e2acfe" - integrity sha512-oVN4zv5/tAIA+l3GbMi7lWeYpJ14oQyJ3uEim20ktYFAcfX1x3LNlFGGlmrZHt7u9YlKExmyJdDGaTt6cMSR/A== - dependencies: - micromark "~2.11.0" - micromark-extension-gfm-autolink-literal "~0.5.0" - micromark-extension-gfm-strikethrough "~0.6.5" - micromark-extension-gfm-table "~0.4.0" - micromark-extension-gfm-tagfilter "~0.3.0" - micromark-extension-gfm-task-list-item "~0.3.0" - -micromark@^2.11.3, micromark@~2.11.0, micromark@~2.11.3: - version "2.11.4" - resolved "https://registry.yarnpkg.com/micromark/-/micromark-2.11.4.tgz#d13436138eea826383e822449c9a5c50ee44665a" - integrity sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA== - dependencies: - debug "^4.0.0" - parse-entities "^2.0.0" - -micromatch@^4.0.4, micromatch@^4.0.5: - version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -min-document@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" - integrity sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ== - dependencies: - dom-walk "^0.1.0" - -min-indent@^1.0.0, min-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" - integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== - -minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.3.tgz#b4dcece1d674dee104bb0fb833ebb85a78cbbca6" - integrity sha512-lIUdtK5hdofgCTu3aT0sOaHsYR37viUuIc0rwnnDXImbwFRcumyLMeZaM0t0I/fgxS6s6JMfu0rLD1Wz9pv1ng== - dependencies: - brace-expansion "^1.1.7" - -minimist-options@4.1.0, minimist-options@^4.0.2: - version "4.1.0" - resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" - integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== - dependencies: - arrify "^1.0.1" - is-plain-obj "^1.1.0" - kind-of "^6.0.3" - -minimist@^1.2.0, minimist@^1.2.6: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -mkdirp@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -moment@^2.30.1: - version "2.30.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" - integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== - -moment@~2.29.1: - version "2.29.4" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" - integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== - -mousetrap-pause@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/mousetrap-pause/-/mousetrap-pause-1.0.0.tgz#91c429f2f5f9ad71508fa0561bb53be3fdf9a9d0" - integrity sha512-/92qasq/TIkogCZKRYZdX+XAiPOD8dBDIipaar+caXSdKrfhYQIe6UmweiXO9yQeETjhNAUWdopwLsU6po/IPw== - -mousetrap@^1.6.5: - version "1.6.5" - resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" - integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA== - -mpd-parser@0.22.1, mpd-parser@^0.22.1: - version "0.22.1" - resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.22.1.tgz#bc2bf7d3e56368e4b0121035b055675401871521" - integrity sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q== - dependencies: - "@babel/runtime" "^7.12.5" - "@videojs/vhs-utils" "^3.0.5" - "@xmldom/xmldom" "^0.8.3" - global "^4.4.0" - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -mute-stream@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" - integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== - -mux.js@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/mux.js/-/mux.js-6.0.1.tgz#65ce0f7a961d56c006829d024d772902d28c7755" - integrity sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w== - dependencies: - "@babel/runtime" "^7.11.2" - global "^4.4.0" - -nanoid@^3.3.6: - version "3.3.8" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" - integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== - -natural-compare-lite@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" - integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -no-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" - integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== - dependencies: - lower-case "^2.0.2" - tslib "^2.0.3" - -node-fetch@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -node-fetch@^2.6.1: - version "2.6.9" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" - integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== - dependencies: - whatwg-url "^5.0.0" - -node-int64@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" - integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== - -node-releases@^2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" - integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== - -normalize-package-data@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-package-data@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" - integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== - dependencies: - hosted-git-info "^4.0.1" - is-core-module "^2.5.0" - semver "^7.3.4" - validate-npm-package-license "^3.0.1" - -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w== - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-url@^4.5.1: - version "4.5.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" - integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== - -nosleep.js@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/nosleep.js/-/nosleep.js-0.7.0.tgz#cfd919c25523ca0d0f4a69fb3305c083adaee289" - integrity sha512-Z4B1HgvzR+en62ghwZf6BwAR6x4/pjezsiMcbF9KMLh7xoscpoYhaSXfY3lLkqC68AtW+/qLJ1lzvBIj0FGaTA== - -nullthrows@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" - integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== - -object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.12.2, object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== - -object-is@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.2, object.assign@^4.1.3, object.assign@^4.1.4: - version "4.1.4" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" - integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - has-symbols "^1.0.3" - object-keys "^1.1.1" - -object.entries@^1.1.5, object.entries@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.6.tgz#9737d0e5b8291edd340a3e3264bb8a3b00d5fa23" - integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - -object.fromentries@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73" - integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - -object.hasown@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.2.tgz#f919e21fad4eb38a57bc6345b3afd496515c3f92" - integrity sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw== - dependencies: - define-properties "^1.1.4" - es-abstract "^1.20.4" - -object.values@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" - integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -onetime@^5.1.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -optimism@^0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.18.0.tgz#e7bb38b24715f3fdad8a9a7fc18e999144bbfa63" - integrity sha512-tGn8+REwLRNFnb9WmcY5IfpOqeX2kpaYJ1s6Ae3mn12AeydLkR3j+jSCmVQFoXqU8D41PAJ1RG1rCRNWmNZVmQ== - dependencies: - "@wry/caches" "^1.0.0" - "@wry/context" "^0.7.0" - "@wry/trie" "^0.4.3" - tslib "^2.3.0" - -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== - dependencies: - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - word-wrap "^1.2.3" - -ora@^5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" - integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== - dependencies: - bl "^4.1.0" - chalk "^4.1.0" - cli-cursor "^3.1.0" - cli-spinners "^2.5.0" - is-interactive "^1.0.0" - is-unicode-supported "^0.1.0" - log-symbols "^4.1.0" - strip-ansi "^6.0.0" - wcwidth "^1.0.1" - -os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== - -p-limit@3.1.0, p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -param-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" - integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-entities@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" - integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ== - dependencies: - character-entities "^1.0.0" - character-entities-legacy "^1.0.0" - character-reference-invalid "^1.0.0" - is-alphanumerical "^1.0.0" - is-decimal "^1.0.0" - is-hexadecimal "^1.0.0" - -parse-filepath@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" - integrity sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q== - dependencies: - is-absolute "^1.0.0" - map-cache "^0.2.0" - path-root "^0.1.1" - -parse-json@^5.0.0, parse-json@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -pascal-case@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" - integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - -path-browserify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" - integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== - -path-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/path-case/-/path-case-3.0.4.tgz#9168645334eb942658375c56f80b4c0cb5f82c6f" - integrity sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-root-regex@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" - integrity sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ== - -path-root@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" - integrity sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg== - dependencies: - path-root-regex "^0.1.0" - -path-to-regexp@^1.7.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.9.0.tgz#5dc0753acbf8521ca2e0f137b4578b917b10cf24" - integrity sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g== - dependencies: - isarray "0.0.1" - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -pify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" - integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== - -pkcs7@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/pkcs7/-/pkcs7-1.0.4.tgz#6090b9e71160dabf69209d719cbafa538b00a1cb" - integrity sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ== - dependencies: - "@babel/runtime" "^7.5.5" - -postcss-resolve-nested-selector@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" - integrity sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw== - -postcss-safe-parser@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz#bb4c29894171a94bc5c996b9a30317ef402adaa1" - integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ== - -postcss-scss@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.6.tgz#5d62a574b950a6ae12f2aa89b60d63d9e4432bfd" - integrity sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ== - -postcss-selector-parser@^6.0.13: - version "6.0.13" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" - integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-sorting@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/postcss-sorting/-/postcss-sorting-8.0.1.tgz#d03852914979ac0a1ef3ca6e517bf4b53c045f35" - integrity sha512-go9Zoxx7KQH+uLrJ9xa5wRErFeXu01ydA6O8m7koPXkmAN7Ts//eRcIqjo0stBR4+Nir2gMYDOWAOx7O5EPUZA== - -postcss-value-parser@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - -postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.27, postcss@^8.4.31: - version "8.4.31" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" - integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -prettier@^2.8.4: - version "2.8.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" - integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== - -promise@^7.1.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" - integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== - dependencies: - asap "~2.0.3" - -prop-types-extra@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.1.tgz#58c3b74cbfbb95d304625975aa2f0848329a010b" - integrity sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew== - dependencies: - react-is "^16.3.2" - warning "^4.0.0" - -prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: - version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" - -prop-types@~15.7.2: - version "15.7.2" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" - integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.8.1" - -property-expr@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4" - integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA== - -property-information@^5.3.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" - integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA== - dependencies: - xtend "^4.0.0" - -punycode@^1.3.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== - -punycode@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== - -pvtsutils@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.2.tgz#9f8570d132cdd3c27ab7d51a2799239bf8d8d5de" - integrity sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ== - dependencies: - tslib "^2.4.0" - -pvutils@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3" - integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -quick-lru@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" - integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== - -quick-lru@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" - integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== - -react-bootstrap@^1.6.6: - version "1.6.6" - resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-1.6.6.tgz#3f3b274f8923b9886008a0e61485b5ac9a2b3073" - integrity sha512-pSzYyJT5u4rc8+5myM8Vid2JG52L8AmYSkpznReH/GM4+FhLqEnxUa0+6HRTaGwjdEixQNGchwY+b3xCdYWrDA== - dependencies: - "@babel/runtime" "^7.14.0" - "@restart/context" "^2.1.4" - "@restart/hooks" "^0.4.7" - "@types/invariant" "^2.2.33" - "@types/prop-types" "^15.7.3" - "@types/react" ">=16.14.8" - "@types/react-transition-group" "^4.4.1" - "@types/warning" "^3.0.0" - classnames "^2.3.1" - dom-helpers "^5.2.1" - invariant "^2.2.4" - prop-types "^15.7.2" - prop-types-extra "^1.1.0" - react-overlays "^5.1.2" - react-transition-group "^4.4.1" - uncontrollable "^7.2.1" - warning "^4.0.3" - -react-datepicker@^4.10.0: - version "4.10.0" - resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.10.0.tgz#3f386ac5873dac5ea56544e51cdc01109938796c" - integrity sha512-6IfBCZyWj54ZZGLmEZJ9c4Yph0s9MVfEGDC2evOvf9AmVz+RRcfP2Czqad88Ff9wREbcbqa4dk7IFYeXF1d3Ag== - dependencies: - "@popperjs/core" "^2.9.2" - classnames "^2.2.6" - date-fns "^2.24.0" - prop-types "^15.7.2" - react-onclickoutside "^6.12.2" - react-popper "^2.3.0" - -react-dom@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" - -react-fast-compare@^2.0.1: - version "2.0.4" - resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" - integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== - -react-fast-compare@^3.0.1, react-fast-compare@^3.1.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.1.tgz#53933d9e14f364281d6cba24bfed7a4afb808b5f" - integrity sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg== - -react-helmet@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726" - integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw== - dependencies: - object-assign "^4.1.1" - prop-types "^15.7.2" - react-fast-compare "^3.1.1" - react-side-effect "^2.1.0" - -react-intl@^6.2.8: - version "6.2.8" - resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-6.2.8.tgz#f61fffc14e69490607d3be9253704ac5afc49d56" - integrity sha512-Njzmbmk58rBx6i0bGQbBLYj+KbR9IXbFfbK2u0AFayjDx+VJW30MdJV6aNL9EiPaXfcOcAYm31R777e/UHWeEw== - dependencies: - "@formatjs/ecma402-abstract" "1.14.3" - "@formatjs/icu-messageformat-parser" "2.2.0" - "@formatjs/intl" "2.6.5" - "@formatjs/intl-displaynames" "6.2.4" - "@formatjs/intl-listformat" "7.1.7" - "@types/hoist-non-react-statics" "^3.3.1" - "@types/react" "16 || 17 || 18" - hoist-non-react-statics "^3.3.2" - intl-messageformat "10.3.0" - tslib "^2.4.0" - -react-is@^16.13.1, react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-lifecycles-compat@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - -react-onclickoutside@^6.12.2: - version "6.12.2" - resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz#8e6cf80c7d17a79f2c908399918158a7b02dda01" - integrity sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA== - -react-overlays@^5.1.2: - version "5.2.1" - resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-5.2.1.tgz#49dc007321adb6784e1f212403f0fb37a74ab86b" - integrity sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA== - dependencies: - "@babel/runtime" "^7.13.8" - "@popperjs/core" "^2.11.6" - "@restart/hooks" "^0.4.7" - "@types/warning" "^3.0.0" - dom-helpers "^5.2.0" - prop-types "^15.7.2" - uncontrollable "^7.2.1" - warning "^4.0.3" - -react-photo-gallery@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/react-photo-gallery/-/react-photo-gallery-8.0.0.tgz#04ff9f902a2342660e63e6817b4f010488db02b8" - integrity sha512-Y9458yygEB9cIZAWlBWuenlR+ghin1RopmmU3Vice8BeJl0Se7hzfxGDq8W1armB/ic/kphGg+G1jq5fOEd0sw== - dependencies: - prop-types "~15.7.2" - resize-observer-polyfill "^1.5.0" - -react-popper@^2.2.5, react-popper@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.3.0.tgz#17891c620e1320dce318bad9fede46a5f71c70ba" - integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q== - dependencies: - react-fast-compare "^3.0.1" - warning "^4.0.2" - -react-refresh@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" - integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== - -react-remark@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/react-remark/-/react-remark-2.1.0.tgz#dd68a32ab2d022e598b27dbfb754400e8f68555c" - integrity sha512-7dEPxRGQ23sOdvteuRGaQAs9cEOH/BOeCN4CqsJdk3laUDIDYRCWnM6a3z92PzXHUuxIRLXQNZx7SiO0ijUcbw== - dependencies: - rehype-react "^6.0.0" - remark-parse "^9.0.0" - remark-rehype "^8.0.0" - unified "^9.0.0" - -react-router-bootstrap@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/react-router-bootstrap/-/react-router-bootstrap-0.25.0.tgz#5d1a99b5b8a2016c011fc46019d2397e563ce0df" - integrity sha512-/22eqxjn6Zv5fvY2rZHn57SKmjmJfK7xzJ6/G1OgxAjLtKVfWgV5sn41W2yiqzbtV5eE4/i4LeDLBGYTqx7jbA== - dependencies: - prop-types "^15.5.10" - -react-router-dom@^5.3.4: - version "5.3.4" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.4.tgz#2ed62ffd88cae6db134445f4a0c0ae8b91d2e5e6" - integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ== - dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - loose-envify "^1.3.1" - prop-types "^15.6.2" - react-router "5.3.4" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - -react-router-hash-link@^2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/react-router-hash-link/-/react-router-hash-link-2.4.3.tgz#570824d53d6c35ce94d73a46c8e98673a127bf08" - integrity sha512-NU7GWc265m92xh/aYD79Vr1W+zAIXDWp3L2YZOYP4rCqPnJ6LI6vh3+rKgkidtYijozHclaEQTAHaAaMWPVI4A== - dependencies: - prop-types "^15.7.2" - -react-router@5.3.4: - version "5.3.4" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.4.tgz#8ca252d70fcc37841e31473c7a151cf777887bb5" - integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== - dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - hoist-non-react-statics "^3.1.0" - loose-envify "^1.3.1" - path-to-regexp "^1.7.0" - prop-types "^15.6.2" - react-is "^16.6.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - -react-select@^5.7.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.7.0.tgz#82921b38f1fcf1471a0b62304da01f2896cd8ce6" - integrity sha512-lJGiMxCa3cqnUr2Jjtg9YHsaytiZqeNOKeibv6WF5zbK/fPegZ1hg3y/9P1RZVLhqBTs0PfqQLKuAACednYGhQ== - dependencies: - "@babel/runtime" "^7.12.0" - "@emotion/cache" "^11.4.0" - "@emotion/react" "^11.8.1" - "@floating-ui/dom" "^1.0.1" - "@types/react-transition-group" "^4.4.0" - memoize-one "^6.0.0" - prop-types "^15.6.0" - react-transition-group "^4.3.0" - use-isomorphic-layout-effect "^1.1.2" - -react-side-effect@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.2.tgz#dc6345b9e8f9906dc2eeb68700b615e0b4fe752a" - integrity sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw== - -react-transition-group@^4.3.0, react-transition-group@^4.4.1: - version "4.4.5" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" - integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== - dependencies: - "@babel/runtime" "^7.5.5" - dom-helpers "^5.0.1" - loose-envify "^1.4.0" - prop-types "^15.6.2" - -react@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -read-babelrc-up@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-babelrc-up/-/read-babelrc-up-1.1.0.tgz#10fd5baaf6ca03eaba6748fa65ddae25bca61e70" - integrity sha512-fcl0JeI85Ss3//kfC3z2rsG2VxSiHl1bJgpjQWrne2YuQEewZpAgAjb17A6q/Q3ozWeZsUSroiIBVsnjmOU8vw== - dependencies: - find-up "^4.1.0" - json5 "^2.1.2" - -read-pkg-up@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" - integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== - dependencies: - find-up "^4.1.0" - read-pkg "^5.2.0" - type-fest "^0.8.1" - -read-pkg-up@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-8.0.0.tgz#72f595b65e66110f43b052dd9af4de6b10534670" - integrity sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ== - dependencies: - find-up "^5.0.0" - read-pkg "^6.0.0" - type-fest "^1.0.1" - -read-pkg@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" - integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== - dependencies: - "@types/normalize-package-data" "^2.4.0" - normalize-package-data "^2.5.0" - parse-json "^5.0.0" - type-fest "^0.6.0" - -read-pkg@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-6.0.0.tgz#a67a7d6a1c2b0c3cd6aa2ea521f40c458a4a504c" - integrity sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q== - dependencies: - "@types/normalize-package-data" "^2.4.0" - normalize-package-data "^3.0.2" - parse-json "^5.2.0" - type-fest "^1.0.1" - -readable-stream@^3.4.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -redent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" - integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== - dependencies: - indent-string "^4.0.0" - strip-indent "^3.0.0" - -redent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-4.0.0.tgz#0c0ba7caabb24257ab3bb7a4fd95dd1d5c5681f9" - integrity sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag== - dependencies: - indent-string "^5.0.0" - strip-indent "^4.0.0" - -regenerate-unicode-properties@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" - integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== - dependencies: - regenerate "^1.4.2" - -regenerate@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.13.11: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - -regenerator-transform@^0.15.1: - version "0.15.1" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" - integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg== - dependencies: - "@babel/runtime" "^7.8.4" - -regexp.prototype.flags@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" - integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - functions-have-names "^1.2.2" - -regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - -regexpu-core@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.1.tgz#66900860f88def39a5cb79ebd9490e84f17bcdfb" - integrity sha512-nCOzW2V/X15XpLsK2rlgdwrysrBq+AauCn+omItIz4R1pIcmeot5zvjdmOBRLzEH/CkC6IxMJVmxDe3QcMuNVQ== - dependencies: - "@babel/regjsgen" "^0.8.0" - regenerate "^1.4.2" - regenerate-unicode-properties "^10.1.0" - regjsparser "^0.9.1" - unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.1.0" - -regjsparser@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" - integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== - dependencies: - jsesc "~0.5.0" - -rehype-react@^6.0.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/rehype-react/-/rehype-react-6.2.1.tgz#9b9bf188451ad6f63796b784fe1f51165c67b73a" - integrity sha512-f9KIrjktvLvmbGc7si25HepocOg4z0MuNOtweigKzBcDjiGSTGhyz6VSgaV5K421Cq1O+z4/oxRJ5G9owo0KVg== - dependencies: - "@mapbox/hast-util-table-cell-style" "^0.2.0" - hast-to-hyperscript "^9.0.0" - -relay-runtime@12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/relay-runtime/-/relay-runtime-12.0.0.tgz#1e039282bdb5e0c1b9a7dc7f6b9a09d4f4ff8237" - integrity sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug== - dependencies: - "@babel/runtime" "^7.0.0" - fbjs "^3.0.0" - invariant "^2.2.4" - -remark-gfm@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-1.0.0.tgz#9213643001be3f277da6256464d56fd28c3b3c0d" - integrity sha512-KfexHJCiqvrdBZVbQ6RopMZGwaXz6wFJEfByIuEwGf0arvITHjiKKZ1dpXujjH9KZdm1//XJQwgfnJ3lmXaDPA== - dependencies: - mdast-util-gfm "^0.1.0" - micromark-extension-gfm "^0.3.0" - -remark-parse@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-9.0.0.tgz#4d20a299665880e4f4af5d90b7c7b8a935853640" - integrity sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw== - dependencies: - mdast-util-from-markdown "^0.8.0" - -remark-rehype@^8.0.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-8.1.0.tgz#610509a043484c1e697437fa5eb3fd992617c945" - integrity sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA== - dependencies: - mdast-util-to-hast "^10.2.0" - -remedial@^1.0.7: - version "1.0.8" - resolved "https://registry.yarnpkg.com/remedial/-/remedial-1.0.8.tgz#a5e4fd52a0e4956adbaf62da63a5a46a78c578a0" - integrity sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg== - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw== - -remove-trailing-spaces@^1.0.6: - version "1.0.8" - resolved "https://registry.yarnpkg.com/remove-trailing-spaces/-/remove-trailing-spaces-1.0.8.tgz#4354d22f3236374702f58ee373168f6d6887ada7" - integrity sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA== - -repeat-string@^1.0.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" - integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== - -resolve-from@5.0.0, resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-pathname@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" - integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== - -resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.1: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -resolve@^2.0.0-next.4: - version "2.0.0-next.4" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660" - integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== - dependencies: - is-core-module "^2.9.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -response-iterator@^0.2.6: - version "0.2.6" - resolved "https://registry.yarnpkg.com/response-iterator/-/response-iterator-0.2.6.tgz#249005fb14d2e4eeb478a3f735a28fd8b4c9f3da" - integrity sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw== - -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rfdc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" - integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -rollup@^3.27.1: - version "3.29.5" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.5.tgz#8a2e477a758b520fb78daf04bca4c522c1da8a54" - integrity sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w== - optionalDependencies: - fsevents "~2.3.2" - -run-async@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -rust-result@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/rust-result/-/rust-result-1.0.0.tgz#34c75b2e6dc39fe5875e5bdec85b5e0f91536f72" - integrity sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA== - dependencies: - individual "^2.0.0" - -rxjs@^7.5.5: - version "7.8.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" - integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== - dependencies: - tslib "^2.1.0" - -safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-json-parse@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/safe-json-parse/-/safe-json-parse-4.0.0.tgz#7c0f578cfccd12d33a71c0e05413e2eca171eaac" - integrity sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ== - dependencies: - rust-result "^1.0.0" - -safe-regex-test@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" - integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" - is-regex "^1.1.4" - -"safer-buffer@>= 2.1.2 < 3": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sass@^1.58.1: - version "1.58.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.58.1.tgz#17ab0390076a50578ed0733f1cc45429e03405f6" - integrity sha512-bnINi6nPXbP1XNRaranMFEBZWUfdW/AF16Ql5+ypRxfTvCRTTKrLsMIakyDcayUt2t/RZotmL4kgJwNH5xO+bg== - dependencies: - chokidar ">=3.0.0 <4.0.0" - immutable "^4.0.0" - source-map-js ">=0.6.2 <2.0.0" - -sax@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" - integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA== - -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -schema-utils@*: - version "4.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" - integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== - dependencies: - "@types/json-schema" "^7.0.9" - ajv "^8.8.0" - ajv-formats "^2.1.1" - ajv-keywords "^5.0.0" - -schema-utils@^2.6.6: - version "2.7.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" - integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== - dependencies: - "@types/json-schema" "^7.0.5" - ajv "^6.12.4" - ajv-keywords "^3.5.2" - -scuid@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/scuid/-/scuid-1.1.0.tgz#d3f9f920956e737a60f72d0e4ad280bf324d5dab" - integrity sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg== - -"semver@2 || 3 || 4 || 5": - version "5.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" - integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== - -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.3.4, semver@^7.3.7: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" - -sentence-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-3.0.4.tgz#3645a7b8c117c787fde8702056225bb62a45131f" - integrity sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - upper-case-first "^2.0.2" - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== - -setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -shell-quote@^1.7.3: - version "1.8.0" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.0.tgz#20d078d0eaf71d54f43bd2ba14a1b5b9bfa5c8ba" - integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ== - -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== - dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" - -signal-exit@^3.0.2: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -signal-exit@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967" - integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== - -signedsource@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/signedsource/-/signedsource-1.0.0.tgz#1ddace4981798f93bd833973803d80d52e93ad6a" - integrity sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww== - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -slice-ansi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" - integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -slick-carousel@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/slick-carousel/-/slick-carousel-1.8.1.tgz#a4bfb29014887bb66ce528b90bd0cda262cc8f8d" - integrity sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA== - -snake-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" - integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" - -sort-keys@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-4.2.0.tgz#6b7638cee42c506fff8c1cecde7376d21315be18" - integrity sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg== - dependencies: - is-plain-obj "^2.0.0" - -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== - -source-map-support@~0.5.20: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.5.7: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== - -source-map@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -space-separated-tokens@^1.0.0: - version "1.1.5" - resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" - integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== - -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.12" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz#69077835abe2710b65f03969898b6637b505a779" - integrity sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA== - -sponge-case@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sponge-case/-/sponge-case-1.0.1.tgz#260833b86453883d974f84854cdb63aecc5aef4c" - integrity sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA== - dependencies: - tslib "^2.0.3" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - -stop-iteration-iterator@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" - integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== - dependencies: - internal-slot "^1.0.4" - -streamsearch@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" - integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== - -string-convert@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97" - integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A== - -string-env-interpolation@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz#ad4397ae4ac53fe6c91d1402ad6f6a52862c7152" - integrity sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg== - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string.prototype.matchall@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3" - integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - get-intrinsic "^1.1.3" - has-symbols "^1.0.3" - internal-slot "^1.0.3" - regexp.prototype.flags "^1.4.3" - side-channel "^1.0.4" - -string.prototype.replaceall@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/string.prototype.replaceall/-/string.prototype.replaceall-1.0.7.tgz#6cf36b20bcb12d55653e1119ddf5bc1d6363103d" - integrity sha512-xB2WV2GlSCSJT5dMGdhdH1noMPiAB91guiepwTYyWY9/0Vq/TZ7RPmnOSUGAEvry08QIK7EMr28aAii+9jC6kw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - get-intrinsic "^1.1.3" - has-symbols "^1.0.3" - is-regex "^1.1.4" - -string.prototype.trimend@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" - integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - -string.prototype.trimstart@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" - integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== - -strip-bom@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" - integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== - -strip-indent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" - integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== - dependencies: - min-indent "^1.0.0" - -strip-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-4.0.0.tgz#b41379433dd06f5eae805e21d631e07ee670d853" - integrity sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA== - dependencies: - min-indent "^1.0.1" - -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -style-search@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" - integrity sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg== - -style-to-object@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46" - integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA== - dependencies: - inline-style-parser "0.1.1" - -stylelint-order@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/stylelint-order/-/stylelint-order-6.0.2.tgz#df54d3ed9aa5a45d4563ada0375e670140a798c2" - integrity sha512-yuac0BE6toHd27wUPvYVVQicAJthKFIv1HPQFH3Q0dExiO3Z6Uam7geoO0tUd5Z9ddsATYK++1qWNDX4RxMH5Q== - dependencies: - postcss "^8.4.21" - postcss-sorting "^8.0.1" - -stylelint@^15.10.1: - version "15.10.1" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.10.1.tgz#93f189958687e330c106b010cbec0c41dcae506d" - integrity sha512-CYkzYrCFfA/gnOR+u9kJ1PpzwG10WLVnoxHDuBA/JiwGqdM9+yx9+ou6SE/y9YHtfv1mcLo06fdadHTOx4gBZQ== - dependencies: - "@csstools/css-parser-algorithms" "^2.3.0" - "@csstools/css-tokenizer" "^2.1.1" - "@csstools/media-query-list-parser" "^2.1.2" - "@csstools/selector-specificity" "^3.0.0" - balanced-match "^2.0.0" - colord "^2.9.3" - cosmiconfig "^8.2.0" - css-functions-list "^3.1.0" - css-tree "^2.3.1" - debug "^4.3.4" - fast-glob "^3.3.0" - fastest-levenshtein "^1.0.16" - file-entry-cache "^6.0.1" - global-modules "^2.0.0" - globby "^11.1.0" - globjoin "^0.1.4" - html-tags "^3.3.1" - ignore "^5.2.4" - import-lazy "^4.0.0" - imurmurhash "^0.1.4" - is-plain-object "^5.0.0" - known-css-properties "^0.27.0" - mathml-tag-names "^2.1.3" - meow "^10.1.5" - micromatch "^4.0.5" - normalize-path "^3.0.0" - picocolors "^1.0.0" - postcss "^8.4.24" - postcss-resolve-nested-selector "^0.1.1" - postcss-safe-parser "^6.0.0" - postcss-selector-parser "^6.0.13" - postcss-value-parser "^4.2.0" - resolve-from "^5.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - style-search "^0.1.0" - supports-hyperlinks "^3.0.0" - svg-tags "^1.0.0" - table "^6.8.1" - write-file-atomic "^5.0.1" - -stylis@4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7" - integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.0.0, supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-hyperlinks@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz#c711352a5c89070779b4dad54c05a2f14b15c94b" - integrity sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA== - dependencies: - has-flag "^4.0.0" - supports-color "^7.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -svg-tags@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" - integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA== - -swap-case@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/swap-case/-/swap-case-2.0.2.tgz#671aedb3c9c137e2985ef51c51f9e98445bf70d9" - integrity sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw== - dependencies: - tslib "^2.0.3" - -symbol-observable@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" - integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== - -systemjs@^6.13.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.13.0.tgz#7b28e74b44352e1650e8652499f42de724c3fc7f" - integrity sha512-P3cgh2bpaPvAO2NE3uRp/n6hmk4xPX4DQf+UzTlCAycssKdqhp6hjw+ENWe+aUS7TogKRFtptMosTSFeC6R55g== - -table@^6.8.1: - version "6.8.1" - resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" - integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== - dependencies: - ajv "^8.0.1" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - -terser@^5.9.0: - version "5.16.4" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.4.tgz#51284b440b93242291a98f2a9903c024cfb70e6e" - integrity sha512-5yEGuZ3DZradbogeYQ1NaGz7rXVBDWujWlx1PT8efXO6Txn+eWbfKqB2bTDVmFXmePFkoLU6XI8UektMIEA0ug== - dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" - commander "^2.20.0" - source-map-support "~0.5.20" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - -thehandy@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/thehandy/-/thehandy-1.0.3.tgz#51c5e9bae5932a6e5c563203711d78610b99d402" - integrity sha512-zuuyWKBx/jqku9+MZkdkoK2oLM2mS8byWVR/vkQYq/ygAT6gPAXwiT94rfGuqv+1BLmsyJxm69nhVIzOZjfyIg== - -three@0.93.0: - version "0.93.0" - resolved "https://registry.yarnpkg.com/three/-/three-0.93.0.tgz#3fd6c367ef4554abbb6e16ad69936283e895c123" - integrity sha512-Ys9+UBBsd6FxTZZl4BH7B4b2F+B2uR0cOwY7OQ/aCzU/VgO4Wmmr1LbWPH1fsTvSVik9KAuwxwOHlSC4IMGOLA== - -throttle-debounce@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-5.0.0.tgz#a17a4039e82a2ed38a5e7268e4132d6960d41933" - integrity sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg== - -through@^2.3.6, through@^2.3.8: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - -tiny-case@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" - integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== - -tiny-invariant@^1.0.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" - integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== - -tiny-warning@^1.0.0, tiny-warning@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - -title-case@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/title-case/-/title-case-3.0.3.tgz#bc689b46f02e411f1d1e1d081f7c3deca0489982" - integrity sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA== - dependencies: - tslib "^2.0.3" - -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toposort@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" - integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -trim-newlines@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" - integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== - -trim-newlines@^4.0.2: - version "4.1.1" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.1.1.tgz#28c88deb50ed10c7ba6dc2474421904a00139125" - integrity sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ== - -trough@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" - integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== - -ts-invariant@^0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.10.3.tgz#3e048ff96e91459ffca01304dbc7f61c1f642f6c" - integrity sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ== - dependencies: - tslib "^2.1.0" - -ts-log@^2.2.3: - version "2.2.5" - resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.5.tgz#aef3252f1143d11047e2cb6f7cfaac7408d96623" - integrity sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA== - -ts-node@^10.9.1: - version "10.9.1" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" - integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - -tsconfck@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-2.0.2.tgz#860a488f9acd024a85b2db458888410009b5383d" - integrity sha512-H3DWlwKpow+GpVLm/2cpmok72pwRr1YFROV3YzAmvzfGFiC1zEM/mc9b7+1XnrxuXtEbhJ7xUSIqjPFbedp7aQ== - -tsconfig-paths@^3.14.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" - integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== - dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.1" - minimist "^1.2.6" - strip-bom "^3.0.0" - -tslib@^1.8.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0, tslib@~2.6.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== - -tslib@~2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" - integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== - -tslib@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" - integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== - -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-fest@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" - integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - -type-fest@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" - integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== - -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== - -type-fest@^1.0.1, type-fest@^1.2.1, type-fest@^1.2.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" - integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== - -type-fest@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" - integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== - -typed-array-length@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" - integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== - dependencies: - call-bind "^1.0.2" - for-each "^0.3.3" - is-typed-array "^1.1.9" - -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - -typescript@^4.0, typescript@~4.8.4: - version "4.8.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" - integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== - -ua-parser-js@^0.7.30: - version "0.7.33" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532" - integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw== - -ua-parser-js@^1.0.2, ua-parser-js@^1.0.34: - version "1.0.34" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.34.tgz#b33f41c415325839f354005d25a2f588be296976" - integrity sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew== - -unbox-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" - integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== - dependencies: - call-bind "^1.0.2" - has-bigints "^1.0.2" - has-symbols "^1.0.3" - which-boxed-primitive "^1.0.2" - -unc-path-regex@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" - integrity sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg== - -uncontrollable@^7.2.1: - version "7.2.1" - resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.2.1.tgz#1fa70ba0c57a14d5f78905d533cf63916dc75738" - integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ== - dependencies: - "@babel/runtime" "^7.6.3" - "@types/react" ">=16.9.11" - invariant "^2.2.4" - react-lifecycles-compat "^3.0.4" - -unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== - -unicode-match-property-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" - integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== - dependencies: - unicode-canonical-property-names-ecmascript "^2.0.0" - unicode-property-aliases-ecmascript "^2.0.0" - -unicode-match-property-value-ecmascript@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" - integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== - -unicode-property-aliases-ecmascript@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" - integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== - -unified@^9.0.0: - version "9.2.2" - resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975" - integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ== - dependencies: - bail "^1.0.0" - extend "^3.0.0" - is-buffer "^2.0.0" - is-plain-obj "^2.0.0" - trough "^1.0.0" - vfile "^4.0.0" - -unist-builder@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-2.0.3.tgz#77648711b5d86af0942f334397a33c5e91516436" - integrity sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw== - -unist-util-generated@^1.0.0: - version "1.1.6" - resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.6.tgz#5ab51f689e2992a472beb1b35f2ce7ff2f324d4b" - integrity sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg== - -unist-util-is@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-3.0.0.tgz#d9e84381c2468e82629e4a5be9d7d05a2dd324cd" - integrity sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A== - -unist-util-is@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.1.0.tgz#976e5f462a7a5de73d94b706bac1b90671b57797" - integrity sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg== - -unist-util-position@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.1.0.tgz#1c42ee6301f8d52f47d14f62bbdb796571fa2d47" - integrity sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA== - -unist-util-stringify-position@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da" - integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g== - dependencies: - "@types/unist" "^2.0.2" - -unist-util-visit-parents@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz#25e43e55312166f3348cae6743588781d112c1e9" - integrity sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g== - dependencies: - unist-util-is "^3.0.0" - -unist-util-visit-parents@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz#65a6ce698f78a6b0f56aa0e88f13801886cdaef6" - integrity sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^4.0.0" - -unist-util-visit@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3" - integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw== - dependencies: - unist-util-visit-parents "^2.0.0" - -unist-util-visit@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c" - integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^4.0.0" - unist-util-visit-parents "^3.0.0" - -universal-cookie@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-4.0.4.tgz#06e8b3625bf9af049569ef97109b4bb226ad798d" - integrity sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw== - dependencies: - "@types/cookie" "^0.3.3" - cookie "^0.4.0" - -universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== - -unixify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unixify/-/unixify-1.0.0.tgz#3a641c8c2ffbce4da683a5c70f03a462940c2090" - integrity sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg== - dependencies: - normalize-path "^2.1.1" - -update-browserslist-db@^1.0.13: - version "1.0.13" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" - integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - -upper-case-first@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-2.0.2.tgz#992c3273f882abd19d1e02894cc147117f844324" - integrity sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg== - dependencies: - tslib "^2.0.3" - -upper-case@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-2.0.2.tgz#d89810823faab1df1549b7d97a76f8662bae6f7a" - integrity sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg== - dependencies: - tslib "^2.0.3" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -url-toolkit@^2.2.1: - version "2.2.5" - resolved "https://registry.yarnpkg.com/url-toolkit/-/url-toolkit-2.2.5.tgz#58406b18e12c58803e14624df5e374f638b0f607" - integrity sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg== - -urlpattern-polyfill@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz#f0a03a97bfb03cdf33553e5e79a2aadd22cac8ec" - integrity sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg== - -urlpattern-polyfill@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-6.0.2.tgz#a193fe773459865a2a5c93b246bb794b13d07256" - integrity sha512-5vZjFlH9ofROmuWmXM9yj2wljYKgWstGwe8YTyiqM7hVum/g9LyCizPZtb3UqsuppVwety9QJmfc42VggLpTgg== - dependencies: - braces "^3.0.2" - -use-isomorphic-layout-effect@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" - integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== - -util-deprecate@^1.0.1, util-deprecate@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -value-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" - integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== - -value-or-promise@^1.0.11, value-or-promise@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.12.tgz#0e5abfeec70148c78460a849f6b003ea7986f15c" - integrity sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q== - -vfile-message@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a" - integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ== - dependencies: - "@types/unist" "^2.0.0" - unist-util-stringify-position "^2.0.0" - -vfile@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624" - integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA== - dependencies: - "@types/unist" "^2.0.0" - is-buffer "^2.0.0" - unist-util-stringify-position "^2.0.0" - vfile-message "^2.0.0" - -"video.js@^5.18.0 || ^6 || ^7", "video.js@^6 || ^7", video.js@^7.21.3: - version "7.21.3" - resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.21.3.tgz#1a5f6379e713de3f5dc036ecdef02efb80765bdd" - integrity sha512-fIboXbSDCT3P8eVzIEC3hnLDKC/y+6QftcHdFGUVGn5a7qmH62Mh0Bt/SrBAgdmKDQM1qdZXfXAxPg5+IaiIXQ== - dependencies: - "@babel/runtime" "^7.12.5" - "@videojs/http-streaming" "2.16.2" - "@videojs/vhs-utils" "^3.0.4" - "@videojs/xhr" "2.6.0" - aes-decrypter "3.1.3" - global "^4.4.0" - keycode "^2.2.0" - m3u8-parser "4.8.0" - mpd-parser "0.22.1" - mux.js "6.0.1" - safe-json-parse "4.0.0" - videojs-font "3.2.0" - videojs-vtt.js "^0.15.4" - -videojs-abloop@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/videojs-abloop/-/videojs-abloop-1.2.0.tgz#ead4054400e6107d6512553ddff2a97260decf3e" - integrity sha512-6/hvtB5gNQUr5FJ969UhXVg5H+3wxhOzh9AVftlezOXlhzzaWfNfiOJYqNKo01Gc/eSQOvfttrOX7jH+aHpwrw== - -videojs-contrib-dash@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/videojs-contrib-dash/-/videojs-contrib-dash-5.1.1.tgz#9f50191677815a7d816c500977811a926aee0643" - integrity sha512-MI0kPHuQ3KH9Mc2mLVLqvFKCoEyTfXzHc02fm8pqMk8v7LXrJKnIv9xfugBccRF7vZHDZISftedD/CmEJfvvrA== - dependencies: - dashjs "^4.2.0" - global "^4.3.2" - video.js "^5.18.0 || ^6 || ^7" - -videojs-font@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/videojs-font/-/videojs-font-3.2.0.tgz#212c9d3f4e4ec3fa7345167d64316add35e92232" - integrity sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA== - -videojs-mobile-ui@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/videojs-mobile-ui/-/videojs-mobile-ui-0.8.0.tgz#40a1c6f9302071b9bbe95937c934114600916ac5" - integrity sha512-Jd+u/ctjUkbZlT1cAA0umTu0LQwSZSFG+02cJxShuwq27B6rfrRALETK/gsuTc7U27lB9fbwcF7HBMaNxW62nA== - dependencies: - global "^4.4.0" - -videojs-seek-buttons@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/videojs-seek-buttons/-/videojs-seek-buttons-3.0.1.tgz#cc2adc23a6372e8aa6c2e9fd0fe7e7831a46747f" - integrity sha512-scVWOqCMqHajlbwYZIzJ5nBYkDXTAhEpWjfcdCu8ykksA1barrKnEKdQvS84TtDWOx6UXDD/e/x0acYEZCDMEQ== - dependencies: - global "^4.4.0" - -videojs-vr@1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/videojs-vr/-/videojs-vr-1.8.0.tgz#7f2f07f760d8a329c615acd316e49da6ee8edd34" - integrity sha512-776gXqt8g6/rLeV56nn/aUcO0sRy+mgFITCw8cIqzTzl93SE1PEK/QE3YNqtppUfU5igayrx7WKsWhDOpsXMpw== - dependencies: - "@babel/runtime" "^7.14.5" - global "^4.4.0" - three "0.93.0" - video.js "^6 || ^7" - webvr-polyfill "0.10.12" - -videojs-vtt.js@^0.15.4: - version "0.15.4" - resolved "https://registry.yarnpkg.com/videojs-vtt.js/-/videojs-vtt.js-0.15.4.tgz#5dc5aabcd82ba40c5595469bd855ea8230ca152c" - integrity sha512-r6IhM325fcLb1D6pgsMkTQT1PpFdUdYZa1iqk7wJEu+QlibBwATPfPc9Bg8Jiym0GE5yP1AG2rMLu+QMVWkYtA== - dependencies: - global "^4.3.1" - -vite-plugin-compression@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz#a75b0d8f48357ebb377b65016da9f20885ef39b6" - integrity sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg== - dependencies: - chalk "^4.1.2" - debug "^4.3.3" - fs-extra "^10.0.0" - -vite-tsconfig-paths@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.0.5.tgz#c7c54e2cf7ccc5e600db565cecd7b368a1fa8889" - integrity sha512-/L/eHwySFYjwxoYt1WRJniuK/jPv+WGwgRGBYx3leciR5wBeqntQpUE6Js6+TJemChc+ter7fDBKieyEWDx4yQ== - dependencies: - debug "^4.1.1" - globrex "^0.1.2" - tsconfck "^2.0.1" - -vite@^4.5.14: - version "4.5.14" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.14.tgz#2e652bc1d898265d987d6543ce866ecd65fa4086" - integrity sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g== - dependencies: - esbuild "^0.18.10" - postcss "^8.4.27" - rollup "^3.27.1" - optionalDependencies: - fsevents "~2.3.2" - -warning@^4.0.0, warning@^4.0.2, warning@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" - integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== - dependencies: - loose-envify "^1.0.0" - -wcwidth@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" - integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== - dependencies: - defaults "^1.0.3" - -web-namespaces@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" - integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== - -web-streams-polyfill@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" - integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== - -"webcomponents.js@git+https://git@github.com/webcomponents/webcomponentsjs.git#v0.7.24": - version "0.7.24" - resolved "git+https://git@github.com/webcomponents/webcomponentsjs.git#8a2e40557b177e2cca0def2553f84c8269c8f93e" - -webcrypto-core@^1.7.4: - version "1.7.6" - resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.7.6.tgz#e32c4a12a13de4251f8f9ef336a6cba7cdec9b55" - integrity sha512-TBPiewB4Buw+HI3EQW+Bexm19/W4cP/qZG/02QJCXN+iN+T5sl074vZ3rJcle/ZtDBQSgjkbsQO/1eFcxnSBUA== - dependencies: - "@peculiar/asn1-schema" "^2.1.6" - "@peculiar/json-schema" "^1.1.12" - asn1js "^3.0.1" - pvtsutils "^1.3.2" - tslib "^2.4.0" - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -webvr-polyfill-dpdb@^1.0.17: - version "1.0.18" - resolved "https://registry.yarnpkg.com/webvr-polyfill-dpdb/-/webvr-polyfill-dpdb-1.0.18.tgz#258484ce06b057bf18898acc911bd173847bce11" - integrity sha512-O0S1ZGEWyPvyZEkS2VbyV7mtir/NM9MNK3EuhbHPoJ8EHTky2pTXehjIl+IiDPr+Lldgx129QGt3NGly7rwRPw== - -webvr-polyfill@0.10.12: - version "0.10.12" - resolved "https://registry.yarnpkg.com/webvr-polyfill/-/webvr-polyfill-0.10.12.tgz#47ea0b0d558f09e089bc49fa7b47a4ee7e4b8148" - integrity sha512-trDJEVUQnRIVAnmImjEQ0BlL1NfuWl8+eaEdu+bs4g59c7OtETi/5tFkgEFDRaWEYwHntXs/uFF3OXZuutNGGA== - dependencies: - cardboard-vr-display "^1.0.19" - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - -which-collection@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" - integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== - dependencies: - is-map "^2.0.1" - is-set "^2.0.1" - is-weakmap "^2.0.1" - is-weakset "^2.0.1" - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== - -which-typed-array@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" - integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== - dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.0" - is-typed-array "^1.1.10" - -which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -word-wrap@^1.2.3: - version "1.2.4" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" - integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -write-file-atomic@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -write-file-atomic@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz#68df4717c55c6fa4281a7860b4c2ba0a6d2b11e7" - integrity sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw== - dependencies: - imurmurhash "^0.1.4" - signal-exit "^4.0.1" - -write-json-file@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-4.3.0.tgz#908493d6fd23225344af324016e4ca8f702dd12d" - integrity sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ== - dependencies: - detect-indent "^6.0.0" - graceful-fs "^4.1.15" - is-plain-obj "^2.0.0" - make-dir "^3.0.0" - sort-keys "^4.0.0" - write-file-atomic "^3.0.0" - -ws@^8.12.0, ws@^8.13.0, ws@^8.15.0: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" - integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== - -xtend@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" - integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml-ast-parser@^0.0.43: - version "0.0.43" - resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" - integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== - -yaml@^1.10.0: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yaml@^2.3.1: - version "2.3.4" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" - integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== - -yargs-parser@^18.1.2, yargs-parser@^18.1.3: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^20.2.9: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - -yargs-parser@^21.1.1: - version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - -yargs@^15.3.1: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" - -yargs@^17.0.0: - version "17.6.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" - integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -yup@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/yup/-/yup-1.3.2.tgz#afffc458f1513ed386e6aaf4bcaa4e67a9e270dc" - integrity sha512-6KCM971iQtJ+/KUaHdrhVr2LDkfhBtFPRnsG1P8F4q3uUVQ2RfEM9xekpha9aA4GXWJevjM10eDcPQ1FfWlmaQ== - dependencies: - property-expr "^2.0.5" - tiny-case "^1.0.3" - toposort "^2.0.2" - type-fest "^2.19.0" - -zen-observable-ts@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz#6c6d9ea3d3a842812c6e9519209365a122ba8b58" - integrity sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg== - dependencies: - zen-observable "0.8.15" - -zen-observable@0.8.15: - version "0.8.15" - resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" - integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== - -zwitch@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" - integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==