diff --git a/.gitignore b/.gitignore index d3a1c21f0..197fd7302 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,6 @@ # GraphQL generated output internal/api/generated_*.go -ui/v2.5/src/core/generated-*.tsx #### # Jetbrains @@ -63,4 +62,5 @@ node_modules /stash dist -.DS_Store \ No newline at end of file +.DS_Store +/.local \ No newline at end of file diff --git a/Makefile b/Makefile index fe7628a56..b2630a4f0 100644 --- a/Makefile +++ b/Makefile @@ -9,9 +9,14 @@ endif ifdef IS_WIN_SHELL SEPARATOR := && SET := set + RM := del /s /q + RMDIR := rmdir /s /q + PWD := $(shell echo %cd%) else SEPARATOR := ; SET := export + RM := rm -f + RMDIR := rm -rf endif # set LDFLAGS environment variable to any extra ldflags required @@ -99,7 +104,7 @@ cross-compile-macos-applesilicon: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT) # can't use static build for OSX cross-compile-macos-applesilicon: build-release -cross-compile-macos: +cross-compile-macos: rm -rf dist/Stash.app dist/Stash-macos.zip make cross-compile-macos-applesilicon make cross-compile-macos-intel @@ -175,7 +180,7 @@ generate-frontend: cd ui/v2.5 && yarn run gqlgen .PHONY: generate-backend -generate-backend: touch-ui +generate-backend: touch-ui go generate -mod=vendor ./cmd/stash .PHONY: generate-dataloaders @@ -211,6 +216,23 @@ it: generate-test-mocks: go run -mod=vendor github.com/vektra/mockery/v2 --dir ./pkg/models --name '.*ReaderWriter' --outpkg mocks --output ./pkg/models/mocks +# runs server +# sets the config file to use the local dev config +.PHONY: server-start +server-start: export STASH_CONFIG_FILE=config.yml +server-start: +ifndef IS_WIN_SHELL + @mkdir -p .local +else + @if not exist ".local" mkdir .local +endif + cd .local && go run ../cmd/stash + +# removes local dev config files +.PHONY: server-clean +server-clean: + $(RMDIR) .local + # installs UI dependencies. Run when first cloning repository, or if UI # dependencies have changed .PHONY: pre-ui @@ -256,3 +278,8 @@ validate-backend: lint it .PHONY: docker-build docker-build: pre-build docker build --build-arg GITHASH=$(GITHASH) --build-arg STASH_VERSION=$(STASH_VERSION) -t stash/build -f docker/build/x86_64/Dockerfile . + +# locally builds and tags a 'stash/cuda-build' docker image +.PHONY: docker-cuda-build +docker-cuda-build: pre-build + docker build --build-arg GITHASH=$(GITHASH) --build-arg STASH_VERSION=$(STASH_VERSION) -t stash/cuda-build -f docker/build/x86_64/Dockerfile-CUDA . diff --git a/README.md b/README.md index 0fe3139d7..5f2c0fdcd 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,11 @@ https://stashapp.cc [![Build](https://github.com/stashapp/stash/actions/workflows/build.yml/badge.svg?branch=develop&event=push)](https://github.com/stashapp/stash/actions/workflows/build.yml) [![Docker pulls](https://img.shields.io/docker/pulls/stashapp/stash.svg)](https://hub.docker.com/r/stashapp/stash 'DockerHub') +[![Open Collective backers](https://img.shields.io/opencollective/backers/stashapp?logo=opencollective)](https://opencollective.com/stashapp) [![Go Report Card](https://goreportcard.com/badge/github.com/stashapp/stash)](https://goreportcard.com/report/github.com/stashapp/stash) [![Discord](https://img.shields.io/discord/559159668438728723.svg?logo=discord)](https://discord.gg/2TsNFKt) +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/stashapp/stash?logo=github)](https://github.com/stashapp/stash/releases/latest) +[![GitHub issues by-label](https://img.shields.io/github/issues-raw/stashapp/stash/bounty)](https://github.com/stashapp/stash/labels/bounty) ### **Stash is a self-hosted webapp written in Go which organizes and serves your porn.** ![demo image](docs/readme_assets/demo_image.png) @@ -26,7 +29,7 @@ For further information you can [read the in-app manual](ui/v2.5/src/docs/en). ## First Run #### Windows Users: Security Prompt -Running the app might present a security prompt since the binary isn't yet signed. Bypass this by clicking "more info" and then the "run anyway" button. +Running the app might present a security prompt since the binary isn't yet signed. Bypass this by clicking "more info" and then the "run anyway" button. #### 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. @@ -39,32 +42,31 @@ On first run, Stash will prompt you for some configuration options and media dir Stash can pull metadata (performers, tags, descriptions, studios, and more) directly from many sites through the use of [scrapers](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en/Scraping.md), which integrate directly into Stash. -Many community-maintained scrapers are available for download at the [Community Scrapers Collection](https://github.com/stashapp/CommunityScrapers). The community also maintains StashDB, a crowd-sourced repository of scene, studio, and performer information, that can automatically identify much of a typical media collection. Inquire in the Discord for details. Identifying an entire collection will typically require a mix of multiple sources. +Many community-maintained scrapers are available for download from [CommunityScrapers repository](https://github.com/stashapp/CommunityScrapers). The community also maintains StashDB, a crowd-sourced repository of scene, studio, and performer information, that can automatically identify much of a typical media collection. Inquire in the Discord for details. Identifying an entire collection will typically require a mix of multiple sources. -StashDB is the canonical instance of our open source metadata API, [stash-box](https://github.com/stashapp/stash-box). +[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.stashapp.cc/widgets/stash/-/stash-desktop-client/svg-badge.svg)](https://translate.stashapp.cc/engage/stash/) 🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇪🇪 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇷🇺 🇪🇸 🇸🇪 🇹🇼 🇹🇷 -Stash is available in 18 languages (so far!) and it could be in your language too. If you want to help us translate Stash into your language, you can make an account at [translate.stashapp.cc](https://translate.stashapp.cc/projects/stash/stash-desktop-client/) to get started contributing new languages or improving existing ones. Thanks! +Stash is available in 25 languages (so far!) and it could be in your language too. If you want to help us translate Stash into your language, you can make an account at [translate.stashapp.cc](https://translate.stashapp.cc/projects/stash/stash-desktop-client/) to get started contributing new languages or improving existing ones. Thanks! # Support (FAQ) -Answers to other Frequently Asked Questions can be found [on our Wiki](https://github.com/stashapp/stash/wiki/FAQ) +Check out our documentation on [Stash-Docs](https://docs.stashapp.cc) for information about the software, questions, guides, add-ons and more. -For issues not addressed there, there are a few options. - -* Read the [Wiki](https://github.com/stashapp/stash/wiki) -* Check the in-app documentation, in the top right corner of the app (also available [here](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en) +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 the [Discord server](https://discord.gg/2TsNFKt), where the community can offer support. +* 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://github.com/stashapp/stash/wiki/Themes) on our Wiki, along with instructions on how to install them. +There is a [directory of community-created themes](https://docs.stashapp.cc/user-interface-ui/themes) on Stash-Docs, along with instructions on how to install them. -You can also make Stash interface fit your desired style with [Custom CSS snippets](https://github.com/stashapp/stash/wiki/Custom-CSS-snippets). +You can also change the Stash interface to fit your desired style with various snippets from [Custom CSS snippets](https://docs.stashapp.cc/user-interface-ui/custom-css-snippets). # For Developers diff --git a/docker/build/x86_64/Dockerfile-CUDA b/docker/build/x86_64/Dockerfile-CUDA new file mode 100644 index 000000000..676e2ead9 --- /dev/null +++ b/docker/build/x86_64/Dockerfile-CUDA @@ -0,0 +1,51 @@ +# This dockerfile should be built with `make docker-cuda-build` from the stash root. + +# Build Frontend +FROM node:alpine as frontend +RUN apk add --no-cache make +## cache node_modules separately +COPY ./ui/v2.5/package.json ./ui/v2.5/yarn.lock /stash/ui/v2.5/ +WORKDIR /stash +RUN yarn --cwd ui/v2.5 install --frozen-lockfile. +COPY Makefile /stash/ +COPY ./graphql /stash/graphql/ +COPY ./ui /stash/ui/ +RUN make generate-frontend +ARG GITHASH +ARG STASH_VERSION +RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui + +# Build Backend +FROM golang:1.19-bullseye as backend +RUN apt update && apt install -y build-essential golang +WORKDIR /stash +COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/ +COPY ./scripts /stash/scripts/ +COPY ./vendor /stash/vendor/ +COPY ./pkg /stash/pkg/ +COPY ./cmd /stash/cmd +COPY ./internal /stash/internal +COPY --from=frontend /stash /stash/ +RUN make generate-backend +ARG GITHASH +ARG STASH_VERSION +RUN make build + +# Final Runnable Image +FROM nvidia/cuda:12.0.1-base-ubuntu22.04 +RUN apt update && apt upgrade -y && apt install -y ca-certificates libvips-tools ffmpeg wget intel-media-va-driver-non-free vainfo +RUN rm -rf /var/lib/apt/lists/* +COPY --from=backend /stash/stash /usr/bin/ + +# NVENC Patch +RUN mkdir -p /usr/local/bin /patched-lib +RUN wget https://raw.githubusercontent.com/keylase/nvidia-patch/master/patch.sh -O /usr/local/bin/patch.sh +RUN wget https://raw.githubusercontent.com/keylase/nvidia-patch/master/docker-entrypoint.sh -O /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/patch.sh /usr/local/bin/docker-entrypoint.sh /usr/bin/stash + +ENV LANG C.UTF-8 +ENV NVIDIA_VISIBLE_DEVICES all +ENV NVIDIA_DRIVER_CAPABILITIES=video,utility +ENV STASH_CONFIG_FILE=/root/.stash/config.yml +EXPOSE 9999 +ENTRYPOINT ["docker-entrypoint.sh", "stash"] diff --git a/docker/ci/x86_64/Dockerfile b/docker/ci/x86_64/Dockerfile index a23763e65..343c3c4a6 100644 --- a/docker/ci/x86_64/Dockerfile +++ b/docker/ci/x86_64/Dockerfile @@ -11,7 +11,11 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then BIN=stash-linux-arm32v6; \ 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 vips-tools ruby && pip install --no-cache-dir mechanicalsoup cloudscraper && gem install faraday +RUN apk add --no-cache --virtual .build-deps gcc python3-dev musl-dev \ + && apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg vips-tools ruby tzdata \ + && pip install mechanicalsoup cloudscraper bencoder.pyx \ + && gem install faraday \ + && apk del .build-deps ENV STASH_CONFIG_FILE=/root/.stash/config.yml EXPOSE 9999 diff --git a/docker/production/docker-compose.yml b/docker/production/docker-compose.yml index 837e3c95c..767fe0cb7 100644 --- a/docker/production/docker-compose.yml +++ b/docker/production/docker-compose.yml @@ -36,5 +36,7 @@ services: - ./metadata:/metadata ## Any other cache content. - ./cache:/cache + ## Where to store binary blob data (scene covers, images) + - ./blobs:/blobs ## Where to store generated content (screenshots,previews,transcodes,sprites) - ./generated:/generated diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 4df0729c0..94882efa5 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -25,7 +25,17 @@ 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` +2. Install dependencies: `brew install go git yarn gcc make node ffmpeg` + +### Linux + +#### Arch Linux +1. Install dependencies: `sudo pacman -S go git yarn gcc make nodejs ffmpeg --needed` + +#### Ubuntu +1. Install dependencies: `sudo apt-get install golang git gcc nodejs ffmpeg -y` +2. Enable corepack in Node.js: `corepack enable` +3. Install yarn: `corepack prepare yarn@stable --activate` ## Commands @@ -39,8 +49,35 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MingW. For examp * `make fmt` - Run `go fmt` * `make it` - Run the unit and integration tests * `make validate` - Run all of the tests and checks required to submit a PR +* `make server-start` - Runs an instance of the server in the `.local` directory. +* `make server-clean` - Removes the `.local` directory and all of its contents. * `make ui-start` - Runs the UI in development mode. Requires a running stash server to connect to. Stash server port can be changed from the default of `9999` using environment variable `VITE_APP_PLATFORM_PORT`. UI runs on port `3000` or the next available port. +## Local development quickstart + +1. Run `make pre-ui` to install UI dependencies +2. Run `make generate` to create generated files +3. In one terminal, run `make server-start` to run the server code +4. In a separate terminal, run `make ui-start` to run the UI in development mode +5. Open the UI in a browser `http://localhost:3000/` + +Changes to the UI code can be seen by reloading the browser page. + +Changes to the server code requires a restart (`CTRL-C` in the server terminal). + +On first launch: +1. On the "Stash Setup Wizard" screen, choose a directory with some files to test with +2. Press "Next" to use the default locations for the database and generated content +3. Press the "Confirm" and "Finish" buttons to get into the UI +4. On the side menu, navigate to "Tasks -> Library -> Scan" and press the "Scan" button +5. You're all set! Set any other configurations you'd like and test your code changes. + +To start fresh with new configuration: +1. Stop the server (`CTRL-C` in the server terminal) +2. Run `make server-clean` to clear all config, database, and generated files (under `.local/`) +3. Run `make server-start` to restart the server +4. Follow the "On first launch" steps above + ## Building a release 1. Run `make pre-ui` to install UI dependencies diff --git a/go.mod b/go.mod index facca58ce..1fbf6858a 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/golang-migrate/migrate/v4 v4.15.0-beta.1 github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.2.0 - github.com/gorilla/websocket v1.4.2 + github.com/gorilla/websocket v1.5.0 github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a github.com/jmoiron/sqlx v1.3.1 github.com/json-iterator/go v1.1.12 @@ -30,16 +30,16 @@ require ( github.com/spf13/afero v1.8.2 // indirect github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.10.1 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.7.1 github.com/tidwall/gjson v1.9.3 github.com/tidwall/pretty v1.2.0 // indirect github.com/vektra/mockery/v2 v2.10.0 golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 - golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb - golang.org/x/net v0.0.0-20220722155237-a158d28d115b - golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 - golang.org/x/text v0.3.7 + golang.org/x/image v0.5.0 + golang.org/x/net v0.7.0 + golang.org/x/sys v0.5.0 + golang.org/x/term v0.5.0 + golang.org/x/text v0.7.0 golang.org/x/tools v0.1.12 // indirect gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/yaml.v2 v2.4.0 @@ -57,7 +57,9 @@ require ( github.com/spf13/cast v1.4.1 github.com/vearutop/statigz v1.1.6 github.com/vektah/dataloaden v0.3.0 - github.com/vektah/gqlparser/v2 v2.4.1 + github.com/vektah/gqlparser/v2 v2.4.2 + github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e + github.com/zencoder/go-dash/v3 v3.0.2 gopkg.in/guregu/null.v4 v4.0.0 ) @@ -82,9 +84,9 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/matryer/moq v0.2.6 // indirect + github.com/matryer/moq v0.2.3 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect @@ -99,11 +101,12 @@ require ( github.com/stretchr/objx v0.2.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect github.com/tidwall/match v1.1.1 // indirect - github.com/urfave/cli/v2 v2.4.0 // indirect + github.com/urfave/cli/v2 v2.8.1 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.uber.org/atomic v1.7.0 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect gopkg.in/ini.v1 v1.66.4 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999 diff --git a/go.sum b/go.sum index def03da0a..14bf5606a 100644 --- a/go.sum +++ b/go.sum @@ -396,8 +396,9 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= @@ -555,9 +556,8 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/matryer/moq v0.2.3 h1:Q06vEqnBYjjfx5KKgHfYRKE/lvlRu+Nj+xodG4YdHnU= github.com/matryer/moq v0.2.3/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE= -github.com/matryer/moq v0.2.6 h1:X4+LF09udTsi2P+Z+1UhSb4p3K8IyiF7KSNFDR9M3M0= -github.com/matryer/moq v0.2.6/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -591,8 +591,9 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= 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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -734,8 +735,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/gjson v1.9.3 h1:hqzS9wAHMO+KVBBkLxYdkEeeFHuqr95GfClRLKlgK0E= @@ -748,22 +750,27 @@ github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I= -github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg= +github.com/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4= +github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY= github.com/vearutop/statigz v1.1.6 h1:si1zvulh/6P4S/SjFticuKQ8/EgQISglaRuycj8PWso= github.com/vearutop/statigz v1.1.6/go.mod h1:czAv7iXgPv/s+xsgXpVEhhD0NSOQ4wZPgmM/n7LANDI= github.com/vektah/dataloaden v0.3.0 h1:ZfVN2QD6swgvp+tDqdH/OIT/wu3Dhu0cus0k5gIZS84= github.com/vektah/dataloaden v0.3.0/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= github.com/vektah/gqlparser/v2 v2.4.0/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0= -github.com/vektah/gqlparser/v2 v2.4.1 h1:QOyEn8DAPMUMARGMeshKDkDgNmVoEaEGiDB0uWxcSlQ= github.com/vektah/gqlparser/v2 v2.4.1/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0= +github.com/vektah/gqlparser/v2 v2.4.2 h1:29TGc6QmhEUq5fll+2FPoTmhUhR65WEKN4VK/jo0OlM= +github.com/vektah/gqlparser/v2 v2.4.2/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0= github.com/vektra/mockery/v2 v2.10.0 h1:MiiQWxwdq7/ET6dCXLaJzSGEN17k758H7JHS9kOdiks= github.com/vektra/mockery/v2 v2.10.0/go.mod h1:m/WO2UzWzqgVX3nvqpRQq70I4Z7jbSCRhdmkgtp+Ab4= github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e h1:GruPsb+44XvYAzuAgJW1d1WHqmcI73L2XSjsbx/eJZw= +github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e/go.mod h1:wA8kQ8WFipMciY9WcWzqQgZordm/P7l8IZdvx1crwmc= github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -772,7 +779,10 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +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= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= @@ -837,8 +847,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= -golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= +golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -922,8 +932,9 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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 h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -956,6 +967,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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= @@ -1048,11 +1060,14 @@ golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 h1:v1W7bwXHsnLLloWYTVEdvGvA7BHMeBYsPcF0GLDxIRs= -golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 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= @@ -1061,8 +1076,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 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= @@ -1330,8 +1346,9 @@ gopkg.in/yaml.v2 v2.2.8/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= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= diff --git a/gqlgen.yml b/gqlgen.yml index 9e419a002..5be8c743a 100644 --- a/gqlgen.yml +++ b/gqlgen.yml @@ -34,6 +34,8 @@ models: title: resolver: true # autobind on config causes generation issues + BlobsStorageType: + model: github.com/stashapp/stash/internal/manager/config.BlobsStorageType StashConfig: model: github.com/stashapp/stash/internal/manager/config.StashConfig StashConfigInput: diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index 57f08eb41..173a7948e 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -10,6 +10,8 @@ fragment ConfigGeneralData on ConfigGeneralResult { metadataPath scrapersPath cachePath + blobsPath + blobsStorage calculateMD5 videoFileNamingAlgorithm parallelTasks @@ -19,6 +21,7 @@ fragment ConfigGeneralData on ConfigGeneralResult { previewExcludeStart previewExcludeEnd previewPreset + transcodeHardwareAcceleration maxTranscodeSize maxStreamingTranscodeSize writeImageThumbnails @@ -31,6 +34,7 @@ fragment ConfigGeneralData on ConfigGeneralResult { logLevel logAccess createGalleriesFromFolders + galleryCoverRegex videoExtensions imageExtensions galleryExtensions @@ -46,6 +50,11 @@ fragment ConfigGeneralData on ConfigGeneralResult { api_key } pythonPath + transcodeInputArgs + transcodeOutputArgs + liveTranscodeInputArgs + liveTranscodeOutputArgs + drawFunscriptHeatmapRange } fragment ConfigInterfaceData on ConfigInterfaceResult { @@ -124,6 +133,7 @@ fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult { scan { useFileMetadata stripFileExtension + scanGenerateCovers scanGeneratePreviews scanGenerateImagePreviews scanGenerateSprites @@ -152,6 +162,7 @@ fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult { } generate { + covers sprites previews imagePreviews diff --git a/graphql/documents/data/gallery-chapter.graphql b/graphql/documents/data/gallery-chapter.graphql new file mode 100644 index 000000000..674991356 --- /dev/null +++ b/graphql/documents/data/gallery-chapter.graphql @@ -0,0 +1,9 @@ +fragment GalleryChapterData on GalleryChapter { + id + title + image_index + + gallery { + id + } +} diff --git a/graphql/documents/data/gallery-slim.graphql b/graphql/documents/data/gallery-slim.graphql index c49ef2c11..9469c8486 100644 --- a/graphql/documents/data/gallery-slim.graphql +++ b/graphql/documents/data/gallery-slim.graphql @@ -22,6 +22,11 @@ fragment SlimGalleryData on Gallery { thumbnail } } + chapters { + id + title + image_index + } studio { id name diff --git a/graphql/documents/data/gallery.graphql b/graphql/documents/data/gallery.graphql index bb804047e..b4df25896 100644 --- a/graphql/documents/data/gallery.graphql +++ b/graphql/documents/data/gallery.graphql @@ -16,6 +16,9 @@ fragment GalleryData on Gallery { ...FolderData } + chapters { + ...GalleryChapterData + } cover { ...SlimImageData } diff --git a/graphql/documents/data/image-slim.graphql b/graphql/documents/data/image-slim.graphql index b9f891fa8..4f787d36e 100644 --- a/graphql/documents/data/image-slim.graphql +++ b/graphql/documents/data/image-slim.graphql @@ -1,6 +1,8 @@ fragment SlimImageData on Image { id title + date + url rating100 organized o_counter diff --git a/graphql/documents/data/image.graphql b/graphql/documents/data/image.graphql index 8142d9d49..f9adb5515 100644 --- a/graphql/documents/data/image.graphql +++ b/graphql/documents/data/image.graphql @@ -2,6 +2,8 @@ fragment ImageData on Image { id title rating100 + date + url organized o_counter created_at diff --git a/graphql/documents/data/performer-slim.graphql b/graphql/documents/data/performer-slim.graphql index 6479717f2..4bac5d90b 100644 --- a/graphql/documents/data/performer-slim.graphql +++ b/graphql/documents/data/performer-slim.graphql @@ -1,6 +1,7 @@ fragment SlimPerformerData on Performer { id name + disambiguation gender url twitter @@ -18,6 +19,7 @@ fragment SlimPerformerData on Performer { career_length tattoos piercings + alias_list tags { id name diff --git a/graphql/documents/data/performer.graphql b/graphql/documents/data/performer.graphql index ecdb9eacc..338ae0e10 100644 --- a/graphql/documents/data/performer.graphql +++ b/graphql/documents/data/performer.graphql @@ -2,6 +2,7 @@ fragment PerformerData on Performer { id checksum name + disambiguation url gender twitter @@ -16,7 +17,7 @@ fragment PerformerData on Performer { career_length tattoos piercings - aliases + alias_list favorite ignore_auto_tag image_path diff --git a/graphql/documents/data/scene-slim.graphql b/graphql/documents/data/scene-slim.graphql index 3e0749dd8..b6ed326e0 100644 --- a/graphql/documents/data/scene-slim.graphql +++ b/graphql/documents/data/scene-slim.graphql @@ -46,6 +46,9 @@ fragment SlimSceneData on Scene { files { path } + folder { + path + } title } diff --git a/graphql/documents/data/scrapers.graphql b/graphql/documents/data/scrapers.graphql index 802220293..8d02b3362 100644 --- a/graphql/documents/data/scrapers.graphql +++ b/graphql/documents/data/scrapers.graphql @@ -1,6 +1,7 @@ fragment ScrapedPerformerData on ScrapedPerformer { stored_id name + disambiguation gender url twitter @@ -30,6 +31,7 @@ fragment ScrapedPerformerData on ScrapedPerformer { fragment ScrapedScenePerformerData on ScrapedPerformer { stored_id name + disambiguation gender url twitter diff --git a/graphql/documents/data/studio.graphql b/graphql/documents/data/studio.graphql index 52fd8b418..34b2400d8 100644 --- a/graphql/documents/data/studio.graphql +++ b/graphql/documents/data/studio.graphql @@ -19,6 +19,7 @@ fragment StudioData on Studio { scene_count image_count gallery_count + performer_count movie_count stash_ids { stash_id diff --git a/graphql/documents/mutations/gallery-chapter.graphql b/graphql/documents/mutations/gallery-chapter.graphql new file mode 100644 index 000000000..520aac8d3 --- /dev/null +++ b/graphql/documents/mutations/gallery-chapter.graphql @@ -0,0 +1,31 @@ +mutation GalleryChapterCreate( + $title: String!, + $image_index: Int!, + $gallery_id: ID!) { + galleryChapterCreate(input: { + title: $title, + image_index: $image_index, + gallery_id: $gallery_id, + }) { + ...GalleryChapterData + } +} + +mutation GalleryChapterUpdate( + $id: ID!, + $title: String!, + $image_index: Int!, + $gallery_id: ID!) { + galleryChapterUpdate(input: { + id: $id, + title: $title, + image_index: $image_index, + gallery_id: $gallery_id, + }) { + ...GalleryChapterData + } +} + +mutation GalleryChapterDestroy($id: ID!) { + galleryChapterDestroy(id: $id) +} diff --git a/graphql/documents/mutations/metadata.graphql b/graphql/documents/mutations/metadata.graphql index 068665d9f..0d6486bed 100644 --- a/graphql/documents/mutations/metadata.graphql +++ b/graphql/documents/mutations/metadata.graphql @@ -41,3 +41,7 @@ mutation MigrateHashNaming { mutation BackupDatabase($input: BackupDatabaseInput!) { backupDatabase(input: $input) } + +mutation AnonymiseDatabase($input: AnonymiseDatabaseInput!) { + anonymiseDatabase(input: $input) +} diff --git a/graphql/documents/mutations/migration.graphql b/graphql/documents/mutations/migration.graphql new file mode 100644 index 000000000..edf483276 --- /dev/null +++ b/graphql/documents/mutations/migration.graphql @@ -0,0 +1,7 @@ +mutation MigrateSceneScreenshots($input: MigrateSceneScreenshotsInput!) { + migrateSceneScreenshots(input: $input) +} + +mutation MigrateBlobs($input: MigrateBlobsInput!) { + migrateBlobs(input: $input) +} \ No newline at end of file diff --git a/graphql/documents/queries/misc.graphql b/graphql/documents/queries/misc.graphql index b8b4871d1..e653635dc 100644 --- a/graphql/documents/queries/misc.graphql +++ b/graphql/documents/queries/misc.graphql @@ -6,26 +6,27 @@ query MarkerStrings($q: String, $sort: String) { } } -query AllTags { - allTags { - ...TagData - } -} - query AllPerformersForFilter { allPerformers { - ...SlimPerformerData + id + name + disambiguation + alias_list } } query AllStudiosForFilter { allStudios { - ...SlimStudioData + id + name + aliases } } + query AllMoviesForFilter { allMovies { - ...SlimMovieData + id + name } } @@ -67,7 +68,9 @@ query Version { query LatestVersion { latestversion { + version shorthash + release_date url } } diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index 959e52b99..112f8aba9 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -96,7 +96,7 @@ type Query { """Scrapes a complete performer record based on a URL""" scrapePerformerURL(url: String!): ScrapedPerformer - """Scrapes a complete performer record based on a URL""" + """Scrapes a complete scene record based on a URL""" scrapeSceneURL(url: String!): ScrapedScene """Scrapes a complete gallery record based on a URL""" scrapeGalleryURL(url: String!): ScrapedGallery @@ -144,6 +144,10 @@ type Query { # Get everything + allScenes: [Scene!]! + allSceneMarkers: [SceneMarker!]! + allImages: [Image!]! + allGalleries: [Gallery!]! allPerformers: [Performer!]! allStudios: [Studio!]! allMovies: [Movie!]! @@ -155,7 +159,7 @@ type Query { version: Version! # LatestVersion - latestversion: ShortVersion! + latestversion: LatestVersion! } type Mutation { @@ -214,6 +218,10 @@ type Mutation { addGalleryImages(input: GalleryAddInput!): Boolean! removeGalleryImages(input: GalleryRemoveInput!): Boolean! + galleryChapterCreate(input: GalleryChapterCreateInput!): GalleryChapter + galleryChapterUpdate(input: GalleryChapterUpdateInput!): GalleryChapter + galleryChapterDestroy(id: ID!): Boolean! + performerCreate(input: PerformerCreateInput!): Performer performerUpdate(input: PerformerUpdateInput!): Performer performerDestroy(input: PerformerDestroyInput!): Boolean! @@ -237,6 +245,14 @@ type Mutation { tagsDestroy(ids: [ID!]!): Boolean! tagsMerge(input: TagsMergeInput!): Tag + """Moves the given files to the given destination. Returns true if successful. + Either the destination_folder or destination_folder_id must be provided. If both are provided, the destination_folder_id takes precedence. + Destination folder must be a subfolder of one of the stash library paths. + If provided, destination_basename must be a valid filename with an extension that + matches one of the media extensions. + Creates folder hierarchy if needed. + """ + moveFiles(input: MoveFilesInput!): Boolean! deleteFiles(ids: [ID!]!): Boolean! # Saved filters @@ -279,8 +295,16 @@ type Mutation { metadataClean(input: CleanMetadataInput!): ID! """Identifies scenes using scrapers. Returns the job ID""" metadataIdentify(input: IdentifyMetadataInput!): ID! + """Migrate generated files for the current hash naming""" migrateHashNaming: ID! + """Migrates legacy scene screenshot files into the blob storage""" + migrateSceneScreenshots(input: MigrateSceneScreenshotsInput!): ID! + """Migrates blobs from the old storage system to the current one""" + migrateBlobs(input: MigrateBlobsInput!): ID! + + """Anonymise the database in a separate file. Optionally returns a link to download the database file""" + anonymiseDatabase(input: AnonymiseDatabaseInput!): String """Reload scrapers""" reloadScrapers: Boolean! diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 7cd1fea5f..df0aba092 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -6,6 +6,10 @@ input SetupInput { databaseFile: String! """Empty to indicate default""" generatedLocation: String! + """Empty to indicate default""" + cacheLocation: String! + """Empty to indicate database storage for blobs""" + blobsLocation: String! } enum StreamingResolutionEnum { @@ -32,6 +36,13 @@ enum HashAlgorithm { "oshash", OSHASH } +enum BlobsStorageType { + # blobs are stored in the database + "Database", DATABASE + # blobs are stored in the filesystem under the configured blobs directory + "Filesystem", FILESYSTEM +} + input ConfigGeneralInput { """Array of file paths to content""" stashes: [StashConfigInput!] @@ -47,6 +58,10 @@ input ConfigGeneralInput { scrapersPath: String """Path to cache""" cachePath: String + """Path to blobs - required for filesystem blob storage""" + blobsPath: String + """Where to store blobs""" + blobsStorage: BlobsStorageType """Whether to calculate MD5 checksums for scene video files""" calculateMD5: Boolean """Hash algorithm to use for generated file naming""" @@ -65,10 +80,30 @@ input ConfigGeneralInput { previewExcludeEnd: String """Preset when generating preview""" previewPreset: PreviewPreset + """Transcode Hardware Acceleration""" + transcodeHardwareAcceleration: Boolean """Max generated transcode size""" maxTranscodeSize: StreamingResolutionEnum """Max streaming transcode size""" maxStreamingTranscodeSize: StreamingResolutionEnum + + """ffmpeg transcode input args - injected before input file + These are applied to generated transcodes (previews and transcodes)""" + transcodeInputArgs: [String!] + """ffmpeg transcode output args - injected before output file + These are applied to generated transcodes (previews and transcodes)""" + transcodeOutputArgs: [String!] + + """ffmpeg stream input args - injected before input file + These are applied when live transcoding""" + liveTranscodeInputArgs: [String!] + """ffmpeg stream output args - injected before output file + These are applied when live transcoding""" + liveTranscodeOutputArgs: [String!] + + """whether to include range in generated funscript heatmaps""" + drawFunscriptHeatmapRange: Boolean + """Write image thumbnails to disk when generating on the fly""" writeImageThumbnails: Boolean """Username""" @@ -89,6 +124,8 @@ input ConfigGeneralInput { logAccess: Boolean """True if galleries should be created from folders with images""" createGalleriesFromFolders: Boolean + """Regex used to identify images as gallery covers""" + galleryCoverRegex: String """Array of video file extensions""" videoExtensions: [String!] """Array of image file extensions""" @@ -130,6 +167,10 @@ type ConfigGeneralResult { scrapersPath: String! """Path to cache""" cachePath: String! + """Path to blobs - required for filesystem blob storage""" + blobsPath: String! + """Where to store blobs""" + blobsStorage: BlobsStorageType! """Whether to calculate MD5 checksums for scene video files""" calculateMD5: Boolean! """Hash algorithm to use for generated file naming""" @@ -148,10 +189,30 @@ type ConfigGeneralResult { previewExcludeEnd: String! """Preset when generating preview""" previewPreset: PreviewPreset! + """Transcode Hardware Acceleration""" + transcodeHardwareAcceleration: Boolean! """Max generated transcode size""" maxTranscodeSize: StreamingResolutionEnum """Max streaming transcode size""" maxStreamingTranscodeSize: StreamingResolutionEnum + + """ffmpeg transcode input args - injected before input file + These are applied to generated transcodes (previews and transcodes)""" + transcodeInputArgs: [String!]! + """ffmpeg transcode output args - injected before output file + These are applied to generated transcodes (previews and transcodes)""" + transcodeOutputArgs: [String!]! + + """ffmpeg stream input args - injected before input file + These are applied when live transcoding""" + liveTranscodeInputArgs: [String!]! + """ffmpeg stream output args - injected before output file + These are applied when live transcoding""" + liveTranscodeOutputArgs: [String!]! + + """whether to include range in generated funscript heatmaps""" + drawFunscriptHeatmapRange: Boolean! + """Write image thumbnails to disk when generating on the fly""" writeImageThumbnails: Boolean! """API Key""" @@ -180,6 +241,8 @@ type ConfigGeneralResult { galleryExtensions: [String!]! """True if galleries should be created from folders with images""" createGalleriesFromFolders: Boolean! + """Regex used to identify images as gallery covers""" + galleryCoverRegex: String! """Array of file regexp to exclude from Video Scans""" excludes: [String!]! """Array of file regexp to exclude from Image Scans""" diff --git a/graphql/schema/types/file.graphql b/graphql/schema/types/file.graphql index 2493b622f..09b733c39 100644 --- a/graphql/schema/types/file.graphql +++ b/graphql/schema/types/file.graphql @@ -94,4 +94,16 @@ type GalleryFile implements BaseFile { created_at: Time! updated_at: Time! -} \ No newline at end of file +} + +input MoveFilesInput { + ids: [ID!]! + """valid for single or multiple file ids""" + destination_folder: String + + """valid for single or multiple file ids""" + destination_folder_id: ID + + """valid only for single file id. If empty, existing basename is used""" + destination_basename: String +} diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index b391ef085..9e124e49e 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -53,6 +53,7 @@ input PerformerFilterType { NOT: PerformerFilterType name: StringCriterionInput + disambiguation: StringCriterionInput details: StringCriterionInput """Filter by favorite""" @@ -323,6 +324,8 @@ input GalleryFilterType { organized: Boolean """Filter by average image resolution""" average_resolution: ResolutionCriterionInput + """Filter to only include galleries that have chapters. `true` or `false`""" + has_chapters: String """Filter to only include galleries with this studio""" studios: HierarchicalMultiCriterionInput """Filter to only include galleries with these tags""" @@ -424,6 +427,10 @@ input ImageFilterType { rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100") # rating expressed as 1-100 rating100: IntCriterionInput + """Filter by date""" + date: DateCriterionInput + """Filter by url""" + url: StringCriterionInput """Filter by organized""" organized: Boolean """Filter by o-counter""" diff --git a/graphql/schema/types/gallery-chapter.graphql b/graphql/schema/types/gallery-chapter.graphql new file mode 100644 index 000000000..0db36f91d --- /dev/null +++ b/graphql/schema/types/gallery-chapter.graphql @@ -0,0 +1,26 @@ +type GalleryChapter { + id: ID! + gallery: Gallery! + title: String! + image_index: Int! + created_at: Time! + updated_at: Time! +} + +input GalleryChapterCreateInput { + gallery_id: ID! + title: String! + image_index: Int! +} + +input GalleryChapterUpdateInput { + id: ID! + gallery_id: ID! + title: String! + image_index: Int! +} + +type FindGalleryChaptersResultType { + count: Int! + chapters: [GalleryChapter!]! +} diff --git a/graphql/schema/types/gallery.graphql b/graphql/schema/types/gallery.graphql index 3716b9478..1f62ddd51 100644 --- a/graphql/schema/types/gallery.graphql +++ b/graphql/schema/types/gallery.graphql @@ -19,6 +19,7 @@ type Gallery { files: [GalleryFile!]! folder: Folder + chapters: [GalleryChapter!]! scenes: [Scene!]! studio: Studio image_count: Int! diff --git a/graphql/schema/types/image.graphql b/graphql/schema/types/image.graphql index 3eed1ee85..6832cab24 100644 --- a/graphql/schema/types/image.graphql +++ b/graphql/schema/types/image.graphql @@ -6,6 +6,8 @@ type Image { rating: Int @deprecated(reason: "Use 1-100 range with rating100") # rating expressed as 1-100 rating100: Int + url: String + date: String o_counter: Int organized: Boolean! path: String! @deprecated(reason: "Use files.path") @@ -45,6 +47,8 @@ input ImageUpdateInput { # rating expressed as 1-100 rating100: Int organized: Boolean + url: String + date: String studio_id: ID performer_ids: [ID!] @@ -63,6 +67,8 @@ input BulkImageUpdateInput { # rating expressed as 1-100 rating100: Int organized: Boolean + url: String + date: String studio_id: ID performer_ids: BulkUpdateIds diff --git a/graphql/schema/types/metadata.graphql b/graphql/schema/types/metadata.graphql index 96784ee9d..ecde11eac 100644 --- a/graphql/schema/types/metadata.graphql +++ b/graphql/schema/types/metadata.graphql @@ -1,6 +1,7 @@ scalar Upload input GenerateMetadataInput { + covers: Boolean sprites: Boolean previews: Boolean imagePreviews: Boolean @@ -37,6 +38,7 @@ input GeneratePreviewOptionsInput { } type GenerateMetadataOptions { + covers: Boolean sprites: Boolean previews: Boolean imagePreviews: Boolean @@ -84,6 +86,8 @@ input ScanMetadataInput { """Strip file extension from title""" stripFileExtension: Boolean @deprecated(reason: "Not implemented") + """Generate covers during scan""" + scanGenerateCovers: Boolean """Generate previews during scan""" scanGeneratePreviews: Boolean """Generate image previews during scan""" @@ -101,9 +105,11 @@ input ScanMetadataInput { type ScanMetadataOptions { """Set name, date, details from metadata (if present)""" - useFileMetadata: Boolean! + useFileMetadata: Boolean! @deprecated(reason: "Not implemented") """Strip file extension from title""" - stripFileExtension: Boolean! + stripFileExtension: Boolean! @deprecated(reason: "Not implemented") + """Generate covers during scan""" + scanGenerateCovers: Boolean! """Generate previews during scan""" scanGeneratePreviews: Boolean! """Generate image previews during scan""" @@ -263,6 +269,10 @@ input BackupDatabaseInput { download: Boolean } +input AnonymiseDatabaseInput { + download: Boolean +} + enum SystemStatusEnum { SETUP NEEDS_MIGRATION diff --git a/graphql/schema/types/migration.graphql b/graphql/schema/types/migration.graphql new file mode 100644 index 000000000..231f79555 --- /dev/null +++ b/graphql/schema/types/migration.graphql @@ -0,0 +1,11 @@ +input MigrateSceneScreenshotsInput { + # if true, delete screenshot files after migrating + deleteFiles: Boolean + # if true, overwrite existing covers with the covers from the screenshots directory + overwriteExisting: Boolean +} + +input MigrateBlobsInput { + # if true, delete blob data from old storage system + deleteOld: Boolean +} diff --git a/graphql/schema/types/performer.graphql b/graphql/schema/types/performer.graphql index 651341fc2..235960bfc 100644 --- a/graphql/schema/types/performer.graphql +++ b/graphql/schema/types/performer.graphql @@ -9,8 +9,9 @@ enum GenderEnum { type Performer { id: ID! - checksum: String! - name: String + checksum: String @deprecated(reason: "Not used") + name: String! + disambiguation: String url: String gender: GenderEnum twitter: String @@ -26,7 +27,8 @@ type Performer { career_length: String tattoos: String piercings: String - aliases: String + aliases: String @deprecated(reason: "Use alias_list") + alias_list: [String!]! favorite: Boolean! tags: [Tag!]! ignore_auto_tag: Boolean! @@ -53,6 +55,7 @@ type Performer { input PerformerCreateInput { name: String! + disambiguation: String url: String gender: GenderEnum birthdate: String @@ -67,7 +70,8 @@ input PerformerCreateInput { career_length: String tattoos: String piercings: String - aliases: String + aliases: String @deprecated(reason: "Use alias_list") + alias_list: [String!] twitter: String instagram: String favorite: Boolean @@ -89,6 +93,7 @@ input PerformerCreateInput { input PerformerUpdateInput { id: ID! name: String + disambiguation: String url: String gender: GenderEnum birthdate: String @@ -103,7 +108,8 @@ input PerformerUpdateInput { career_length: String tattoos: String piercings: String - aliases: String + aliases: String @deprecated(reason: "Use alias_list") + alias_list: [String!] twitter: String instagram: String favorite: Boolean @@ -122,9 +128,15 @@ input PerformerUpdateInput { ignore_auto_tag: Boolean } +input BulkUpdateStrings { + values: [String!] + mode: BulkUpdateIdMode! +} + input BulkPerformerUpdateInput { clientMutationId: String ids: [ID!] + disambiguation: String url: String gender: GenderEnum birthdate: String @@ -139,7 +151,8 @@ input BulkPerformerUpdateInput { career_length: String tattoos: String piercings: String - aliases: String + aliases: String @deprecated(reason: "Use alias_list") + alias_list: BulkUpdateStrings twitter: String instagram: String favorite: Boolean diff --git a/graphql/schema/types/scraped-performer.graphql b/graphql/schema/types/scraped-performer.graphql index b11b9b1b5..518e5abca 100644 --- a/graphql/schema/types/scraped-performer.graphql +++ b/graphql/schema/types/scraped-performer.graphql @@ -3,6 +3,7 @@ type ScrapedPerformer { """Set if performer matched""" stored_id: ID name: String + disambiguation: String gender: String url: String twitter: String @@ -17,6 +18,7 @@ type ScrapedPerformer { career_length: String tattoos: String piercings: String + # aliases must be comma-delimited to be parsed correctly aliases: String tags: [ScrapedTag!] @@ -34,6 +36,7 @@ input ScrapedPerformerInput { """Set if performer matched""" stored_id: ID name: String + disambiguation: String gender: String url: String twitter: String diff --git a/graphql/schema/types/studio.graphql b/graphql/schema/types/studio.graphql index 097ea8340..f9b72544e 100644 --- a/graphql/schema/types/studio.graphql +++ b/graphql/schema/types/studio.graphql @@ -12,6 +12,7 @@ type Studio { scene_count: Int # Resolver image_count: Int # Resolver gallery_count: Int # Resolver + performer_count: Int # Resolver stash_ids: [StashID!]! # rating expressed as 1-5 rating: Int @deprecated(reason: "Use 1-100 range with rating100") diff --git a/graphql/schema/types/version.graphql b/graphql/schema/types/version.graphql index 305c431cf..4385430ca 100644 --- a/graphql/schema/types/version.graphql +++ b/graphql/schema/types/version.graphql @@ -4,7 +4,9 @@ type Version { build_time: String! } -type ShortVersion { +type LatestVersion { + version: String! shorthash: String! + release_date: String! url: String! } diff --git a/internal/api/authentication.go b/internal/api/authentication.go index 176c532a9..d02f98b13 100644 --- a/internal/api/authentication.go +++ b/internal/api/authentication.go @@ -13,21 +13,24 @@ import ( "github.com/stashapp/stash/pkg/session" ) -const loginEndPoint = "/login" +const ( + loginEndPoint = "/login" + logoutEndPoint = "/logout" +) const ( tripwireActivatedErrMsg = "Stash is exposed to the public internet without authentication, and is not serving any more content to protect your privacy. " + - "More information and fixes are available at https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet" + "More information and fixes are available at https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet" externalAccessErrMsg = "You have attempted to access Stash over the internet, and authentication is not enabled. " + "This is extremely dangerous! The whole world can see your your stash page and browse your files! " + "Stash is not answering any other requests to protect your privacy. " + - "Please read the log entry or visit https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet" + "Please read the log entry or visit https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet" ) func allowUnauthenticated(r *http.Request) bool { // #2715 - allow access to UI files - return strings.HasPrefix(r.URL.Path, loginEndPoint) || r.URL.Path == "/css" || strings.HasPrefix(r.URL.Path, "/assets") + return strings.HasPrefix(r.URL.Path, loginEndPoint) || r.URL.Path == logoutEndPoint || r.URL.Path == "/css" || strings.HasPrefix(r.URL.Path, "/assets") } func authenticateHandler() func(http.Handler) http.Handler { @@ -85,12 +88,16 @@ func authenticateHandler() func(http.Handler) http.Handler { prefix := getProxyPrefix(r.Header) // otherwise redirect to the login page - u := url.URL{ - Path: prefix + "/login", + returnURL := url.URL{ + Path: prefix + r.URL.Path, + RawQuery: r.URL.RawQuery, + } + q := make(url.Values) + q.Set(returnURLParam, returnURL.String()) + u := url.URL{ + Path: prefix + "/login", + RawQuery: q.Encode(), } - q := u.Query() - q.Set(returnURLParam, prefix+r.URL.Path) - u.RawQuery = q.Encode() http.Redirect(w, r, u.String(), http.StatusFound) return } diff --git a/internal/api/byterange.go b/internal/api/byterange.go deleted file mode 100644 index 564444579..000000000 --- a/internal/api/byterange.go +++ /dev/null @@ -1,51 +0,0 @@ -package api - -import ( - "strconv" - "strings" -) - -type byteRange struct { - Start int64 - End *int64 - RawString string -} - -func createByteRange(s string) byteRange { - // strip bytes= - r := strings.TrimPrefix(s, "bytes=") - e := strings.Split(r, "-") - - ret := byteRange{ - RawString: s, - } - if len(e) > 0 { - ret.Start, _ = strconv.ParseInt(e[0], 10, 64) - } - if len(e) > 1 && e[1] != "" { - end, _ := strconv.ParseInt(e[1], 10, 64) - ret.End = &end - } - - return ret -} - -func (r byteRange) toHeaderValue(fileLength int64) string { - if r.End == nil { - return "" - } - end := *r.End - return "bytes " + strconv.FormatInt(r.Start, 10) + "-" + strconv.FormatInt(end, 10) + "/" + strconv.FormatInt(fileLength, 10) -} - -func (r byteRange) apply(bytes []byte) []byte { - if r.End == nil { - return bytes[r.Start:] - } - - end := *r.End + 1 - if int(end) > len(bytes) { - end = int64(len(bytes)) - } - return bytes[r.Start:end] -} diff --git a/internal/api/changeset_translator.go b/internal/api/changeset_translator.go index 3e768a136..ff182ed32 100644 --- a/internal/api/changeset_translator.go +++ b/internal/api/changeset_translator.go @@ -143,7 +143,7 @@ func (t changesetTranslator) optionalDate(value *string, field string) models.Op return models.OptionalDate{} } - if value == nil { + if value == nil || *value == "" { return models.OptionalDate{ Set: true, Null: true, diff --git a/internal/api/check_version.go b/internal/api/check_version.go index c3014eab4..bb621cb9d 100644 --- a/internal/api/check_version.go +++ b/internal/api/check_version.go @@ -21,11 +21,7 @@ const apiReleases string = "https://api.github.com/repos/stashapp/stash/releases const apiTags string = "https://api.github.com/repos/stashapp/stash/tags" const apiAcceptHeader string = "application/vnd.github.v3+json" const developmentTag string = "latest_develop" -const defaultSHLength int = 7 // default length of SHA short hash returned by - -// ErrNoVersion indicates that no version information has been embedded in the -// stash binary -var ErrNoVersion = errors.New("no stash version") +const defaultSHLength int = 8 // default length of SHA short hash returned by var stashReleases = func() map[string]string { return map[string]string{ @@ -108,9 +104,21 @@ type githubTagResponse struct { Node_id string } +type LatestRelease struct { + Version string + Hash string + ShortHash string + Date string + Url string +} + func makeGithubRequest(ctx context.Context, url string, output interface{}) error { + + transport := &http.Transport{Proxy: http.ProxyFromEnvironment} + client := &http.Client{ - Timeout: 3 * time.Second, + Timeout: 3 * time.Second, + Transport: transport, } req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -144,14 +152,16 @@ func makeGithubRequest(ctx context.Context, url string, output interface{}) erro return nil } -// GetLatestVersion gets latest version (git commit hash) from github API +// GetLatestRelease gets latest release information from github API // If running a build from the "master" branch, then the latest full release // is used, otherwise it uses the release that is tagged with "latest_develop" // which is the latest pre-release build. -func GetLatestVersion(ctx context.Context, shortHash bool) (latestVersion string, latestRelease string, err error) { +func GetLatestRelease(ctx context.Context) (*LatestRelease, error) { + arch := runtime.GOARCH - arch := runtime.GOARCH // https://en.wikipedia.org/wiki/Comparison_of_ARM_cores - isARMv7 := cpu.ARM.HasNEON || cpu.ARM.HasVFPv3 || cpu.ARM.HasVFPv3D16 || cpu.ARM.HasVFPv4 // armv6 doesn't support any of these features + // https://en.wikipedia.org/wiki/Comparison_of_ARM_cores + // armv6 doesn't support any of these features + isARMv7 := cpu.ARM.HasNEON || cpu.ARM.HasVFPv3 || cpu.ARM.HasVFPv3D16 || cpu.ARM.HasVFPv4 if arch == "arm" && isARMv7 { arch = "armv7" } @@ -159,125 +169,98 @@ func GetLatestVersion(ctx context.Context, shortHash bool) (latestVersion string platform := fmt.Sprintf("%s/%s", runtime.GOOS, arch) wantedRelease := stashReleases()[platform] - version, _, _ := GetVersion() - if version == "" { - return "", "", ErrNoVersion - } - - // if the version is suffixed with -x-xxxx, then we are running a development build - usePreRelease := false - re := regexp.MustCompile(`-\d+-g\w+$`) - if re.MatchString(version) { - usePreRelease = true - } - url := apiReleases - if !usePreRelease { - // just get the latest full release - url += "/latest" - } else { + if IsDevelop() { // get the release tagged with the development tag url += "/tags/" + developmentTag + } else { + // just get the latest full release + url += "/latest" } - release := githubReleasesResponse{} - err = makeGithubRequest(ctx, url, &release) - + var release githubReleasesResponse + err := makeGithubRequest(ctx, url, &release) if err != nil { - return "", "", err + return nil, err } - if release.Prerelease == usePreRelease { - latestVersion = getReleaseHash(ctx, release, shortHash, usePreRelease) + version := release.Name + if release.Prerelease { + // find version in prerelease name + re := regexp.MustCompile(`v[\w-\.]+-\d+-g[0-9a-f]+`) + if match := re.FindString(version); match != "" { + version = match + } + } - if wantedRelease != "" { - for _, asset := range release.Assets { - if asset.Name == wantedRelease { - latestRelease = asset.Browser_download_url - break - } + latestHash, err := getReleaseHash(ctx, release.Tag_name) + if err != nil { + return nil, err + } + + var releaseDate string + if publishedAt, err := time.Parse(time.RFC3339, release.Published_at); err == nil { + releaseDate = publishedAt.Format("2006-01-02") + } + + var releaseUrl string + if wantedRelease != "" { + for _, asset := range release.Assets { + if asset.Name == wantedRelease { + releaseUrl = asset.Browser_download_url + break } } } - if latestVersion == "" { - return "", "", fmt.Errorf("no version found for \"%s\"", version) + _, githash, _ := GetVersion() + shLength := len(githash) + if shLength == 0 { + shLength = defaultSHLength } - return latestVersion, latestRelease, nil + + return &LatestRelease{ + Version: version, + Hash: latestHash, + ShortHash: latestHash[:shLength], + Date: releaseDate, + Url: releaseUrl, + }, nil } -func getReleaseHash(ctx context.Context, release githubReleasesResponse, shortHash bool, usePreRelease bool) string { - shaLength := len(release.Target_commitish) - // the /latest API call doesn't return the hash in target_commitish - // also add sanity check in case Target_commitish is not 40 characters - if !usePreRelease || shaLength != 40 { - return getShaFromTags(ctx, shortHash, release.Tag_name) - } - - if shortHash { - last := defaultSHLength // default length of git short hash - _, gitShort, _ := GetVersion() // retrieve it to check actual length - if len(gitShort) > last && len(gitShort) < shaLength { // sometimes short hash is longer - last = len(gitShort) - } - return release.Target_commitish[0:last] - } - - return release.Target_commitish -} - -func printLatestVersion(ctx context.Context) { - _, githash, _ = GetVersion() - latest, _, err := GetLatestVersion(ctx, true) - if err != nil { - logger.Errorf("Couldn't find latest version: %s", err) - } else { - if githash == latest { - logger.Infof("Version: (%s) is already the latest released.", latest) - } else { - logger.Infof("New version: (%s) available.", latest) - } - } -} - -// get sha from the github api tags endpoint -// returns the sha1 hash/shorthash or "" if something's wrong -func getShaFromTags(ctx context.Context, shortHash bool, name string) string { +func getReleaseHash(ctx context.Context, tagName string) (string, error) { url := apiTags tags := []githubTagResponse{} err := makeGithubRequest(ctx, url, &tags) - if err != nil { - // If the context is canceled, we don't want to log this as an error - // in the path. The function here just gives up and returns "" if - // something goes wrong. Hence, log the error at the info-level so - // it's still present, but don't treat this as an error. - if errors.Is(err, context.Canceled) { - logger.Infof("aborting sha request due to context cancellation") - } else { - logger.Errorf("Github Tags Api: %v", err) - } - return "" + return "", err } - _, gitShort, _ := GetVersion() // retrieve short hash to check actual length for _, tag := range tags { - if tag.Name == name { - shaLength := len(tag.Commit.Sha) - if shaLength != 40 { - return "" + if tag.Name == tagName { + if len(tag.Commit.Sha) != 40 { + return "", errors.New("invalid Github API response") } - if shortHash { - last := defaultSHLength // default length of git short hash - if len(gitShort) > last && len(gitShort) < shaLength { // sometimes short hash is longer - last = len(gitShort) - } - return tag.Commit.Sha[0:last] - } - - return tag.Commit.Sha + return tag.Commit.Sha, nil } } - return "" + return "", errors.New("invalid Github API response") +} + +func printLatestVersion(ctx context.Context) { + latestRelease, err := GetLatestRelease(ctx) + if err != nil { + logger.Errorf("Couldn't retrieve latest version: %v", err) + } else { + _, githash, _ = GetVersion() + switch { + case githash == "": + logger.Infof("Latest version: %s (%s)", latestRelease.Version, latestRelease.ShortHash) + case githash == latestRelease.ShortHash: + logger.Infof("Version %s (%s) is already the latest released", latestRelease.Version, latestRelease.ShortHash) + default: + logger.Infof("New version available: %s (%s)", latestRelease.Version, latestRelease.ShortHash) + } + } } diff --git a/internal/api/resolver.go b/internal/api/resolver.go index bfe96939f..8d2ccc744 100644 --- a/internal/api/resolver.go +++ b/internal/api/resolver.go @@ -47,6 +47,9 @@ func (r *Resolver) scraperCache() *scraper.Cache { func (r *Resolver) Gallery() GalleryResolver { return &galleryResolver{r} } +func (r *Resolver) GalleryChapter() GalleryChapterResolver { + return &galleryChapterResolver{r} +} func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } @@ -83,6 +86,7 @@ type queryResolver struct{ *Resolver } type subscriptionResolver struct{ *Resolver } type galleryResolver struct{ *Resolver } +type galleryChapterResolver struct{ *Resolver } type performerResolver struct{ *Resolver } type sceneResolver struct{ *Resolver } type sceneMarkerResolver struct{ *Resolver } @@ -184,19 +188,22 @@ func (r *queryResolver) Version(ctx context.Context) (*Version, error) { }, nil } -// Latestversion returns the latest git shorthash commit. -func (r *queryResolver) Latestversion(ctx context.Context) (*ShortVersion, error) { - ver, url, err := GetLatestVersion(ctx, true) - if err == nil { - logger.Infof("Retrieved latest hash: %s", ver) - } else { - logger.Errorf("Error while retrieving latest hash: %s", err) +func (r *queryResolver) Latestversion(ctx context.Context) (*LatestVersion, error) { + latestRelease, err := GetLatestRelease(ctx) + if err != nil { + if !errors.Is(err, context.Canceled) { + logger.Errorf("Error while retrieving latest version: %v", err) + } + return nil, err } + logger.Infof("Retrieved latest version: %s (%s)", latestRelease.Version, latestRelease.ShortHash) - return &ShortVersion{ - Shorthash: ver, - URL: url, - }, err + return &LatestVersion{ + Version: latestRelease.Version, + Shorthash: latestRelease.ShortHash, + ReleaseDate: latestRelease.Date, + URL: latestRelease.Url, + }, nil } // Get scene marker tags which show up under the video. diff --git a/internal/api/resolver_model_gallery.go b/internal/api/resolver_model_gallery.go index 43c5b2221..8157404dc 100644 --- a/internal/api/resolver_model_gallery.go +++ b/internal/api/resolver_model_gallery.go @@ -6,6 +6,7 @@ import ( "time" "github.com/stashapp/stash/internal/api/loaders" + "github.com/stashapp/stash/internal/manager/config" "github.com/stashapp/stash/pkg/file" "github.com/stashapp/stash/pkg/image" @@ -145,8 +146,8 @@ func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) (ret func (r *galleryResolver) Cover(ctx context.Context, obj *models.Gallery) (ret *models.Image, err error) { if err := r.withReadTxn(ctx, func(ctx context.Context) error { - // find cover.jpg first - ret, err = image.FindGalleryCover(ctx, r.repository.Image, obj.ID) + // Find cover image first + ret, err = image.FindGalleryCover(ctx, r.repository.Image, obj.ID, config.GetInstance().GetGalleryCoverRegex()) return err }); err != nil { return nil, err @@ -248,3 +249,14 @@ func (r *galleryResolver) ImageCount(ctx context.Context, obj *models.Gallery) ( return ret, nil } + +func (r *galleryResolver) Chapters(ctx context.Context, obj *models.Gallery) (ret []*models.GalleryChapter, err error) { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + ret, err = r.repository.GalleryChapter.FindByGalleryID(ctx, obj.ID) + return err + }); err != nil { + return nil, err + } + + return ret, nil +} diff --git a/internal/api/resolver_model_gallery_chapter.go b/internal/api/resolver_model_gallery_chapter.go new file mode 100644 index 000000000..216336e12 --- /dev/null +++ b/internal/api/resolver_model_gallery_chapter.go @@ -0,0 +1,32 @@ +package api + +import ( + "context" + "time" + + "github.com/stashapp/stash/pkg/models" +) + +func (r *galleryChapterResolver) Gallery(ctx context.Context, obj *models.GalleryChapter) (ret *models.Gallery, err error) { + if !obj.GalleryID.Valid { + panic("Invalid gallery id") + } + + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + galleryID := int(obj.GalleryID.Int64) + ret, err = r.repository.Gallery.Find(ctx, galleryID) + return err + }); err != nil { + return nil, err + } + + return ret, nil +} + +func (r *galleryChapterResolver) CreatedAt(ctx context.Context, obj *models.GalleryChapter) (*time.Time, error) { + return &obj.CreatedAt.Timestamp, nil +} + +func (r *galleryChapterResolver) UpdatedAt(ctx context.Context, obj *models.GalleryChapter) (*time.Time, error) { + return &obj.UpdatedAt.Timestamp, nil +} diff --git a/internal/api/resolver_model_image.go b/internal/api/resolver_model_image.go index c7fdb8c5f..2a1965c4e 100644 --- a/internal/api/resolver_model_image.go +++ b/internal/api/resolver_model_image.go @@ -75,6 +75,14 @@ func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*ImageFile }, nil } +func (r *imageResolver) Date(ctx context.Context, obj *models.Image) (*string, error) { + if obj.Date != nil { + result := obj.Date.String() + return &result, nil + } + return nil, nil +} + func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*ImageFile, error) { files, err := r.getFiles(ctx, obj) if err != nil { diff --git a/internal/api/resolver_model_movie.go b/internal/api/resolver_model_movie.go index fbde8a80a..9967ef323 100644 --- a/internal/api/resolver_model_movie.go +++ b/internal/api/resolver_model_movie.go @@ -93,10 +93,10 @@ func (r *movieResolver) FrontImagePath(ctx context.Context, obj *models.Movie) ( func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*string, error) { // don't return any thing if there is no back image - var img []byte + hasImage := false if err := r.withReadTxn(ctx, func(ctx context.Context) error { var err error - img, err = r.repository.Movie.GetBackImage(ctx, obj.ID) + hasImage, err = r.repository.Movie.HasBackImage(ctx, obj.ID) if err != nil { return err } @@ -106,7 +106,7 @@ func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (* return nil, err } - if img == nil { + if !hasImage { return nil, nil } diff --git a/internal/api/resolver_model_performer.go b/internal/api/resolver_model_performer.go index 414b894a4..8aac29022 100644 --- a/internal/api/resolver_model_performer.go +++ b/internal/api/resolver_model_performer.go @@ -3,13 +3,45 @@ package api import ( "context" "strconv" + "strings" + "github.com/stashapp/stash/internal/api/loaders" "github.com/stashapp/stash/internal/api/urlbuilders" "github.com/stashapp/stash/pkg/gallery" "github.com/stashapp/stash/pkg/image" "github.com/stashapp/stash/pkg/models" ) +// Checksum is deprecated +func (r *performerResolver) Checksum(ctx context.Context, obj *models.Performer) (*string, error) { + return nil, nil +} + +func (r *performerResolver) Aliases(ctx context.Context, obj *models.Performer) (*string, error) { + if !obj.Aliases.Loaded() { + if err := r.withTxn(ctx, func(ctx context.Context) error { + return obj.LoadAliases(ctx, r.repository.Performer) + }); err != nil { + return nil, err + } + } + + ret := strings.Join(obj.Aliases.List(), ", ") + return &ret, nil +} + +func (r *performerResolver) AliasList(ctx context.Context, obj *models.Performer) ([]string, error) { + if !obj.Aliases.Loaded() { + if err := r.withTxn(ctx, func(ctx context.Context) error { + return obj.LoadAliases(ctx, r.repository.Performer) + }); err != nil { + return nil, err + } + } + + return obj.Aliases.List(), nil +} + func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) { if obj.Height != nil { ret := strconv.Itoa(*obj.Height) @@ -37,14 +69,17 @@ func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer } func (r *performerResolver) Tags(ctx context.Context, obj *models.Performer) (ret []*models.Tag, err error) { - if err := r.withReadTxn(ctx, func(ctx context.Context) error { - ret, err = r.repository.Tag.FindByPerformerID(ctx, obj.ID) - return err - }); err != nil { - return nil, err + if !obj.TagIDs.Loaded() { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + return obj.LoadTagIDs(ctx, r.repository.Performer) + }); err != nil { + return nil, err + } } - return ret, nil + var errs []error + ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List()) + return ret, firstError(errs) } func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performer) (ret *int, err error) { @@ -95,16 +130,13 @@ func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) ( } func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) ([]*models.StashID, error) { - var ret []models.StashID if err := r.withReadTxn(ctx, func(ctx context.Context) error { - var err error - ret, err = r.repository.Performer.GetStashIDs(ctx, obj.ID) - return err + return obj.LoadStashIDs(ctx, r.repository.Performer) }); err != nil { return nil, err } - return stashIDsSliceToPtrSlice(ret), nil + return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil } func (r *performerResolver) Rating(ctx context.Context, obj *models.Performer) (*int, error) { diff --git a/internal/api/resolver_model_scene.go b/internal/api/resolver_model_scene.go index 47a4d0382..a5c70fadc 100644 --- a/internal/api/resolver_model_scene.go +++ b/internal/api/resolver_model_scene.go @@ -179,13 +179,13 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePat baseURL, _ := ctx.Value(BaseURLCtxKey).(string) config := manager.GetInstance().Config builder := urlbuilders.NewSceneURLBuilder(baseURL, obj.ID) - builder.APIKey = config.GetAPIKey() screenshotPath := builder.GetScreenshotURL(obj.UpdatedAt) previewPath := builder.GetStreamPreviewURL() - streamPath := builder.GetStreamURL().String() + streamPath := builder.GetStreamURL(config.GetAPIKey()).String() webpPath := builder.GetStreamPreviewImageURL() - vttPath := builder.GetSpriteVTTURL() - spritePath := builder.GetSpriteURL() + objHash := obj.GetHash(config.GetVideoFileNamingAlgorithm()) + vttPath := builder.GetSpriteVTTURL(objHash) + spritePath := builder.GetSpriteURL(objHash) chaptersVttPath := builder.GetChaptersVTTURL() funscriptPath := builder.GetFunscriptURL() captionBasePath := builder.GetCaptionURL() @@ -371,9 +371,9 @@ func (r *sceneResolver) SceneStreams(ctx context.Context, obj *models.Scene) ([] baseURL, _ := ctx.Value(BaseURLCtxKey).(string) builder := urlbuilders.NewSceneURLBuilder(baseURL, obj.ID) - builder.APIKey = config.GetAPIKey() + apiKey := config.GetAPIKey() - return manager.GetSceneStreamPaths(obj, builder.GetStreamURL(), config.GetMaxStreamingTranscodeSize()) + return manager.GetSceneStreamPaths(obj, builder.GetStreamURL(apiKey), config.GetMaxStreamingTranscodeSize()) } func (r *sceneResolver) Interactive(ctx context.Context, obj *models.Scene) (bool, error) { diff --git a/internal/api/resolver_model_studio.go b/internal/api/resolver_model_studio.go index 282e5a46e..4d689df77 100644 --- a/internal/api/resolver_model_studio.go +++ b/internal/api/resolver_model_studio.go @@ -9,6 +9,7 @@ import ( "github.com/stashapp/stash/pkg/gallery" "github.com/stashapp/stash/pkg/image" "github.com/stashapp/stash/pkg/models" + "github.com/stashapp/stash/pkg/performer" ) func (r *studioResolver) Name(ctx context.Context, obj *models.Studio) (string, error) { @@ -93,6 +94,18 @@ func (r *studioResolver) GalleryCount(ctx context.Context, obj *models.Studio) ( return &res, nil } +func (r *studioResolver) PerformerCount(ctx context.Context, obj *models.Studio) (ret *int, err error) { + var res int + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + res, err = performer.CountByStudioID(ctx, r.repository.Performer, obj.ID) + return err + }); err != nil { + return nil, err + } + + return &res, nil +} + func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) { if !obj.ParentID.Valid { return nil, nil diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index 96891d69d..fc46bc323 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "path/filepath" + "regexp" "github.com/stashapp/stash/internal/manager" "github.com/stashapp/stash/internal/manager/config" @@ -122,6 +123,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen c.Set(config.Metadata, input.MetadataPath) } + refreshStreamManager := false existingCachePath := c.GetCachePath() if input.CachePath != nil && existingCachePath != *input.CachePath { if err := validateDir(config.Cache, *input.CachePath, true); err != nil { @@ -129,6 +131,29 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen } c.Set(config.Cache, input.CachePath) + refreshStreamManager = true + } + + refreshBlobStorage := false + existingBlobsPath := c.GetBlobsPath() + if input.BlobsPath != nil && existingBlobsPath != *input.BlobsPath { + if err := validateDir(config.BlobsPath, *input.BlobsPath, true); err != nil { + return makeConfigGeneralResult(), err + } + + c.Set(config.BlobsPath, input.BlobsPath) + refreshBlobStorage = true + } + + if input.BlobsStorage != nil && *input.BlobsStorage != c.GetBlobsStorage() { + if *input.BlobsStorage == config.BlobStorageTypeFilesystem && c.GetBlobsPath() == "" { + return makeConfigGeneralResult(), fmt.Errorf("blobs path must be set when using filesystem storage") + } + + // TODO - migrate between systems + c.Set(config.BlobsStorage, input.BlobsStorage) + + refreshBlobStorage = true } if input.VideoFileNamingAlgorithm != nil && *input.VideoFileNamingAlgorithm != c.GetVideoFileNamingAlgorithm() { @@ -178,6 +203,9 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen c.Set(config.PreviewPreset, input.PreviewPreset.String()) } + if input.TranscodeHardwareAcceleration != nil { + c.Set(config.TranscodeHardwareAcceleration, *input.TranscodeHardwareAcceleration) + } if input.MaxTranscodeSize != nil { c.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String()) } @@ -190,6 +218,16 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen c.Set(config.WriteImageThumbnails, *input.WriteImageThumbnails) } + if input.GalleryCoverRegex != nil { + + _, err := regexp.Compile(*input.GalleryCoverRegex) + if err != nil { + return makeConfigGeneralResult(), fmt.Errorf("Gallery cover regex '%v' invalid, '%v'", *input.GalleryCoverRegex, err.Error()) + } + + c.Set(config.GalleryCoverRegex, *input.GalleryCoverRegex) + } + if input.Username != nil { c.Set(config.Username, input.Username) } @@ -227,10 +265,22 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen } if input.Excludes != nil { + for _, r := range input.Excludes { + _, err := regexp.Compile(r) + if err != nil { + return makeConfigGeneralResult(), fmt.Errorf("video exclusion pattern '%v' invalid: %w", r, err) + } + } c.Set(config.Exclude, input.Excludes) } if input.ImageExcludes != nil { + for _, r := range input.ImageExcludes { + _, err := regexp.Compile(r) + if err != nil { + return makeConfigGeneralResult(), fmt.Errorf("image/gallery exclusion pattern '%v' invalid: %w", r, err) + } + } c.Set(config.ImageExclude, input.ImageExcludes) } @@ -280,6 +330,23 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen c.Set(config.PythonPath, input.PythonPath) } + if input.TranscodeInputArgs != nil { + c.Set(config.TranscodeInputArgs, input.TranscodeInputArgs) + } + if input.TranscodeOutputArgs != nil { + c.Set(config.TranscodeOutputArgs, input.TranscodeOutputArgs) + } + if input.LiveTranscodeInputArgs != nil { + c.Set(config.LiveTranscodeInputArgs, input.LiveTranscodeInputArgs) + } + if input.LiveTranscodeOutputArgs != nil { + c.Set(config.LiveTranscodeOutputArgs, input.LiveTranscodeOutputArgs) + } + + if input.DrawFunscriptHeatmapRange != nil { + c.Set(config.DrawFunscriptHeatmapRange, input.DrawFunscriptHeatmapRange) + } + if err := c.Write(); err != nil { return makeConfigGeneralResult(), err } @@ -288,6 +355,12 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen if refreshScraperCache { manager.GetInstance().RefreshScraperCache() } + if refreshStreamManager { + manager.GetInstance().RefreshStreamManager() + } + if refreshBlobStorage { + manager.GetInstance().SetBlobStoreOptions() + } return makeConfigGeneralResult(), nil } @@ -451,6 +524,12 @@ func (r *mutationResolver) ConfigureScraping(ctx context.Context, input ConfigSc } if input.ExcludeTagPatterns != nil { + for _, r := range input.ExcludeTagPatterns { + _, err := regexp.Compile(r) + if err != nil { + return makeConfigScrapingResult(), fmt.Errorf("tag exclusion pattern '%v' invalid: %w", r, err) + } + } c.Set(config.ScraperExcludeTagPatterns, input.ExcludeTagPatterns) } @@ -476,6 +555,8 @@ func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input ConfigDe } if input.Scan != nil { + // if input.Scan is used then ScanMetadataOptions is included in the config file + // this causes the values to not be read correctly c.Set(config.DefaultScanSettings, input.Scan.ScanMetadataOptions) } diff --git a/internal/api/resolver_mutation_file.go b/internal/api/resolver_mutation_file.go index 1a9fd66a0..0b8b84ea0 100644 --- a/internal/api/resolver_mutation_file.go +++ b/internal/api/resolver_mutation_file.go @@ -3,11 +3,145 @@ package api import ( "context" "fmt" + "strconv" + "github.com/stashapp/stash/internal/manager" "github.com/stashapp/stash/pkg/file" + "github.com/stashapp/stash/pkg/fsutil" "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) +func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput) (bool, error) { + if err := r.withTxn(ctx, func(ctx context.Context) error { + fileStore := r.repository.File + folderStore := r.repository.Folder + mover := file.NewMover(fileStore, folderStore) + mover.RegisterHooks(ctx, r.txnManager) + + var ( + folder *file.Folder + basename string + ) + + fileIDs, err := stringslice.StringSliceToIntSlice(input.Ids) + if err != nil { + return fmt.Errorf("converting file ids: %w", err) + } + + switch { + case input.DestinationFolderID != nil: + var err error + + folderID, err := strconv.Atoi(*input.DestinationFolderID) + if err != nil { + return fmt.Errorf("invalid folder id %s: %w", *input.DestinationFolderID, err) + } + + folder, err = folderStore.Find(ctx, file.FolderID(folderID)) + if err != nil { + return fmt.Errorf("finding destination folder: %w", err) + } + + if folder == nil { + return fmt.Errorf("folder with id %d not found", input.DestinationFolderID) + } + + if folder.ZipFileID != nil { + return fmt.Errorf("cannot move to %s, is in a zip file", folder.Path) + } + case input.DestinationFolder != nil: + folderPath := *input.DestinationFolder + + // ensure folder path is within the library + if err := r.validateFolderPath(folderPath); err != nil { + return err + } + + // get or create folder hierarchy + var err error + folder, err = file.GetOrCreateFolderHierarchy(ctx, folderStore, folderPath) + if err != nil { + return fmt.Errorf("getting or creating folder hierarchy: %w", err) + } + default: + return fmt.Errorf("must specify destination folder or path") + } + + if input.DestinationBasename != nil { + // ensure only one file was supplied + if len(input.Ids) != 1 { + return fmt.Errorf("must specify one file when providing destination path") + } + + basename = *input.DestinationBasename + } + + // create the folder hierarchy in the filesystem if needed + if err := mover.CreateFolderHierarchy(folder.Path); err != nil { + return fmt.Errorf("creating folder hierarchy %s in filesystem: %w", folder.Path, err) + } + + for _, fileIDInt := range fileIDs { + fileID := file.ID(fileIDInt) + f, err := fileStore.Find(ctx, fileID) + if err != nil { + return fmt.Errorf("finding file %d: %w", fileID, err) + } + + // ensure that the file extension matches the existing file type + if basename != "" { + if err := r.validateFileExtension(f[0].Base().Basename, basename); err != nil { + return err + } + } + + if err := mover.Move(ctx, f[0], folder, basename); err != nil { + return err + } + } + + return nil + }); err != nil { + return false, err + } + + return true, nil +} + +func (r *mutationResolver) validateFolderPath(folderPath string) error { + paths := manager.GetInstance().Config.GetStashPaths() + if l := paths.GetStashFromDirPath(folderPath); l == nil { + return fmt.Errorf("folder path %s must be within a stash library path", folderPath) + } + + return nil +} + +func (r *mutationResolver) validateFileExtension(oldBasename, newBasename string) error { + c := manager.GetInstance().Config + if err := r.validateFileExtensionList(c.GetVideoExtensions(), oldBasename, newBasename); err != nil { + return err + } + + if err := r.validateFileExtensionList(c.GetImageExtensions(), oldBasename, newBasename); err != nil { + return err + } + + if err := r.validateFileExtensionList(c.GetGalleryExtensions(), oldBasename, newBasename); err != nil { + return err + } + + return nil +} + +func (r *mutationResolver) validateFileExtensionList(exts []string, oldBasename, newBasename string) error { + if fsutil.MatchExtension(oldBasename, exts) && !fsutil.MatchExtension(newBasename, exts) { + return fmt.Errorf("file extension for %s is inconsistent with old filename %s", newBasename, oldBasename) + } + + return nil +} + func (r *mutationResolver) DeleteFiles(ctx context.Context, ids []string) (ret bool, err error) { fileIDs, err := stringslice.StringSliceToIntSlice(ids) if err != nil { diff --git a/internal/api/resolver_mutation_gallery.go b/internal/api/resolver_mutation_gallery.go index 51ff989a3..aad2efe5d 100644 --- a/internal/api/resolver_mutation_gallery.go +++ b/internal/api/resolver_mutation_gallery.go @@ -2,6 +2,7 @@ package api import ( "context" + "database/sql" "errors" "fmt" "os" @@ -10,6 +11,7 @@ import ( "github.com/stashapp/stash/internal/manager" "github.com/stashapp/stash/pkg/file" + "github.com/stashapp/stash/pkg/gallery" "github.com/stashapp/stash/pkg/image" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/plugin" @@ -489,3 +491,150 @@ func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input Galler return true, nil } + +func (r *mutationResolver) getGalleryChapter(ctx context.Context, id int) (ret *models.GalleryChapter, err error) { + if err := r.withTxn(ctx, func(ctx context.Context) error { + ret, err = r.repository.GalleryChapter.Find(ctx, id) + return err + }); err != nil { + return nil, err + } + + return ret, nil +} + +func (r *mutationResolver) GalleryChapterCreate(ctx context.Context, input GalleryChapterCreateInput) (*models.GalleryChapter, error) { + galleryID, err := strconv.Atoi(input.GalleryID) + if err != nil { + return nil, err + } + + var imageCount int + if err := r.withTxn(ctx, func(ctx context.Context) error { + imageCount, err = r.repository.Image.CountByGalleryID(ctx, galleryID) + return err + }); err != nil { + return nil, err + } + // Sanity Check of Index + if input.ImageIndex > imageCount || input.ImageIndex < 1 { + return nil, errors.New("Image # must greater than zero and in range of the gallery images") + } + + currentTime := time.Now() + newGalleryChapter := models.GalleryChapter{ + Title: input.Title, + ImageIndex: input.ImageIndex, + GalleryID: sql.NullInt64{Int64: int64(galleryID), Valid: galleryID != 0}, + CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime}, + UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime}, + } + + if err != nil { + return nil, err + } + + ret, err := r.changeChapter(ctx, create, newGalleryChapter) + if err != nil { + return nil, err + } + + r.hookExecutor.ExecutePostHooks(ctx, ret.ID, plugin.GalleryChapterCreatePost, input, nil) + return r.getGalleryChapter(ctx, ret.ID) +} + +func (r *mutationResolver) GalleryChapterUpdate(ctx context.Context, input GalleryChapterUpdateInput) (*models.GalleryChapter, error) { + // Populate gallery chapter from the input + galleryChapterID, err := strconv.Atoi(input.ID) + if err != nil { + return nil, err + } + + galleryID, err := strconv.Atoi(input.GalleryID) + if err != nil { + return nil, err + } + + var imageCount int + if err := r.withTxn(ctx, func(ctx context.Context) error { + imageCount, err = r.repository.Image.CountByGalleryID(ctx, galleryID) + return err + }); err != nil { + return nil, err + } + // Sanity Check of Index + if input.ImageIndex > imageCount || input.ImageIndex < 1 { + return nil, errors.New("Image # must greater than zero and in range of the gallery images") + } + + updatedGalleryChapter := models.GalleryChapter{ + ID: galleryChapterID, + Title: input.Title, + ImageIndex: input.ImageIndex, + GalleryID: sql.NullInt64{Int64: int64(galleryID), Valid: galleryID != 0}, + UpdatedAt: models.SQLiteTimestamp{Timestamp: time.Now()}, + } + + ret, err := r.changeChapter(ctx, update, updatedGalleryChapter) + if err != nil { + return nil, err + } + + translator := changesetTranslator{ + inputMap: getUpdateInputMap(ctx), + } + r.hookExecutor.ExecutePostHooks(ctx, ret.ID, plugin.GalleryChapterUpdatePost, input, translator.getFields()) + return r.getGalleryChapter(ctx, ret.ID) +} + +func (r *mutationResolver) GalleryChapterDestroy(ctx context.Context, id string) (bool, error) { + chapterID, err := strconv.Atoi(id) + if err != nil { + return false, err + } + + if err := r.withTxn(ctx, func(ctx context.Context) error { + qb := r.repository.GalleryChapter + + chapter, err := qb.Find(ctx, chapterID) + + if err != nil { + return err + } + + if chapter == nil { + return fmt.Errorf("Chapter with id %d not found", chapterID) + } + + return gallery.DestroyChapter(ctx, chapter, qb) + }); err != nil { + return false, err + } + + r.hookExecutor.ExecutePostHooks(ctx, chapterID, plugin.GalleryChapterDestroyPost, id, nil) + + return true, nil +} + +func (r *mutationResolver) changeChapter(ctx context.Context, changeType int, changedChapter models.GalleryChapter) (*models.GalleryChapter, error) { + var galleryChapter *models.GalleryChapter + + // Start the transaction and save the gallery chapter + var err = r.withTxn(ctx, func(ctx context.Context) error { + qb := r.repository.GalleryChapter + var err error + + switch changeType { + case create: + galleryChapter, err = qb.Create(ctx, changedChapter) + case update: + galleryChapter, err = qb.Update(ctx, changedChapter) + if err != nil { + return err + } + } + return err + }) + + return galleryChapter, err +} diff --git a/internal/api/resolver_mutation_image.go b/internal/api/resolver_mutation_image.go index 135888f8f..6a482ff04 100644 --- a/internal/api/resolver_mutation_image.go +++ b/internal/api/resolver_mutation_image.go @@ -104,6 +104,8 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp updatedImage := models.NewImagePartial() updatedImage.Title = translator.optionalString(input.Title, "title") updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) + updatedImage.URL = translator.optionalString(input.URL, "url") + updatedImage.Date = translator.optionalDate(input.Date, "date") updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id") if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) @@ -190,6 +192,8 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU updatedImage.Title = translator.optionalString(input.Title, "title") updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) + updatedImage.URL = translator.optionalString(input.URL, "url") + updatedImage.Date = translator.optionalDate(input.Date, "date") updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id") if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) diff --git a/internal/api/resolver_mutation_metadata.go b/internal/api/resolver_mutation_metadata.go index 040dc9fc1..6b0eba66f 100644 --- a/internal/api/resolver_mutation_metadata.go +++ b/internal/api/resolver_mutation_metadata.go @@ -156,3 +156,55 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab return nil, nil } + +func (r *mutationResolver) AnonymiseDatabase(ctx context.Context, input AnonymiseDatabaseInput) (*string, error) { + // if download is true, then backup to temporary file and return a link + download := input.Download != nil && *input.Download + mgr := manager.GetInstance() + database := mgr.Database + var outPath string + if download { + if err := fsutil.EnsureDir(mgr.Paths.Generated.Downloads); err != nil { + return nil, fmt.Errorf("could not create backup directory %v: %w", mgr.Paths.Generated.Downloads, err) + } + f, err := os.CreateTemp(mgr.Paths.Generated.Downloads, "anonymous*.sqlite") + if err != nil { + return nil, err + } + + outPath = f.Name() + f.Close() + } else { + backupDirectoryPath := mgr.Config.GetBackupDirectoryPathOrDefault() + if backupDirectoryPath != "" { + if err := fsutil.EnsureDir(backupDirectoryPath); err != nil { + return nil, fmt.Errorf("could not create backup directory %v: %w", backupDirectoryPath, err) + } + } + outPath = database.AnonymousDatabasePath(backupDirectoryPath) + } + + err := database.Anonymise(outPath) + if err != nil { + logger.Errorf("Error anonymising database: %v", err) + return nil, err + } + + if download { + downloadHash, err := mgr.DownloadStore.RegisterFile(outPath, "", false) + if err != nil { + return nil, fmt.Errorf("error registering file for download: %w", err) + } + logger.Debugf("Generated anonymised file %s with hash %s", outPath, downloadHash) + + baseURL, _ := ctx.Value(BaseURLCtxKey).(string) + + fn := filepath.Base(database.DatabaseBackupPath("")) + ret := baseURL + "/downloads/" + downloadHash + "/" + fn + return &ret, nil + } else { + logger.Infof("Successfully anonymised database to: %s", outPath) + } + + return nil, nil +} diff --git a/internal/api/resolver_mutation_migrate.go b/internal/api/resolver_mutation_migrate.go new file mode 100644 index 000000000..477f46b44 --- /dev/null +++ b/internal/api/resolver_mutation_migrate.go @@ -0,0 +1,40 @@ +package api + +import ( + "context" + "strconv" + + "github.com/stashapp/stash/internal/manager" + "github.com/stashapp/stash/internal/manager/task" + "github.com/stashapp/stash/pkg/scene" + "github.com/stashapp/stash/pkg/utils" +) + +func (r *mutationResolver) MigrateSceneScreenshots(ctx context.Context, input MigrateSceneScreenshotsInput) (string, error) { + db := manager.GetInstance().Database + t := &task.MigrateSceneScreenshotsJob{ + ScreenshotsPath: manager.GetInstance().Paths.Generated.Screenshots, + Input: scene.MigrateSceneScreenshotsInput{ + DeleteFiles: utils.IsTrue(input.DeleteFiles), + OverwriteExisting: utils.IsTrue(input.OverwriteExisting), + }, + SceneRepo: db.Scene, + TxnManager: db, + } + jobID := manager.GetInstance().JobManager.Add(ctx, "Migrating scene screenshots to blobs...", t) + + return strconv.Itoa(jobID), nil +} + +func (r *mutationResolver) MigrateBlobs(ctx context.Context, input MigrateBlobsInput) (string, error) { + db := manager.GetInstance().Database + t := &task.MigrateBlobsJob{ + TxnManager: db, + BlobStore: db.Blobs, + Vacuumer: db, + DeleteOld: utils.IsTrue(input.DeleteOld), + } + jobID := manager.GetInstance().JobManager.Add(ctx, "Migrating blobs...", t) + + return strconv.Itoa(jobID), nil +} diff --git a/internal/api/resolver_mutation_movie.go b/internal/api/resolver_mutation_movie.go index f3d3e529d..009e9bc92 100644 --- a/internal/api/resolver_mutation_movie.go +++ b/internal/api/resolver_mutation_movie.go @@ -111,7 +111,13 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp // update image table if len(frontimageData) > 0 { - if err := qb.UpdateImages(ctx, movie.ID, frontimageData, backimageData); err != nil { + if err := qb.UpdateFrontImage(ctx, movie.ID, frontimageData); err != nil { + return err + } + } + + if len(backimageData) > 0 { + if err := qb.UpdateBackImage(ctx, movie.ID, backimageData); err != nil { return err } } @@ -184,35 +190,15 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp } // update image table - if frontImageIncluded || backImageIncluded { - if !frontImageIncluded { - frontimageData, err = qb.GetFrontImage(ctx, updatedMovie.ID) - if err != nil { - return err - } - } - if !backImageIncluded { - backimageData, err = qb.GetBackImage(ctx, updatedMovie.ID) - if err != nil { - return err - } + if frontImageIncluded { + if err := qb.UpdateFrontImage(ctx, movie.ID, frontimageData); err != nil { + return err } + } - if len(frontimageData) == 0 && len(backimageData) == 0 { - // both images are being nulled. Destroy them. - if err := qb.DestroyImages(ctx, movie.ID); err != nil { - return err - } - } else { - // HACK - if front image is null and back image is not null, then set the front image - // to the default image since we can't have a null front image and a non-null back image - if frontimageData == nil && backimageData != nil { - frontimageData, _ = utils.ProcessImageInput(ctx, models.DefaultMovieImage) - } - - if err := qb.UpdateImages(ctx, movie.ID, frontimageData, backimageData); err != nil { - return err - } + if backImageIncluded { + if err := qb.UpdateBackImage(ctx, movie.ID, backimageData); err != nil { + return err } } diff --git a/internal/api/resolver_mutation_performer.go b/internal/api/resolver_mutation_performer.go index 33e440fd7..88aab07d0 100644 --- a/internal/api/resolver_mutation_performer.go +++ b/internal/api/resolver_mutation_performer.go @@ -6,7 +6,6 @@ import ( "strconv" "time" - "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/performer" "github.com/stashapp/stash/pkg/plugin" @@ -36,9 +35,6 @@ func stashIDPtrSliceToSlice(v []*models.StashID) []models.StashID { } func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerCreateInput) (*models.Performer, error) { - // generate checksum from performer name rather than image - checksum := md5.FromString(input.Name) - var imageData []byte var err error @@ -50,14 +46,23 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC return nil, err } + tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds) + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) + } + // Populate a new performer from the input currentTime := time.Now() newPerformer := models.Performer{ Name: input.Name, - Checksum: checksum, + TagIDs: models.NewRelatedIDs(tagIDs), + StashIDs: models.NewRelatedStashIDs(stashIDPtrSliceToSlice(input.StashIds)), CreatedAt: currentTime, UpdatedAt: currentTime, } + if input.Disambiguation != nil { + newPerformer.Disambiguation = *input.Disambiguation + } if input.URL != nil { newPerformer.URL = *input.URL } @@ -102,8 +107,10 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC if input.Piercings != nil { newPerformer.Piercings = *input.Piercings } - if input.Aliases != nil { - newPerformer.Aliases = *input.Aliases + if input.AliasList != nil { + newPerformer.Aliases = models.NewRelatedStrings(input.AliasList) + } else if input.Aliases != nil { + newPerformer.Aliases = models.NewRelatedStrings(stringslice.FromString(*input.Aliases, ",")) } if input.Twitter != nil { newPerformer.Twitter = *input.Twitter @@ -152,12 +159,6 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC return err } - if len(input.TagIds) > 0 { - if err := r.updatePerformerTags(ctx, newPerformer.ID, input.TagIds); err != nil { - return err - } - } - // update image table if len(imageData) > 0 { if err := qb.UpdateImage(ctx, newPerformer.ID, imageData); err != nil { @@ -165,14 +166,6 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC } } - // Save the stash_ids - if input.StashIds != nil { - stashIDJoins := stashIDPtrSliceToSlice(input.StashIds) - if err := qb.UpdateStashIDs(ctx, newPerformer.ID, stashIDJoins); err != nil { - return err - } - } - return nil }); err != nil { return nil, err @@ -201,14 +194,8 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU } } - if input.Name != nil { - // generate checksum from performer name rather than image - checksum := md5.FromString(*input.Name) - - updatedPerformer.Name = models.NewOptionalString(*input.Name) - updatedPerformer.Checksum = models.NewOptionalString(checksum) - } - + updatedPerformer.Name = translator.optionalString(input.Name, "name") + updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation") updatedPerformer.URL = translator.optionalString(input.URL, "url") if translator.hasField("gender") { @@ -238,7 +225,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length") updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos") updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings") - updatedPerformer.Aliases = translator.optionalString(input.Aliases, "aliases") updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter") updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram") updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite") @@ -249,6 +235,33 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight") updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") + if translator.hasField("alias_list") { + updatedPerformer.Aliases = &models.UpdateStrings{ + Values: input.AliasList, + Mode: models.RelationshipUpdateModeSet, + } + } else if translator.hasField("aliases") { + updatedPerformer.Aliases = &models.UpdateStrings{ + Values: stringslice.FromString(*input.Aliases, ","), + Mode: models.RelationshipUpdateModeSet, + } + } + + if translator.hasField("tag_ids") { + updatedPerformer.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet) + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) + } + } + + // Save the stash_ids + if translator.hasField("stash_ids") { + updatedPerformer.StashIDs = &models.UpdateStashIDs{ + StashIDs: stashIDPtrSliceToSlice(input.StashIds), + Mode: models.RelationshipUpdateModeSet, + } + } + // Start the transaction and save the p if err := r.withTxn(ctx, func(ctx context.Context) error { qb := r.repository.Performer @@ -274,13 +287,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU return err } - // Save the tags - if translator.hasField("tag_ids") { - if err := r.updatePerformerTags(ctx, performerID, input.TagIds); err != nil { - return err - } - } - // update image table if len(imageData) > 0 { if err := qb.UpdateImage(ctx, performerID, imageData); err != nil { @@ -293,14 +299,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU } } - // Save the stash_ids - if translator.hasField("stash_ids") { - stashIDJoins := stashIDPtrSliceToSlice(input.StashIds) - if err := qb.UpdateStashIDs(ctx, performerID, stashIDJoins); err != nil { - return err - } - } - return nil }); err != nil { return nil, err @@ -310,14 +308,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU return r.getPerformer(ctx, performerID) } -func (r *mutationResolver) updatePerformerTags(ctx context.Context, performerID int, tagsIDs []string) error { - ids, err := stringslice.StringSliceToIntSlice(tagsIDs) - if err != nil { - return err - } - return r.repository.Performer.UpdateTags(ctx, performerID, ids) -} - func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPerformerUpdateInput) ([]*models.Performer, error) { performerIDs, err := stringslice.StringSliceToIntSlice(input.Ids) if err != nil { @@ -331,6 +321,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe updatedPerformer := models.NewPerformerPartial() + updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation") updatedPerformer.URL = translator.optionalString(input.URL, "url") updatedPerformer.Birthdate = translator.optionalDate(input.Birthdate, "birthdate") updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity") @@ -351,7 +342,6 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length") updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos") updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings") - updatedPerformer.Aliases = translator.optionalString(input.Aliases, "aliases") updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter") updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram") updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite") @@ -362,6 +352,18 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight") updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") + if translator.hasField("alias_list") { + updatedPerformer.Aliases = &models.UpdateStrings{ + Values: input.AliasList.Values, + Mode: input.AliasList.Mode, + } + } else if translator.hasField("aliases") { + updatedPerformer.Aliases = &models.UpdateStrings{ + Values: stringslice.FromString(*input.Aliases, ","), + Mode: models.RelationshipUpdateModeSet, + } + } + if translator.hasField("gender") { if input.Gender != nil { updatedPerformer.Gender = models.NewOptionalString(input.Gender.String()) @@ -370,6 +372,13 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe } } + if translator.hasField("tag_ids") { + updatedPerformer.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode) + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) + } + } + ret := []*models.Performer{} // Start the transaction and save the scene marker @@ -399,18 +408,6 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe } ret = append(ret, performer) - - // Save the tags - if translator.hasField("tag_ids") { - tagIDs, err := adjustTagIDs(ctx, qb, performerID, *input.TagIds) - if err != nil { - return err - } - - if err := qb.UpdateTags(ctx, performerID, tagIDs); err != nil { - return err - } - } } return nil diff --git a/internal/api/resolver_mutation_scene.go b/internal/api/resolver_mutation_scene.go index 301bd3b8e..dfdb29507 100644 --- a/internal/api/resolver_mutation_scene.go +++ b/internal/api/resolver_mutation_scene.go @@ -9,14 +9,12 @@ import ( "time" "github.com/stashapp/stash/internal/manager" - "github.com/stashapp/stash/internal/manager/config" "github.com/stashapp/stash/pkg/file" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/plugin" "github.com/stashapp/stash/pkg/scene" "github.com/stashapp/stash/pkg/sliceutil/intslice" "github.com/stashapp/stash/pkg/sliceutil/stringslice" - "github.com/stashapp/stash/pkg/txn" "github.com/stashapp/stash/pkg/utils" ) @@ -320,13 +318,6 @@ func (r *mutationResolver) sceneUpdateCoverImage(ctx context.Context, s *models. if err := qb.UpdateCover(ctx, s.ID, coverImageData); err != nil { return err } - - if s.Path != "" { - // update the file-based screenshot after commit - txn.AddPostCommitHook(ctx, func(ctx context.Context) error { - return scene.SetScreenshot(manager.GetInstance().Paths, s.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), coverImageData) - }) - } } return nil @@ -423,56 +414,6 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU return newRet, nil } -func adjustIDs(existingIDs []int, updateIDs BulkUpdateIds) []int { - // if we are setting the ids, just return the ids - if updateIDs.Mode == models.RelationshipUpdateModeSet { - existingIDs = []int{} - for _, idStr := range updateIDs.Ids { - id, _ := strconv.Atoi(idStr) - existingIDs = append(existingIDs, id) - } - - return existingIDs - } - - for _, idStr := range updateIDs.Ids { - id, _ := strconv.Atoi(idStr) - - // look for the id in the list - foundExisting := false - for idx, existingID := range existingIDs { - if existingID == id { - if updateIDs.Mode == models.RelationshipUpdateModeRemove { - // remove from the list - existingIDs = append(existingIDs[:idx], existingIDs[idx+1:]...) - } - - foundExisting = true - break - } - } - - if !foundExisting && updateIDs.Mode != models.RelationshipUpdateModeRemove { - existingIDs = append(existingIDs, id) - } - } - - return existingIDs -} - -type tagIDsGetter interface { - GetTagIDs(ctx context.Context, id int) ([]int, error) -} - -func adjustTagIDs(ctx context.Context, qb tagIDsGetter, sceneID int, ids BulkUpdateIds) (ret []int, err error) { - ret, err = qb.GetTagIDs(ctx, sceneID) - if err != nil { - return nil, err - } - - return adjustIDs(ret, ids), nil -} - func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) { sceneID, err := strconv.Atoi(input.ID) if err != nil { diff --git a/internal/api/resolver_mutation_stash_box.go b/internal/api/resolver_mutation_stash_box.go index d1a7e2de2..92e0923e7 100644 --- a/internal/api/resolver_mutation_stash_box.go +++ b/internal/api/resolver_mutation_stash_box.go @@ -7,6 +7,7 @@ import ( "github.com/stashapp/stash/internal/manager" "github.com/stashapp/stash/internal/manager/config" + "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/scraper/stashbox" ) @@ -58,9 +59,16 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input S return err } - filepath := manager.GetInstance().Paths.Scene.GetScreenshotPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())) + if scene == nil { + return fmt.Errorf("scene with id %d not found", id) + } - res, err = client.SubmitSceneDraft(ctx, scene, boxes[input.StashBoxIndex].Endpoint, filepath) + cover, err := qb.GetCover(ctx, id) + if err != nil { + logger.Errorf("Error getting scene cover: %v", err) + } + + res, err = client.SubmitSceneDraft(ctx, scene, boxes[input.StashBoxIndex].Endpoint, cover) return err }) diff --git a/internal/api/resolver_mutation_studio.go b/internal/api/resolver_mutation_studio.go index 98c871323..f9862d9be 100644 --- a/internal/api/resolver_mutation_studio.go +++ b/internal/api/resolver_mutation_studio.go @@ -176,15 +176,10 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateI } // update image table - if len(imageData) > 0 { + if imageIncluded { if err := qb.UpdateImage(ctx, s.ID, imageData); err != nil { return err } - } else if imageIncluded { - // must be unsetting - if err := qb.DestroyImage(ctx, s.ID); err != nil { - return err - } } // Save the stash_ids diff --git a/internal/api/resolver_mutation_tag.go b/internal/api/resolver_mutation_tag.go index 47986c56e..04f10ce88 100644 --- a/internal/api/resolver_mutation_tag.go +++ b/internal/api/resolver_mutation_tag.go @@ -208,15 +208,10 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput) } // update image table - if len(imageData) > 0 { + if imageIncluded { if err := qb.UpdateImage(ctx, tagID, imageData); err != nil { return err } - } else if imageIncluded { - // must be unsetting - if err := qb.DestroyImage(ctx, tagID); err != nil { - return err - } } if translator.hasField("aliases") { diff --git a/internal/api/resolver_query_configuration.go b/internal/api/resolver_query_configuration.go index 941fb9a49..fd598ce92 100644 --- a/internal/api/resolver_query_configuration.go +++ b/internal/api/resolver_query_configuration.go @@ -83,46 +83,55 @@ func makeConfigGeneralResult() *ConfigGeneralResult { scraperCDPPath := config.GetScraperCDPPath() return &ConfigGeneralResult{ - Stashes: config.GetStashPaths(), - DatabasePath: config.GetDatabasePath(), - BackupDirectoryPath: config.GetBackupDirectoryPath(), - GeneratedPath: config.GetGeneratedPath(), - MetadataPath: config.GetMetadataPath(), - ConfigFilePath: config.GetConfigFile(), - ScrapersPath: config.GetScrapersPath(), - CachePath: config.GetCachePath(), - CalculateMd5: config.IsCalculateMD5(), - VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(), - ParallelTasks: config.GetParallelTasks(), - PreviewAudio: config.GetPreviewAudio(), - PreviewSegments: config.GetPreviewSegments(), - PreviewSegmentDuration: config.GetPreviewSegmentDuration(), - PreviewExcludeStart: config.GetPreviewExcludeStart(), - PreviewExcludeEnd: config.GetPreviewExcludeEnd(), - PreviewPreset: config.GetPreviewPreset(), - MaxTranscodeSize: &maxTranscodeSize, - MaxStreamingTranscodeSize: &maxStreamingTranscodeSize, - WriteImageThumbnails: config.IsWriteImageThumbnails(), - APIKey: config.GetAPIKey(), - Username: config.GetUsername(), - Password: config.GetPasswordHash(), - MaxSessionAge: config.GetMaxSessionAge(), - LogFile: &logFile, - LogOut: config.GetLogOut(), - LogLevel: config.GetLogLevel(), - LogAccess: config.GetLogAccess(), - VideoExtensions: config.GetVideoExtensions(), - ImageExtensions: config.GetImageExtensions(), - GalleryExtensions: config.GetGalleryExtensions(), - CreateGalleriesFromFolders: config.GetCreateGalleriesFromFolders(), - Excludes: config.GetExcludes(), - ImageExcludes: config.GetImageExcludes(), - CustomPerformerImageLocation: &customPerformerImageLocation, - ScraperUserAgent: &scraperUserAgent, - ScraperCertCheck: config.GetScraperCertCheck(), - ScraperCDPPath: &scraperCDPPath, - StashBoxes: config.GetStashBoxes(), - PythonPath: config.GetPythonPath(), + Stashes: config.GetStashPaths(), + DatabasePath: config.GetDatabasePath(), + BackupDirectoryPath: config.GetBackupDirectoryPath(), + GeneratedPath: config.GetGeneratedPath(), + MetadataPath: config.GetMetadataPath(), + ConfigFilePath: config.GetConfigFile(), + ScrapersPath: config.GetScrapersPath(), + CachePath: config.GetCachePath(), + BlobsPath: config.GetBlobsPath(), + BlobsStorage: config.GetBlobsStorage(), + CalculateMd5: config.IsCalculateMD5(), + VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(), + ParallelTasks: config.GetParallelTasks(), + PreviewAudio: config.GetPreviewAudio(), + PreviewSegments: config.GetPreviewSegments(), + PreviewSegmentDuration: config.GetPreviewSegmentDuration(), + PreviewExcludeStart: config.GetPreviewExcludeStart(), + PreviewExcludeEnd: config.GetPreviewExcludeEnd(), + PreviewPreset: config.GetPreviewPreset(), + TranscodeHardwareAcceleration: config.GetTranscodeHardwareAcceleration(), + MaxTranscodeSize: &maxTranscodeSize, + MaxStreamingTranscodeSize: &maxStreamingTranscodeSize, + WriteImageThumbnails: config.IsWriteImageThumbnails(), + GalleryCoverRegex: config.GetGalleryCoverRegex(), + APIKey: config.GetAPIKey(), + Username: config.GetUsername(), + Password: config.GetPasswordHash(), + MaxSessionAge: config.GetMaxSessionAge(), + LogFile: &logFile, + LogOut: config.GetLogOut(), + LogLevel: config.GetLogLevel(), + LogAccess: config.GetLogAccess(), + VideoExtensions: config.GetVideoExtensions(), + ImageExtensions: config.GetImageExtensions(), + GalleryExtensions: config.GetGalleryExtensions(), + CreateGalleriesFromFolders: config.GetCreateGalleriesFromFolders(), + Excludes: config.GetExcludes(), + ImageExcludes: config.GetImageExcludes(), + CustomPerformerImageLocation: &customPerformerImageLocation, + ScraperUserAgent: &scraperUserAgent, + ScraperCertCheck: config.GetScraperCertCheck(), + ScraperCDPPath: &scraperCDPPath, + StashBoxes: config.GetStashBoxes(), + PythonPath: config.GetPythonPath(), + TranscodeInputArgs: config.GetTranscodeInputArgs(), + TranscodeOutputArgs: config.GetTranscodeOutputArgs(), + LiveTranscodeInputArgs: config.GetLiveTranscodeInputArgs(), + LiveTranscodeOutputArgs: config.GetLiveTranscodeOutputArgs(), + DrawFunscriptHeatmapRange: config.GetDrawFunscriptHeatmapRange(), } } diff --git a/internal/api/resolver_query_find_gallery.go b/internal/api/resolver_query_find_gallery.go index e8d47d70b..db1fcafaf 100644 --- a/internal/api/resolver_query_find_gallery.go +++ b/internal/api/resolver_query_find_gallery.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/stashapp/stash/pkg/models" @@ -16,7 +18,7 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Gallery.Find(ctx, idInt) return err - }); err != nil { + }); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } @@ -41,3 +43,14 @@ func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models return ret, nil } + +func (r *queryResolver) AllGalleries(ctx context.Context) (ret []*models.Gallery, err error) { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + ret, err = r.repository.Gallery.All(ctx) + return err + }); err != nil { + return nil, err + } + + return ret, nil +} diff --git a/internal/api/resolver_query_find_image.go b/internal/api/resolver_query_find_image.go index 6468ba9f3..e979f3f11 100644 --- a/internal/api/resolver_query_find_image.go +++ b/internal/api/resolver_query_find_image.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/99designs/gqlgen/graphql" @@ -23,7 +25,7 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str } image, err = qb.Find(ctx, idInt) - if err != nil { + if err != nil && !errors.Is(err, sql.ErrNoRows) { return err } } else if checksum != nil { @@ -84,3 +86,14 @@ func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.Imag return ret, nil } + +func (r *queryResolver) AllImages(ctx context.Context) (ret []*models.Image, err error) { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + ret, err = r.repository.Image.All(ctx) + return err + }); err != nil { + return nil, err + } + + return ret, nil +} diff --git a/internal/api/resolver_query_find_movie.go b/internal/api/resolver_query_find_movie.go index a7e72dbdc..a728089cc 100644 --- a/internal/api/resolver_query_find_movie.go +++ b/internal/api/resolver_query_find_movie.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/stashapp/stash/pkg/models" @@ -16,7 +18,7 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Movie.Find(ctx, idInt) return err - }); err != nil { + }); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } @@ -34,7 +36,6 @@ func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.Movi Count: total, Movies: movies, } - return nil }); err != nil { return nil, err diff --git a/internal/api/resolver_query_find_performer.go b/internal/api/resolver_query_find_performer.go index 437ac8fcf..b94d67e94 100644 --- a/internal/api/resolver_query_find_performer.go +++ b/internal/api/resolver_query_find_performer.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/stashapp/stash/pkg/models" @@ -16,7 +18,7 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Performer.Find(ctx, idInt) return err - }); err != nil { + }); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } diff --git a/internal/api/resolver_query_find_saved_filter.go b/internal/api/resolver_query_find_saved_filter.go index 4f196fd65..6098decea 100644 --- a/internal/api/resolver_query_find_saved_filter.go +++ b/internal/api/resolver_query_find_saved_filter.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/stashapp/stash/pkg/models" @@ -16,7 +18,7 @@ func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *mo if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.SavedFilter.Find(ctx, idInt) return err - }); err != nil { + }); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } return ret, err @@ -40,7 +42,7 @@ func (r *queryResolver) FindDefaultFilter(ctx context.Context, mode models.Filte if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.SavedFilter.FindDefault(ctx, mode) return err - }); err != nil { + }); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } return ret, err diff --git a/internal/api/resolver_query_find_scene.go b/internal/api/resolver_query_find_scene.go index 95519cd49..1eaa2dc03 100644 --- a/internal/api/resolver_query_find_scene.go +++ b/internal/api/resolver_query_find_scene.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/99designs/gqlgen/graphql" @@ -21,7 +23,7 @@ func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *str return err } scene, err = qb.Find(ctx, idInt) - if err != nil { + if err != nil && !errors.Is(err, sql.ErrNoRows) { return err } } else if checksum != nil { @@ -232,3 +234,14 @@ func (r *queryResolver) FindDuplicateScenes(ctx context.Context, distance *int) return ret, nil } + +func (r *queryResolver) AllScenes(ctx context.Context) (ret []*models.Scene, err error) { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + ret, err = r.repository.Scene.All(ctx) + return err + }); err != nil { + return nil, err + } + + return ret, nil +} diff --git a/internal/api/resolver_query_find_scene_marker.go b/internal/api/resolver_query_find_scene_marker.go index 4bd70e658..895ebc057 100644 --- a/internal/api/resolver_query_find_scene_marker.go +++ b/internal/api/resolver_query_find_scene_marker.go @@ -24,3 +24,14 @@ func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter return ret, nil } + +func (r *queryResolver) AllSceneMarkers(ctx context.Context) (ret []*models.SceneMarker, err error) { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + ret, err = r.repository.SceneMarker.All(ctx) + return err + }); err != nil { + return nil, err + } + + return ret, nil +} diff --git a/internal/api/resolver_query_find_studio.go b/internal/api/resolver_query_find_studio.go index 51cac6208..3f4260bce 100644 --- a/internal/api/resolver_query_find_studio.go +++ b/internal/api/resolver_query_find_studio.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/stashapp/stash/pkg/models" @@ -17,7 +19,7 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models. var err error ret, err = r.repository.Studio.Find(ctx, idInt) return err - }); err != nil { + }); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } diff --git a/internal/api/resolver_query_find_tag.go b/internal/api/resolver_query_find_tag.go index fd4b04ad2..9ea16525a 100644 --- a/internal/api/resolver_query_find_tag.go +++ b/internal/api/resolver_query_find_tag.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/stashapp/stash/pkg/models" @@ -16,7 +18,7 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Tag.Find(ctx, idInt) return err - }); err != nil { + }); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } diff --git a/internal/api/resolver_query_scene.go b/internal/api/resolver_query_scene.go index f4dea464e..120998d71 100644 --- a/internal/api/resolver_query_scene.go +++ b/internal/api/resolver_query_scene.go @@ -7,7 +7,6 @@ import ( "github.com/stashapp/stash/internal/api/urlbuilders" "github.com/stashapp/stash/internal/manager" - "github.com/stashapp/stash/internal/manager/config" "github.com/stashapp/stash/pkg/models" ) @@ -32,8 +31,11 @@ func (r *queryResolver) SceneStreams(ctx context.Context, id *string) ([]*manage return nil, errors.New("nil scene") } + config := manager.GetInstance().Config + baseURL, _ := ctx.Value(BaseURLCtxKey).(string) builder := urlbuilders.NewSceneURLBuilder(baseURL, scene.ID) + apiKey := config.GetAPIKey() - return manager.GetSceneStreamPaths(scene, builder.GetStreamURL(), config.GetInstance().GetMaxStreamingTranscodeSize()) + return manager.GetSceneStreamPaths(scene, builder.GetStreamURL(apiKey), config.GetMaxStreamingTranscodeSize()) } diff --git a/internal/api/routes_movie.go b/internal/api/routes_movie.go index c29718566..7b77586a6 100644 --- a/internal/api/routes_movie.go +++ b/internal/api/routes_movie.go @@ -42,8 +42,9 @@ func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) { var image []byte if defaultParam != "true" { readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { - image, _ = rs.movieFinder.GetFrontImage(ctx, movie.ID) - return nil + var err error + image, err = rs.movieFinder.GetFrontImage(ctx, movie.ID) + return err }) if errors.Is(readTxnErr, context.Canceled) { return @@ -68,8 +69,9 @@ func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) { var image []byte if defaultParam != "true" { readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { - image, _ = rs.movieFinder.GetBackImage(ctx, movie.ID) - return nil + var err error + image, err = rs.movieFinder.GetBackImage(ctx, movie.ID) + return err }) if errors.Is(readTxnErr, context.Canceled) { return diff --git a/internal/api/routes_performer.go b/internal/api/routes_performer.go index c8295467a..1717e99f9 100644 --- a/internal/api/routes_performer.go +++ b/internal/api/routes_performer.go @@ -42,8 +42,9 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) { var image []byte if defaultParam != "true" { readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { - image, _ = rs.performerFinder.GetImage(ctx, performer.ID) - return nil + var err error + image, err = rs.performerFinder.GetImage(ctx, performer.ID) + return err }) if errors.Is(readTxnErr, context.Canceled) { return diff --git a/internal/api/routes_scene.go b/internal/api/routes_scene.go index d1b1b02c8..c01c43104 100644 --- a/internal/api/routes_scene.go +++ b/internal/api/routes_scene.go @@ -56,16 +56,21 @@ func (rs sceneRoutes) Routes() chi.Router { // streaming endpoints r.Get("/stream", rs.StreamDirect) - r.Get("/stream.mkv", rs.StreamMKV) - r.Get("/stream.webm", rs.StreamWebM) - r.Get("/stream.m3u8", rs.StreamHLS) - r.Get("/stream.ts", rs.StreamTS) r.Get("/stream.mp4", rs.StreamMp4) + r.Get("/stream.webm", rs.StreamWebM) + r.Get("/stream.mkv", rs.StreamMKV) + r.Get("/stream.m3u8", rs.StreamHLS) + r.Get("/stream.m3u8/{segment}.ts", rs.StreamHLSSegment) + r.Get("/stream.mpd", rs.StreamDASH) + r.Get("/stream.mpd/{segment}_v.webm", rs.StreamDASHVideoSegment) + r.Get("/stream.mpd/{segment}_a.webm", rs.StreamDASHAudioSegment) r.Get("/screenshot", rs.Screenshot) r.Get("/preview", rs.Preview) r.Get("/webp", rs.Webp) - r.Get("/vtt/chapter", rs.ChapterVtt) + r.Get("/vtt/chapter", rs.VttChapter) + r.Get("/vtt/thumbs", rs.VttThumbs) + r.Get("/vtt/sprite", rs.VttSprite) r.Get("/funscript", rs.Funscript) r.Get("/interactive_heatmap", rs.InteractiveHeatmap) r.Get("/caption", rs.CaptionLang) @@ -74,8 +79,8 @@ func (rs sceneRoutes) Routes() chi.Router { r.Get("/scene_marker/{sceneMarkerId}/preview", rs.SceneMarkerPreview) r.Get("/scene_marker/{sceneMarkerId}/screenshot", rs.SceneMarkerScreenshot) }) - r.With(rs.SceneCtx).Get("/{sceneId}_thumbs.vtt", rs.VttThumbs) - r.With(rs.SceneCtx).Get("/{sceneId}_sprite.jpg", rs.VttSprite) + r.Get("/{sceneHash}_thumbs.vtt", rs.VttThumbs) + r.Get("/{sceneHash}_sprite.jpg", rs.VttSprite) return r } @@ -83,13 +88,32 @@ func (rs sceneRoutes) Routes() chi.Router { // region Handlers func (rs sceneRoutes) StreamDirect(w http.ResponseWriter, r *http.Request) { - scene := r.Context().Value(sceneKey).(*models.Scene) - ss := manager.SceneServer{ - TxnManager: rs.txnManager, - SceneCoverGetter: rs.sceneFinder, + scene := r.Context().Value(sceneKey).(*models.Scene) + // #3526 - return 404 if the scene does not have any files + if scene.Path == "" { + w.WriteHeader(http.StatusNotFound) + return } - ss.StreamSceneDirect(scene, w, r) + + sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()) + + filepath := manager.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. + _ = manager.GetInstance().ReadLockManager.ReadLock(streamRequestCtx, filepath) + http.ServeFile(w, r, filepath) +} + +func (rs sceneRoutes) StreamMp4(w http.ResponseWriter, r *http.Request) { + rs.streamTranscode(w, r, ffmpeg.StreamTypeMP4) +} + +func (rs sceneRoutes) StreamWebM(w http.ResponseWriter, r *http.Request) { + rs.streamTranscode(w, r, ffmpeg.StreamTypeWEBM) } func (rs sceneRoutes) StreamMKV(w http.ResponseWriter, r *http.Request) { @@ -114,118 +138,118 @@ func (rs sceneRoutes) StreamMKV(w http.ResponseWriter, r *http.Request) { return } - rs.streamTranscode(w, r, ffmpeg.StreamFormatMKVAudio) + rs.streamTranscode(w, r, ffmpeg.StreamTypeMKV) } -func (rs sceneRoutes) StreamWebM(w http.ResponseWriter, r *http.Request) { - rs.streamTranscode(w, r, ffmpeg.StreamFormatVP9) -} - -func (rs sceneRoutes) StreamMp4(w http.ResponseWriter, r *http.Request) { - rs.streamTranscode(w, r, ffmpeg.StreamFormatH264) -} - -func (rs sceneRoutes) StreamHLS(w http.ResponseWriter, r *http.Request) { +func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, streamType ffmpeg.StreamFormat) { scene := r.Context().Value(sceneKey).(*models.Scene) - pf := scene.Files.Primary() - if pf == nil { + streamManager := manager.GetInstance().StreamManager + if streamManager == nil { + http.Error(w, "Live transcoding disabled", http.StatusServiceUnavailable) return } - logger.Debug("Returning HLS playlist") - - // getting the playlist manifest only - w.Header().Set("Content-Type", ffmpeg.MimeHLS) - var str strings.Builder - - ffmpeg.WriteHLSPlaylist(pf.Duration, r.URL.String(), &str) - - requestByteRange := createByteRange(r.Header.Get("Range")) - if requestByteRange.RawString != "" { - logger.Debugf("Requested range: %s", requestByteRange.RawString) - } - - ret := requestByteRange.apply([]byte(str.String())) - rangeStr := requestByteRange.toHeaderValue(int64(str.Len())) - w.Header().Set("Content-Range", rangeStr) - - if n, err := w.Write(ret); err != nil { - logger.Warnf("[stream] error writing stream (wrote %v bytes): %v", n, err) - } -} - -func (rs sceneRoutes) StreamTS(w http.ResponseWriter, r *http.Request) { - rs.streamTranscode(w, r, ffmpeg.StreamFormatHLS) -} - -func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, streamFormat ffmpeg.StreamFormat) { - scene := r.Context().Value(sceneKey).(*models.Scene) - f := scene.Files.Primary() if f == nil { return } - logger.Debugf("Streaming as %s", streamFormat.MimeType) - // start stream based on query param, if provided if err := r.ParseForm(); err != nil { - logger.Warnf("[stream] error parsing query form: %v", err) + logger.Warnf("[transcode] error parsing query form: %v", err) } startTime := r.Form.Get("start") ss, _ := strconv.ParseFloat(startTime, 64) - requestedSize := r.Form.Get("resolution") + resolution := r.Form.Get("resolution") - audioCodec := ffmpeg.MissingUnsupported - if f.AudioCodec != "" { - audioCodec = ffmpeg.ProbeAudioCodec(f.AudioCodec) + options := ffmpeg.TranscodeOptions{ + StreamType: streamType, + VideoFile: f, + Resolution: resolution, + StartTime: ss, } - width := f.Width - height := f.Height + logger.Debugf("[transcode] streaming scene %d as %s", scene.ID, streamType.MimeType) + streamManager.ServeTranscode(w, r, options) +} - options := ffmpeg.TranscodeStreamOptions{ - Input: f.Path, - Codec: streamFormat, - VideoOnly: audioCodec == ffmpeg.MissingUnsupported, +func (rs sceneRoutes) StreamHLS(w http.ResponseWriter, r *http.Request) { + rs.streamManifest(w, r, ffmpeg.StreamTypeHLS, "HLS") +} - VideoWidth: width, - VideoHeight: height, +func (rs sceneRoutes) StreamDASH(w http.ResponseWriter, r *http.Request) { + rs.streamManifest(w, r, ffmpeg.StreamTypeDASHVideo, "DASH") +} - StartTime: ss, - MaxTranscodeSize: config.GetInstance().GetMaxStreamingTranscodeSize().GetMaxResolution(), - } +func (rs sceneRoutes) streamManifest(w http.ResponseWriter, r *http.Request, streamType *ffmpeg.StreamType, logName string) { + scene := r.Context().Value(sceneKey).(*models.Scene) - if requestedSize != "" { - options.MaxTranscodeSize = models.StreamingResolutionEnum(requestedSize).GetMaxResolution() - } - - encoder := manager.GetInstance().FFMPEG - - lm := manager.GetInstance().ReadLockManager - streamRequestCtx := manager.NewStreamRequestContext(w, r) - lockCtx := lm.ReadLock(streamRequestCtx, f.Path) - - // hijacking and closing the connection here causes video playback to hang in Chrome - // due to ERR_INCOMPLETE_CHUNKED_ENCODING - // We trust that the request context will be closed, so we don't need to call Cancel on the returned context here. - - stream, err := encoder.GetTranscodeStream(lockCtx, options) - - if err != nil { - logger.Errorf("[stream] error transcoding video file: %v", err) - w.WriteHeader(http.StatusBadRequest) - if _, err := w.Write([]byte(err.Error())); err != nil { - logger.Warnf("[stream] error writing response: %v", err) - } + streamManager := manager.GetInstance().StreamManager + if streamManager == nil { + http.Error(w, "Live transcoding disabled", http.StatusServiceUnavailable) return } - lockCtx.AttachCommand(stream.Cmd) + f := scene.Files.Primary() + if f == nil { + return + } - stream.Serve(w, r) - w.(http.Flusher).Flush() + if err := r.ParseForm(); err != nil { + logger.Warnf("[transcode] error parsing query form: %v", err) + } + + resolution := r.Form.Get("resolution") + + logger.Debugf("[transcode] returning %s manifest for scene %d", logName, scene.ID) + streamManager.ServeManifest(w, r, streamType, f, resolution) +} + +func (rs sceneRoutes) StreamHLSSegment(w http.ResponseWriter, r *http.Request) { + rs.streamSegment(w, r, ffmpeg.StreamTypeHLS) +} + +func (rs sceneRoutes) StreamDASHVideoSegment(w http.ResponseWriter, r *http.Request) { + rs.streamSegment(w, r, ffmpeg.StreamTypeDASHVideo) +} + +func (rs sceneRoutes) StreamDASHAudioSegment(w http.ResponseWriter, r *http.Request) { + rs.streamSegment(w, r, ffmpeg.StreamTypeDASHAudio) +} + +func (rs sceneRoutes) streamSegment(w http.ResponseWriter, r *http.Request, streamType *ffmpeg.StreamType) { + scene := r.Context().Value(sceneKey).(*models.Scene) + + streamManager := manager.GetInstance().StreamManager + if streamManager == nil { + http.Error(w, "Live transcoding disabled", http.StatusServiceUnavailable) + return + } + + f := scene.Files.Primary() + if f == nil { + return + } + + if err := r.ParseForm(); err != nil { + logger.Warnf("[transcode] error parsing query form: %v", err) + } + + sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()) + + segment := chi.URLParam(r, "segment") + resolution := r.Form.Get("resolution") + + options := ffmpeg.StreamOptions{ + StreamType: streamType, + VideoFile: f, + Resolution: resolution, + Hash: sceneHash, + Segment: segment, + } + + streamManager.ServeSegment(w, r, options) } func (rs sceneRoutes) Screenshot(w http.ResponseWriter, r *http.Request) { @@ -240,7 +264,8 @@ func (rs sceneRoutes) Screenshot(w http.ResponseWriter, r *http.Request) { func (rs sceneRoutes) Preview(w http.ResponseWriter, r *http.Request) { scene := r.Context().Value(sceneKey).(*models.Scene) - filepath := manager.GetInstance().Paths.Scene.GetVideoPreviewPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())) + sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()) + filepath := manager.GetInstance().Paths.Scene.GetVideoPreviewPath(sceneHash) serveFileNoCache(w, r, filepath) } @@ -254,7 +279,8 @@ func serveFileNoCache(w http.ResponseWriter, r *http.Request, filepath string) { func (rs sceneRoutes) Webp(w http.ResponseWriter, r *http.Request) { scene := r.Context().Value(sceneKey).(*models.Scene) - filepath := manager.GetInstance().Paths.Scene.GetWebpPreviewPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())) + sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()) + filepath := manager.GetInstance().Paths.Scene.GetWebpPreviewPath(sceneHash) http.ServeFile(w, r, filepath) } @@ -290,7 +316,7 @@ func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.Sce return &title, nil } -func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) { +func (rs sceneRoutes) VttChapter(w http.ResponseWriter, r *http.Request) { scene := r.Context().Value(sceneKey).(*models.Scene) var sceneMarkers []*models.SceneMarker readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { @@ -332,6 +358,32 @@ func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(vtt)) } +func (rs sceneRoutes) VttThumbs(w http.ResponseWriter, r *http.Request) { + scene, ok := r.Context().Value(sceneKey).(*models.Scene) + var sceneHash string + if ok && scene != nil { + sceneHash = scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()) + } else { + sceneHash = chi.URLParam(r, "sceneHash") + } + w.Header().Set("Content-Type", "text/vtt") + filepath := manager.GetInstance().Paths.Scene.GetSpriteVttFilePath(sceneHash) + http.ServeFile(w, r, filepath) +} + +func (rs sceneRoutes) VttSprite(w http.ResponseWriter, r *http.Request) { + scene, ok := r.Context().Value(sceneKey).(*models.Scene) + var sceneHash string + if ok && scene != nil { + sceneHash = scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()) + } else { + sceneHash = chi.URLParam(r, "sceneHash") + } + w.Header().Set("Content-Type", "image/jpeg") + filepath := manager.GetInstance().Paths.Scene.GetSpriteImageFilePath(sceneHash) + http.ServeFile(w, r, filepath) +} + func (rs sceneRoutes) Funscript(w http.ResponseWriter, r *http.Request) { s := r.Context().Value(sceneKey).(*models.Scene) funscript := video.GetFunscriptPath(s.Path) @@ -340,8 +392,9 @@ func (rs sceneRoutes) Funscript(w http.ResponseWriter, r *http.Request) { func (rs sceneRoutes) InteractiveHeatmap(w http.ResponseWriter, r *http.Request) { scene := r.Context().Value(sceneKey).(*models.Scene) + sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()) w.Header().Set("Content-Type", "image/png") - filepath := manager.GetInstance().Paths.Scene.GetInteractiveHeatmapPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())) + filepath := manager.GetInstance().Paths.Scene.GetInteractiveHeatmapPath(sceneHash) http.ServeFile(w, r, filepath) } @@ -405,22 +458,9 @@ func (rs sceneRoutes) CaptionLang(w http.ResponseWriter, r *http.Request) { rs.Caption(w, r, l, ext) } -func (rs sceneRoutes) VttThumbs(w http.ResponseWriter, r *http.Request) { - scene := r.Context().Value(sceneKey).(*models.Scene) - w.Header().Set("Content-Type", "text/vtt") - filepath := manager.GetInstance().Paths.Scene.GetSpriteVttFilePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())) - http.ServeFile(w, r, filepath) -} - -func (rs sceneRoutes) VttSprite(w http.ResponseWriter, r *http.Request) { - scene := r.Context().Value(sceneKey).(*models.Scene) - w.Header().Set("Content-Type", "image/jpeg") - filepath := manager.GetInstance().Paths.Scene.GetSpriteImageFilePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())) - http.ServeFile(w, r, filepath) -} - func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request) { scene := r.Context().Value(sceneKey).(*models.Scene) + sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()) sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId")) var sceneMarker *models.SceneMarker readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { @@ -442,12 +482,13 @@ func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request) return } - filepath := manager.GetInstance().Paths.SceneMarkers.GetVideoPreviewPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds)) + filepath := manager.GetInstance().Paths.SceneMarkers.GetVideoPreviewPath(sceneHash, int(sceneMarker.Seconds)) http.ServeFile(w, r, filepath) } func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request) { scene := r.Context().Value(sceneKey).(*models.Scene) + sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()) sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId")) var sceneMarker *models.SceneMarker readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { @@ -469,7 +510,7 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request) return } - filepath := manager.GetInstance().Paths.SceneMarkers.GetWebpPreviewPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds)) + filepath := manager.GetInstance().Paths.SceneMarkers.GetWebpPreviewPath(sceneHash, int(sceneMarker.Seconds)) // If the image doesn't exist, send the placeholder exists, _ := fsutil.FileExists(filepath) @@ -485,6 +526,7 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request) func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Request) { scene := r.Context().Value(sceneKey).(*models.Scene) + sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()) sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId")) var sceneMarker *models.SceneMarker readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { @@ -506,7 +548,7 @@ func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Reque return } - filepath := manager.GetInstance().Paths.SceneMarkers.GetScreenshotPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds)) + filepath := manager.GetInstance().Paths.SceneMarkers.GetScreenshotPath(sceneHash, int(sceneMarker.Seconds)) // If the image doesn't exist, send the placeholder exists, _ := fsutil.FileExists(filepath) @@ -524,28 +566,16 @@ func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Reque func (rs sceneRoutes) SceneCtx(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - sceneIdentifierQueryParam := chi.URLParam(r, "sceneId") - sceneID, _ := strconv.Atoi(sceneIdentifierQueryParam) + sceneID, err := strconv.Atoi(chi.URLParam(r, "sceneId")) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } var scene *models.Scene _ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { qb := rs.sceneFinder - if sceneID == 0 { - var scenes []*models.Scene - // determine checksum/os by the length of the query param - if len(sceneIdentifierQueryParam) == 32 { - scenes, _ = qb.FindByChecksum(ctx, sceneIdentifierQueryParam) - - } else { - scenes, _ = qb.FindByOSHash(ctx, sceneIdentifierQueryParam) - } - - if len(scenes) > 0 { - scene = scenes[0] - } - } else { - scene, _ = qb.Find(ctx, sceneID) - } + scene, _ = qb.Find(ctx, sceneID) if scene != nil { if err := scene.LoadPrimaryFile(ctx, rs.fileFinder); err != nil { diff --git a/internal/api/routes_studio.go b/internal/api/routes_studio.go index 2ddeb51a3..a77763e8d 100644 --- a/internal/api/routes_studio.go +++ b/internal/api/routes_studio.go @@ -42,8 +42,9 @@ func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) { var image []byte if defaultParam != "true" { readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { - image, _ = rs.studioFinder.GetImage(ctx, studio.ID) - return nil + var err error + image, err = rs.studioFinder.GetImage(ctx, studio.ID) + return err }) if errors.Is(readTxnErr, context.Canceled) { return diff --git a/internal/api/routes_tag.go b/internal/api/routes_tag.go index 1f72928c2..e3ee439e9 100644 --- a/internal/api/routes_tag.go +++ b/internal/api/routes_tag.go @@ -42,8 +42,9 @@ func (rs tagRoutes) Image(w http.ResponseWriter, r *http.Request) { var image []byte if defaultParam != "true" { readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { - image, _ = rs.tagFinder.GetImage(ctx, tag.ID) - return nil + var err error + image, err = rs.tagFinder.GetImage(ctx, tag.ID) + return err }) if errors.Is(readTxnErr, context.Canceled) { return diff --git a/internal/api/server.go b/internal/api/server.go index 124c89739..c8e8a7b28 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -1,14 +1,17 @@ package api import ( + "bytes" "context" "crypto/tls" "errors" "fmt" + "io" "io/fs" "net/http" "os" "path" + "regexp" "runtime/debug" "strconv" "strings" @@ -31,6 +34,7 @@ import ( "github.com/stashapp/stash/internal/manager/config" "github.com/stashapp/stash/pkg/fsutil" "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/plugin" "github.com/stashapp/stash/ui" ) @@ -131,7 +135,7 @@ func Start() error { // session handlers r.Post(loginEndPoint, handleLogin(loginUIBox)) - r.Get("/logout", handleLogout(loginUIBox)) + r.Get(logoutEndPoint, handleLogout(loginUIBox)) r.Get(loginEndPoint, getLoginHandler(loginUIBox)) @@ -166,36 +170,8 @@ func Start() error { }.Routes()) r.Mount("/downloads", downloadsRoutes{}.Routes()) - r.HandleFunc("/css", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/css") - if !c.GetCSSEnabled() { - return - } - - // search for custom.css in current directory, then $HOME/.stash - fn := c.GetCSSPath() - exists, _ := fsutil.FileExists(fn) - if !exists { - return - } - - http.ServeFile(w, r, fn) - }) - r.HandleFunc("/javascript", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/javascript") - if !c.GetJavascriptEnabled() { - return - } - - // search for custom.js in current directory, then $HOME/.stash - fn := c.GetJavascriptPath() - exists, _ := fsutil.FileExists(fn) - if !exists { - return - } - - http.ServeFile(w, r, fn) - }) + r.HandleFunc("/css", cssHandler(c, pluginCache)) + r.HandleFunc("/javascript", javascriptHandler(c, pluginCache)) r.HandleFunc("/customlocales", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if c.GetCustomLocalesEnabled() { @@ -329,23 +305,139 @@ func Start() error { return nil } +func copyFile(w io.Writer, path string) (time.Time, error) { + f, err := os.Open(path) + if err != nil { + return time.Time{}, err + } + defer f.Close() + + info, err := f.Stat() + if err != nil { + return time.Time{}, err + } + + _, err = io.Copy(w, f) + + return info.ModTime(), err +} + +func serveFiles(w http.ResponseWriter, r *http.Request, name string, paths []string) { + buffer := bytes.Buffer{} + + latestModTime := time.Time{} + + for _, path := range paths { + modTime, err := copyFile(&buffer, path) + if err != nil { + logger.Errorf("error serving file %s: %v", path, err) + } else { + if modTime.After(latestModTime) { + latestModTime = modTime + } + buffer.Write([]byte("\n")) + } + } + + // Always revalidate with server + w.Header().Set("Cache-Control", "no-cache") + + bufferReader := bytes.NewReader(buffer.Bytes()) + http.ServeContent(w, r, name, latestModTime, bufferReader) +} + +func cssHandler(c *config.Instance, pluginCache *plugin.Cache) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // concatenate with plugin css files + w.Header().Set("Content-Type", "text/css") + + // add plugin css files first + var paths []string + + for _, p := range pluginCache.ListPlugins() { + paths = append(paths, p.UI.CSS...) + } + + if c.GetCSSEnabled() { + // search for custom.css in current directory, then $HOME/.stash + fn := c.GetCSSPath() + exists, _ := fsutil.FileExists(fn) + if exists { + paths = append(paths, fn) + } + } + + serveFiles(w, r, "custom.css", paths) + } +} + +func javascriptHandler(c *config.Instance, pluginCache *plugin.Cache) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/javascript") + + // add plugin javascript files first + var paths []string + + for _, p := range pluginCache.ListPlugins() { + paths = append(paths, p.UI.Javascript...) + } + + if c.GetJavascriptEnabled() { + // search for custom.js in current directory, then $HOME/.stash + fn := c.GetJavascriptPath() + exists, _ := fsutil.FileExists(fn) + if exists { + paths = append(paths, fn) + } + } + + serveFiles(w, r, "custom.js", paths) + } +} + func printVersion() { - versionString := githash + var versionString string + switch { + case version != "": + if githash != "" && !IsDevelop() { + versionString = version + " (" + githash + ")" + } else { + versionString = version + } + case githash != "": + versionString = githash + default: + versionString = "unknown" + } if config.IsOfficialBuild() { versionString += " - Official Build" } else { versionString += " - Unofficial Build" } - if version != "" { - versionString = version + " (" + versionString + ")" + if buildstamp != "" { + versionString += " - " + buildstamp } - fmt.Printf("stash version: %s - %s\n", versionString, buildstamp) + logger.Infof("stash version: %s\n", versionString) } func GetVersion() (string, string, string) { return version, githash, buildstamp } +func IsDevelop() bool { + if githash == "" { + return false + } + + // if the version is suffixed with -x-xxxx, then we are running a development build + develop := false + re := regexp.MustCompile(`-\d+-g\w+$`) + if re.MatchString(version) { + develop = true + } + return develop +} + func makeTLSConfig(c *config.Instance) (*tls.Config, error) { c.InitTLS() certFile, keyFile := c.GetTLSFiles() @@ -366,12 +458,12 @@ func makeTLSConfig(c *config.Instance) (*tls.Config, error) { cert, err := os.ReadFile(certFile) if err != nil { - return nil, fmt.Errorf("error reading SSL certificate file %s: %s", certFile, err.Error()) + return nil, fmt.Errorf("error reading SSL certificate file %s: %v", certFile, err) } key, err := os.ReadFile(keyFile) if err != nil { - return nil, fmt.Errorf("error reading SSL key file %s: %s", keyFile, err.Error()) + return nil, fmt.Errorf("error reading SSL key file %s: %v", keyFile, err) } certs := make([]tls.Certificate, 1) @@ -411,7 +503,7 @@ func SecurityHeadersMiddleware(next http.Handler) http.Handler { } connectableOrigins += "; " - cspDirectives := "default-src data: 'self' 'unsafe-inline';" + connectableOrigins + "img-src data: *; script-src 'self' https://cdn.jsdelivr.net 'unsafe-inline' 'unsafe-eval'; style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; style-src-elem 'self' https://cdn.jsdelivr.net 'unsafe-inline'; media-src 'self' blob:; child-src 'none'; object-src 'none'; form-action 'self'" + cspDirectives := "default-src data: 'self' 'unsafe-inline';" + connectableOrigins + "img-src data: *; script-src 'self' https://cdn.jsdelivr.net 'unsafe-inline' 'unsafe-eval'; style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; style-src-elem 'self' https://cdn.jsdelivr.net 'unsafe-inline'; media-src 'self' blob:; child-src 'none'; worker-src blob:; object-src 'none'; form-action 'self'" w.Header().Set("Referrer-Policy", "same-origin") w.Header().Set("X-Content-Type-Options", "nosniff") diff --git a/internal/api/urlbuilders/scene.go b/internal/api/urlbuilders/scene.go index 014daec95..ae4fce1c0 100644 --- a/internal/api/urlbuilders/scene.go +++ b/internal/api/urlbuilders/scene.go @@ -10,7 +10,6 @@ import ( type SceneURLBuilder struct { BaseURL string SceneID string - APIKey string } func NewSceneURLBuilder(baseURL string, sceneID int) SceneURLBuilder { @@ -20,16 +19,16 @@ func NewSceneURLBuilder(baseURL string, sceneID int) SceneURLBuilder { } } -func (b SceneURLBuilder) GetStreamURL() *url.URL { +func (b SceneURLBuilder) GetStreamURL(apiKey string) *url.URL { u, err := url.Parse(fmt.Sprintf("%s/scene/%s/stream", b.BaseURL, b.SceneID)) if err != nil { // shouldn't happen panic(err) } - if b.APIKey != "" { + if apiKey != "" { v := u.Query() - v.Set("apikey", b.APIKey) + v.Set("apikey", apiKey) u.RawQuery = v.Encode() } return u @@ -43,12 +42,12 @@ func (b SceneURLBuilder) GetStreamPreviewImageURL() string { return b.BaseURL + "/scene/" + b.SceneID + "/webp" } -func (b SceneURLBuilder) GetSpriteVTTURL() string { - return b.BaseURL + "/scene/" + b.SceneID + "_thumbs.vtt" +func (b SceneURLBuilder) GetSpriteVTTURL(checksum string) string { + return b.BaseURL + "/scene/" + checksum + "_thumbs.vtt" } -func (b SceneURLBuilder) GetSpriteURL() string { - return b.BaseURL + "/scene/" + b.SceneID + "_sprite.jpg" +func (b SceneURLBuilder) GetSpriteURL(checksum string) string { + return b.BaseURL + "/scene/" + checksum + "_sprite.jpg" } func (b SceneURLBuilder) GetScreenshotURL(updateTime time.Time) string { diff --git a/internal/autotag/gallery_test.go b/internal/autotag/gallery_test.go index cae45e6c9..556c09ce2 100644 --- a/internal/autotag/gallery_test.go +++ b/internal/autotag/gallery_test.go @@ -21,15 +21,17 @@ func TestGalleryPerformers(t *testing.T) { const performerName = "performer name" const performerID = 2 performer := models.Performer{ - ID: performerID, - Name: performerName, + ID: performerID, + Name: performerName, + Aliases: models.NewRelatedStrings([]string{}), } const reversedPerformerName = "name performer" const reversedPerformerID = 3 reversedPerformer := models.Performer{ - ID: reversedPerformerID, - Name: reversedPerformerName, + ID: reversedPerformerID, + Name: reversedPerformerName, + Aliases: models.NewRelatedStrings([]string{}), } testTables := generateTestTable(performerName, galleryExt) diff --git a/internal/autotag/image_test.go b/internal/autotag/image_test.go index b98842398..62133aea8 100644 --- a/internal/autotag/image_test.go +++ b/internal/autotag/image_test.go @@ -18,15 +18,17 @@ func TestImagePerformers(t *testing.T) { const performerName = "performer name" const performerID = 2 performer := models.Performer{ - ID: performerID, - Name: performerName, + ID: performerID, + Name: performerName, + Aliases: models.NewRelatedStrings([]string{}), } const reversedPerformerName = "name performer" const reversedPerformerID = 3 reversedPerformer := models.Performer{ - ID: reversedPerformerID, - Name: reversedPerformerName, + ID: reversedPerformerID, + Name: reversedPerformerName, + Aliases: models.NewRelatedStrings([]string{}), } testTables := generateTestTable(performerName, imageExt) diff --git a/internal/autotag/integration_test.go b/internal/autotag/integration_test.go index e49c3637a..aab4b2f9b 100644 --- a/internal/autotag/integration_test.go +++ b/internal/autotag/integration_test.go @@ -86,8 +86,7 @@ func TestMain(m *testing.M) { func createPerformer(ctx context.Context, pqb models.PerformerWriter) error { // create the performer performer := models.Performer{ - Checksum: testName, - Name: testName, + Name: testName, } err := pqb.Create(ctx, &performer) @@ -548,6 +547,9 @@ func TestParsePerformerScenes(t *testing.T) { for _, p := range performers { if err := withDB(func(ctx context.Context) error { + if err := p.LoadAliases(ctx, r.Performer); err != nil { + return err + } return tagger.PerformerScenes(ctx, p, nil, r.Scene) }); err != nil { t.Errorf("Error auto-tagging performers: %s", err) @@ -715,6 +717,9 @@ func TestParsePerformerImages(t *testing.T) { for _, p := range performers { if err := withDB(func(ctx context.Context) error { + if err := p.LoadAliases(ctx, r.Performer); err != nil { + return err + } return tagger.PerformerImages(ctx, p, nil, r.Image) }); err != nil { t.Errorf("Error auto-tagging performers: %s", err) @@ -884,6 +889,9 @@ func TestParsePerformerGalleries(t *testing.T) { for _, p := range performers { if err := withDB(func(ctx context.Context) error { + if err := p.LoadAliases(ctx, r.Performer); err != nil { + return err + } return tagger.PerformerGalleries(ctx, p, nil, r.Gallery) }); err != nil { t.Errorf("Error auto-tagging performers: %s", err) diff --git a/internal/autotag/performer.go b/internal/autotag/performer.go index c18bf0b0a..32364dc50 100644 --- a/internal/autotag/performer.go +++ b/internal/autotag/performer.go @@ -30,83 +30,111 @@ type GalleryQueryPerformerUpdater interface { gallery.PartialUpdater } -func getPerformerTagger(p *models.Performer, cache *match.Cache) tagger { - return tagger{ +func getPerformerTaggers(p *models.Performer, cache *match.Cache) []tagger { + ret := []tagger{{ ID: p.ID, Type: "performer", Name: p.Name, cache: cache, - } + }} + + // TODO - disabled until we can have finer control over alias matching + // for _, a := range p.Aliases.List() { + // ret = append(ret, tagger{ + // ID: p.ID, + // Type: "performer", + // Name: a, + // cache: cache, + // }) + // } + + return ret } // PerformerScenes searches for scenes whose path matches the provided performer name and tags the scene with the performer. +// Performer aliases must be loaded. func (tagger *Tagger) PerformerScenes(ctx context.Context, p *models.Performer, paths []string, rw SceneQueryPerformerUpdater) error { - t := getPerformerTagger(p, tagger.Cache) + t := getPerformerTaggers(p, tagger.Cache) - return t.tagScenes(ctx, paths, rw, func(o *models.Scene) (bool, error) { - if err := o.LoadPerformerIDs(ctx, rw); err != nil { - return false, err - } - existing := o.PerformerIDs.List() + for _, tt := range t { + if err := tt.tagScenes(ctx, paths, rw, func(o *models.Scene) (bool, error) { + if err := o.LoadPerformerIDs(ctx, rw); err != nil { + return false, err + } + existing := o.PerformerIDs.List() - if intslice.IntInclude(existing, p.ID) { - return false, nil - } + if intslice.IntInclude(existing, p.ID) { + return false, nil + } - if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error { - return scene.AddPerformer(ctx, rw, o, p.ID) + if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error { + return scene.AddPerformer(ctx, rw, o, p.ID) + }); err != nil { + return false, err + } + + return true, nil }); err != nil { - return false, err + return err } - - return true, nil - }) + } + return nil } // PerformerImages searches for images whose path matches the provided performer name and tags the image with the performer. func (tagger *Tagger) PerformerImages(ctx context.Context, p *models.Performer, paths []string, rw ImageQueryPerformerUpdater) error { - t := getPerformerTagger(p, tagger.Cache) + t := getPerformerTaggers(p, tagger.Cache) - return t.tagImages(ctx, paths, rw, func(o *models.Image) (bool, error) { - if err := o.LoadPerformerIDs(ctx, rw); err != nil { - return false, err - } - existing := o.PerformerIDs.List() + for _, tt := range t { + if err := tt.tagImages(ctx, paths, rw, func(o *models.Image) (bool, error) { + if err := o.LoadPerformerIDs(ctx, rw); err != nil { + return false, err + } + existing := o.PerformerIDs.List() - if intslice.IntInclude(existing, p.ID) { - return false, nil - } + if intslice.IntInclude(existing, p.ID) { + return false, nil + } - if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error { - return image.AddPerformer(ctx, rw, o, p.ID) + if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error { + return image.AddPerformer(ctx, rw, o, p.ID) + }); err != nil { + return false, err + } + + return true, nil }); err != nil { - return false, err + return err } - - return true, nil - }) + } + return nil } // PerformerGalleries searches for galleries whose path matches the provided performer name and tags the gallery with the performer. func (tagger *Tagger) PerformerGalleries(ctx context.Context, p *models.Performer, paths []string, rw GalleryQueryPerformerUpdater) error { - t := getPerformerTagger(p, tagger.Cache) + t := getPerformerTaggers(p, tagger.Cache) - return t.tagGalleries(ctx, paths, rw, func(o *models.Gallery) (bool, error) { - if err := o.LoadPerformerIDs(ctx, rw); err != nil { - return false, err - } - existing := o.PerformerIDs.List() + for _, tt := range t { + if err := tt.tagGalleries(ctx, paths, rw, func(o *models.Gallery) (bool, error) { + if err := o.LoadPerformerIDs(ctx, rw); err != nil { + return false, err + } + existing := o.PerformerIDs.List() - if intslice.IntInclude(existing, p.ID) { - return false, nil - } + if intslice.IntInclude(existing, p.ID) { + return false, nil + } - if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error { - return gallery.AddPerformer(ctx, rw, o, p.ID) + if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error { + return gallery.AddPerformer(ctx, rw, o, p.ID) + }); err != nil { + return false, err + } + + return true, nil }); err != nil { - return false, err + return err } - - return true, nil - }) + } + return nil } diff --git a/internal/autotag/performer_test.go b/internal/autotag/performer_test.go index c2590b19a..5f7b12c22 100644 --- a/internal/autotag/performer_test.go +++ b/internal/autotag/performer_test.go @@ -60,8 +60,9 @@ func testPerformerScenes(t *testing.T, performerName, expectedRegex string) { } performer := models.Performer{ - ID: performerID, - Name: performerName, + ID: performerID, + Name: performerName, + Aliases: models.NewRelatedStrings([]string{}), } organized := false @@ -148,8 +149,9 @@ func testPerformerImages(t *testing.T, performerName, expectedRegex string) { } performer := models.Performer{ - ID: performerID, - Name: performerName, + ID: performerID, + Name: performerName, + Aliases: models.NewRelatedStrings([]string{}), } organized := false @@ -237,8 +239,9 @@ func testPerformerGalleries(t *testing.T, performerName, expectedRegex string) { } performer := models.Performer{ - ID: performerID, - Name: performerName, + ID: performerID, + Name: performerName, + Aliases: models.NewRelatedStrings([]string{}), } organized := false diff --git a/internal/autotag/scene_test.go b/internal/autotag/scene_test.go index cb0ff32db..71a28336c 100644 --- a/internal/autotag/scene_test.go +++ b/internal/autotag/scene_test.go @@ -151,15 +151,17 @@ func TestScenePerformers(t *testing.T) { const performerName = "performer name" const performerID = 2 performer := models.Performer{ - ID: performerID, - Name: performerName, + ID: performerID, + Name: performerName, + Aliases: models.NewRelatedStrings([]string{}), } const reversedPerformerName = "name performer" const reversedPerformerID = 3 reversedPerformer := models.Performer{ - ID: reversedPerformerID, - Name: reversedPerformerName, + ID: reversedPerformerID, + Name: reversedPerformerName, + Aliases: models.NewRelatedStrings([]string{}), } testTables := generateTestTable(performerName, sceneExt) diff --git a/internal/desktop/systray_nonlinux.go b/internal/desktop/systray_nonlinux.go index 78ccfbebc..a565a930c 100644 --- a/internal/desktop/systray_nonlinux.go +++ b/internal/desktop/systray_nonlinux.go @@ -9,6 +9,8 @@ import ( "github.com/kermieisinthehouse/systray" "github.com/stashapp/stash/internal/manager/config" "github.com/stashapp/stash/pkg/logger" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) // MUST be run on the main goroutine or will have no effect on macOS @@ -55,7 +57,8 @@ func systrayInitialize(shutdownHandler ShutdownHandler, faviconProvider FaviconP if !c.IsNewSystem() { menuItems = c.GetMenuItems() for _, item := range menuItems { - titleCaseItem := strings.Title(strings.ToLower(item)) + c := cases.Title(language.Und) + titleCaseItem := c.String(strings.ToLower(item)) curr := systray.AddMenuItem(titleCaseItem, "Open to "+titleCaseItem) go func(item string) { for { diff --git a/internal/identify/identify.go b/internal/identify/identify.go index c828f4164..04eccb7b0 100644 --- a/internal/identify/identify.go +++ b/internal/identify/identify.go @@ -35,7 +35,6 @@ type SceneIdentifier struct { DefaultOptions *MetadataOptions Sources []ScraperSource - ScreenshotSetter scene.ScreenshotSetter SceneUpdatePostHookExecutor SceneUpdatePostHookExecutor } @@ -216,7 +215,7 @@ func (t *SceneIdentifier) modifyScene(ctx context.Context, txnManager txn.Manage return nil } - if _, err := updater.Update(ctx, t.SceneReaderUpdater, t.ScreenshotSetter); err != nil { + if _, err := updater.Update(ctx, t.SceneReaderUpdater); err != nil { return fmt.Errorf("error updating scene: %w", err) } diff --git a/internal/identify/performer.go b/internal/identify/performer.go index d417d8bac..a78a0ce6c 100644 --- a/internal/identify/performer.go +++ b/internal/identify/performer.go @@ -6,13 +6,12 @@ import ( "strconv" "time" - "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/models" + "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) type PerformerCreator interface { Create(ctx context.Context, newPerformer *models.Performer) error - UpdateStashIDs(ctx context.Context, performerID int, stashIDs []models.StashID) error } func getPerformerID(ctx context.Context, endpoint string, w PerformerCreator, p *models.ScrapedPerformer, createMissing bool) (*int, error) { @@ -33,20 +32,18 @@ func getPerformerID(ctx context.Context, endpoint string, w PerformerCreator, p func createMissingPerformer(ctx context.Context, endpoint string, w PerformerCreator, p *models.ScrapedPerformer) (*int, error) { performerInput := scrapedToPerformerInput(p) - err := w.Create(ctx, &performerInput) - if err != nil { - return nil, fmt.Errorf("error creating performer: %w", err) - } - if endpoint != "" && p.RemoteSiteID != nil { - if err := w.UpdateStashIDs(ctx, performerInput.ID, []models.StashID{ + performerInput.StashIDs = models.NewRelatedStashIDs([]models.StashID{ { Endpoint: endpoint, StashID: *p.RemoteSiteID, }, - }); err != nil { - return nil, fmt.Errorf("error setting performer stash id: %w", err) - } + }) + } + + err := w.Create(ctx, &performerInput) + if err != nil { + return nil, fmt.Errorf("error creating performer: %w", err) } return &performerInput.ID, nil @@ -56,7 +53,6 @@ func scrapedToPerformerInput(performer *models.ScrapedPerformer) models.Performe currentTime := time.Now() ret := models.Performer{ Name: *performer.Name, - Checksum: md5.FromString(*performer.Name), CreatedAt: currentTime, UpdatedAt: currentTime, } @@ -111,7 +107,7 @@ func scrapedToPerformerInput(performer *models.ScrapedPerformer) models.Performe ret.Piercings = *performer.Piercings } if performer.Aliases != nil { - ret.Aliases = *performer.Aliases + ret.Aliases = models.NewRelatedStrings(stringslice.FromString(*performer.Aliases, ",")) } if performer.Twitter != nil { ret.Twitter = *performer.Twitter diff --git a/internal/identify/performer_test.go b/internal/identify/performer_test.go index 764b4ec79..0a78ea173 100644 --- a/internal/identify/performer_test.go +++ b/internal/identify/performer_test.go @@ -127,7 +127,6 @@ func Test_getPerformerID(t *testing.T) { func Test_createMissingPerformer(t *testing.T) { emptyEndpoint := "" validEndpoint := "validEndpoint" - invalidEndpoint := "invalidEndpoint" remoteSiteID := "remoteSiteID" validName := "validName" invalidName := "invalidName" @@ -145,19 +144,6 @@ func Test_createMissingPerformer(t *testing.T) { return p.Name == invalidName })).Return(errors.New("error creating performer")) - mockPerformerReaderWriter.On("UpdateStashIDs", testCtx, performerID, []models.StashID{ - { - Endpoint: invalidEndpoint, - StashID: remoteSiteID, - }, - }).Return(errors.New("error updating stash ids")) - mockPerformerReaderWriter.On("UpdateStashIDs", testCtx, performerID, []models.StashID{ - { - Endpoint: validEndpoint, - StashID: remoteSiteID, - }, - }).Return(nil) - type args struct { endpoint string p *models.ScrapedPerformer @@ -202,18 +188,6 @@ func Test_createMissingPerformer(t *testing.T) { &performerID, false, }, - { - "invalid stash id", - args{ - invalidEndpoint, - &models.ScrapedPerformer{ - Name: &validName, - RemoteSiteID: &remoteSiteID, - }, - }, - nil, - true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -231,7 +205,6 @@ func Test_createMissingPerformer(t *testing.T) { func Test_scrapedToPerformerInput(t *testing.T) { name := "name" - md5 := "b068931cc450442b63f5b3d276ea4297" var stringValues []string for i := 0; i < 17; i++ { @@ -284,7 +257,6 @@ func Test_scrapedToPerformerInput(t *testing.T) { }, models.Performer{ Name: name, - Checksum: md5, Birthdate: dateToDatePtr(models.NewDate(*nextVal())), DeathDate: dateToDatePtr(models.NewDate(*nextVal())), Gender: models.GenderEnum(*nextVal()), @@ -299,7 +271,7 @@ func Test_scrapedToPerformerInput(t *testing.T) { CareerLength: *nextVal(), Tattoos: *nextVal(), Piercings: *nextVal(), - Aliases: *nextVal(), + Aliases: models.NewRelatedStrings([]string{*nextVal()}), Twitter: *nextVal(), Instagram: *nextVal(), }, @@ -310,8 +282,7 @@ func Test_scrapedToPerformerInput(t *testing.T) { Name: &name, }, models.Performer{ - Name: name, - Checksum: md5, + Name: name, }, }, } diff --git a/internal/identify/scene.go b/internal/identify/scene.go index d74b47d12..a952cb73b 100644 --- a/internal/identify/scene.go +++ b/internal/identify/scene.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/scene" "github.com/stashapp/stash/pkg/sliceutil" @@ -234,7 +235,7 @@ func (g sceneRelationships) cover(ctx context.Context) ([]byte, error) { // always overwrite if present existingCover, err := g.sceneReader.GetCover(ctx, g.scene.ID) if err != nil { - return nil, fmt.Errorf("error getting scene cover: %w", err) + logger.Errorf("Error getting scene cover: %v", err) } data, err := utils.ProcessImageInput(ctx, *scraped) diff --git a/internal/identify/scene_test.go b/internal/identify/scene_test.go index 511023680..5e8091e6f 100644 --- a/internal/identify/scene_test.go +++ b/internal/identify/scene_test.go @@ -742,8 +742,8 @@ func Test_sceneRelationships_cover(t *testing.T) { "error getting scene cover", errSceneID, &newDataEncoded, - nil, - true, + newData, + false, }, { "invalid data", diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index 8c47b1989..4b2ba7921 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -31,12 +31,15 @@ const ( BackupDirectoryPath = "backup_directory_path" Generated = "generated" Metadata = "metadata" + BlobsPath = "blobs_path" Downloads = "downloads" ApiKey = "api_key" Username = "username" Password = "password" MaxSessionAge = "max_session_age" + BlobsStorage = "blobs_storage" + DefaultMaxSessionAge = 60 * 60 * 1 // 1 hours Database = "database" @@ -60,10 +63,20 @@ const ( MaxTranscodeSize = "max_transcode_size" MaxStreamingTranscodeSize = "max_streaming_transcode_size" + // ffmpeg extra args options + TranscodeInputArgs = "ffmpeg.transcode.input_args" + TranscodeOutputArgs = "ffmpeg.transcode.output_args" + LiveTranscodeInputArgs = "ffmpeg.live_transcode.input_args" + LiveTranscodeOutputArgs = "ffmpeg.live_transcode.output_args" + ParallelTasks = "parallel_tasks" parallelTasksDefault = 1 - PreviewPreset = "preview_preset" + PreviewPreset = "preview_preset" + TranscodeHardwareAcceleration = "ffmpeg.hardware_acceleration" + + SequentialScanning = "sequential_scanning" + SequentialScanningDefault = false PreviewAudio = "preview_audio" previewAudioDefault = true @@ -91,6 +104,13 @@ const ( ExternalHost = "external_host" + // http proxy url if required + Proxy = "proxy" + + // urls or IPs that should not use the proxy + NoProxy = "no_proxy" + noProxyDefault = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12" + // key used to sign JWT tokens JWTSignKey = "jwt_secret_key" @@ -123,6 +143,10 @@ const ( // rather than use the embedded UI. CustomUILocation = "custom_ui_location" + // Gallery Cover Regex + GalleryCoverRegex = "gallery_cover_regex" + galleryCoverRegexDefault = `(poster|cover|folder|board)\.[^\.]+$` + // Interface options MenuItems = "menu_items" @@ -168,6 +192,9 @@ const ( HandyKey = "handy_key" FunscriptOffset = "funscript_offset" + DrawFunscriptHeatmapRange = "draw_funscript_heatmap_range" + drawFunscriptHeatmapRangeDefault = true + ThemeColor = "theme_color" DefaultThemeColor = "#202b33" @@ -477,27 +504,14 @@ func (i *Instance) getStringMapString(key string) map[string]string { return ret } -type StashConfig struct { - Path string `json:"path"` - ExcludeVideo bool `json:"excludeVideo"` - ExcludeImage bool `json:"excludeImage"` -} - -// Stash configuration details -type StashConfigInput struct { - Path string `json:"path"` - ExcludeVideo bool `json:"excludeVideo"` - ExcludeImage bool `json:"excludeImage"` -} - // GetStathPaths 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 *Instance) GetStashPaths() []*StashConfig { +func (i *Instance) GetStashPaths() StashConfigs { i.RLock() defer i.RUnlock() - var ret []*StashConfig + var ret StashConfigs v := i.main if !v.IsSet(Stash) { @@ -527,6 +541,22 @@ func (i *Instance) GetGeneratedPath() string { return i.getString(Generated) } +func (i *Instance) GetBlobsPath() string { + return i.getString(BlobsPath) +} + +func (i *Instance) GetBlobsStorage() BlobsStorageType { + ret := BlobsStorageType(i.getString(BlobsStorage)) + + if !ret.IsValid() { + // default to database storage + // for legacy systems this is probably the safer option + ret = BlobStorageTypeDatabase + } + + return ret +} + func (i *Instance) GetMetadataPath() string { return i.getString(Metadata) } @@ -629,6 +659,22 @@ func (i *Instance) GetVideoFileNamingAlgorithm() models.HashAlgorithm { return models.HashAlgorithm(ret) } +func (i *Instance) GetSequentialScanning() bool { + return i.getBool(SequentialScanning) +} + +func (i *Instance) GetGalleryCoverRegex() string { + var regexString = i.getString(GalleryCoverRegex) + + _, err := regexp.Compile(regexString) + if err != nil { + logger.Warnf("Gallery cover regex '%v' invalid, reverting to default.", regexString) + return galleryCoverRegexDefault + } + + return regexString +} + func (i *Instance) GetScrapersPath() string { return i.getString(ScrapersPath) } @@ -764,6 +810,10 @@ func (i *Instance) GetPreviewPreset() models.PreviewPreset { return models.PreviewPreset(ret) } +func (i *Instance) GetTranscodeHardwareAcceleration() bool { + return i.getBool(TranscodeHardwareAcceleration) +} + func (i *Instance) GetMaxTranscodeSize() models.StreamingResolutionEnum { ret := i.getString(MaxTranscodeSize) @@ -786,6 +836,26 @@ func (i *Instance) GetMaxStreamingTranscodeSize() models.StreamingResolutionEnum return models.StreamingResolutionEnum(ret) } +func (i *Instance) GetTranscodeInputArgs() []string { + return i.getStringSlice(TranscodeInputArgs) +} + +func (i *Instance) GetTranscodeOutputArgs() []string { + return i.getStringSlice(TranscodeOutputArgs) +} + +func (i *Instance) GetLiveTranscodeInputArgs() []string { + return i.getStringSlice(LiveTranscodeInputArgs) +} + +func (i *Instance) GetLiveTranscodeOutputArgs() []string { + return i.getStringSlice(LiveTranscodeOutputArgs) +} + +func (i *Instance) GetDrawFunscriptHeatmapRange() bool { + return i.getBoolDefault(DrawFunscriptHeatmapRange, drawFunscriptHeatmapRangeDefault) +} + // IsWriteImageThumbnails returns true if image thumbnails should be written // to disk after generating on the fly. func (i *Instance) IsWriteImageThumbnails() bool { @@ -1265,7 +1335,7 @@ func (i *Instance) GetDefaultGenerateSettings() *models.GenerateMetadataOptions } // GetDangerousAllowPublicWithoutAuth determines if the security feature is enabled. -// See https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet +// See https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet func (i *Instance) GetDangerousAllowPublicWithoutAuth() bool { return i.getBool(dangerousAllowPublicWithoutAuth) } @@ -1343,6 +1413,27 @@ func (i *Instance) GetMaxUploadSize() int64 { return ret << 20 } +// GetProxy returns the url of a http proxy to be used for all outgoing http calls. +func (i *Instance) GetProxy() string { + // Validate format + reg := regexp.MustCompile(`^((?:socks5h?|https?):\/\/)(([\P{Cc}]+):([\P{Cc}]+)@)?(([a-zA-Z0-9][a-zA-Z0-9.-]*)(:[0-9]{1,5})?)`) + proxy := i.getString(Proxy) + if proxy != "" && reg.MatchString(proxy) { + logger.Debug("Proxy is valid, using it") + return proxy + } else if proxy != "" { + logger.Error("Proxy is invalid, please review your configuration") + return "" + } + return "" +} + +// GetProxy returns the url of a http proxy to be used for all outgoing http calls. +func (i *Instance) GetNoProxy() string { + // NoProxy does not require validation, it is validated by the native Go library sufficiently + return i.getString(NoProxy) +} + // ActivatePublicAccessTripwire sets the security_tripwire_accessed_from_public_internet // config field to the provided IP address to indicate that stash has been accessed // from this public IP without authentication. @@ -1373,6 +1464,12 @@ func (i *Instance) Validate() error { } } + if i.GetBlobsStorage() == BlobStorageTypeFilesystem && i.viper(BlobsPath).GetString(BlobsPath) == "" { + return MissingConfigError{ + missingFields: []string{BlobsPath}, + } + } + return nil } @@ -1391,6 +1488,7 @@ func (i *Instance) setDefaultValues(write bool) error { i.main.SetDefault(Port, portDefault) i.main.SetDefault(ParallelTasks, parallelTasksDefault) + i.main.SetDefault(SequentialScanning, SequentialScanningDefault) i.main.SetDefault(PreviewSegmentDuration, previewSegmentDurationDefault) i.main.SetDefault(PreviewSegments, previewSegmentsDefault) i.main.SetDefault(PreviewExcludeStart, previewExcludeStartDefault) @@ -1418,6 +1516,12 @@ func (i *Instance) setDefaultValues(write bool) error { i.main.SetDefault(ScrapersPath, defaultScrapersPath) i.main.SetDefault(PluginsPath, defaultPluginsPath) + // Set default gallery cover regex + i.main.SetDefault(GalleryCoverRegex, galleryCoverRegexDefault) + + // Set NoProxy default + i.main.SetDefault(NoProxy, noProxyDefault) + if write { return i.main.WriteConfig() } diff --git a/internal/manager/config/enums.go b/internal/manager/config/enums.go new file mode 100644 index 000000000..c1e045344 --- /dev/null +++ b/internal/manager/config/enums.go @@ -0,0 +1,50 @@ +package config + +import ( + "fmt" + "io" + "strconv" +) + +type BlobsStorageType string + +const ( + // Database + BlobStorageTypeDatabase BlobsStorageType = "DATABASE" + // Filesystem + BlobStorageTypeFilesystem BlobsStorageType = "FILESYSTEM" +) + +var AllBlobStorageType = []BlobsStorageType{ + BlobStorageTypeDatabase, + BlobStorageTypeFilesystem, +} + +func (e BlobsStorageType) IsValid() bool { + switch e { + case BlobStorageTypeDatabase, BlobStorageTypeFilesystem: + return true + } + return false +} + +func (e BlobsStorageType) String() string { + return string(e) +} + +func (e *BlobsStorageType) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = BlobsStorageType(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid BlobStorageType", str) + } + return nil +} + +func (e BlobsStorageType) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} diff --git a/internal/manager/config/stash_config.go b/internal/manager/config/stash_config.go new file mode 100644 index 000000000..4a2cc7d60 --- /dev/null +++ b/internal/manager/config/stash_config.go @@ -0,0 +1,40 @@ +package config + +import ( + "path/filepath" + + "github.com/stashapp/stash/pkg/fsutil" +) + +// Stash configuration details +type StashConfigInput struct { + Path string `json:"path"` + ExcludeVideo bool `json:"excludeVideo"` + ExcludeImage bool `json:"excludeImage"` +} + +type StashConfig struct { + Path string `json:"path"` + ExcludeVideo bool `json:"excludeVideo"` + ExcludeImage bool `json:"excludeImage"` +} + +type StashConfigs []*StashConfig + +func (s StashConfigs) GetStashFromPath(path string) *StashConfig { + for _, f := range s { + if fsutil.IsPathInDir(f.Path, filepath.Dir(path)) { + return f + } + } + return nil +} + +func (s StashConfigs) GetStashFromDirPath(dirPath string) *StashConfig { + for _, f := range s { + if fsutil.IsPathInDir(f.Path, dirPath) { + return f + } + } + return nil +} diff --git a/internal/manager/config/tasks.go b/internal/manager/config/tasks.go index 2f69c8a50..1e541fcc5 100644 --- a/internal/manager/config/tasks.go +++ b/internal/manager/config/tasks.go @@ -7,6 +7,8 @@ type ScanMetadataOptions struct { // Strip file extension from title // Deprecated: not implemented StripFileExtension bool `json:"stripFileExtension"` + // Generate scene covers during scan + ScanGenerateCovers bool `json:"scanGenerateCovers"` // Generate previews during scan ScanGeneratePreviews bool `json:"scanGeneratePreviews"` // Generate image previews during scan diff --git a/internal/manager/generator_interactive_heatmap_speed.go b/internal/manager/generator_interactive_heatmap_speed.go index c9b295983..3b3b98bf4 100644 --- a/internal/manager/generator_interactive_heatmap_speed.go +++ b/internal/manager/generator_interactive_heatmap_speed.go @@ -15,14 +15,13 @@ import ( ) type InteractiveHeatmapSpeedGenerator struct { - sceneDurationMilli int64 - InteractiveSpeed int - Funscript Script - FunscriptPath string - HeatmapPath string - Width int - Height int - NumSegments int + InteractiveSpeed int + Funscript Script + Width int + Height int + NumSegments int + + DrawRange bool } type Script struct { @@ -33,8 +32,7 @@ type Script struct { // Range is the percentage of a full stroke to use. Range int `json:"range,omitempty"` // Actions are the timed moves. - Actions []Action `json:"actions"` - AvarageSpeed int64 + Actions []Action `json:"actions"` } // Action is a move at a specific time. @@ -50,23 +48,22 @@ type Action struct { } type GradientTable []struct { - Col colorful.Color - Pos float64 + Col colorful.Color + Pos float64 + YRange [2]float64 } -func NewInteractiveHeatmapSpeedGenerator(funscriptPath string, heatmapPath string, sceneDuration float64) *InteractiveHeatmapSpeedGenerator { +func NewInteractiveHeatmapSpeedGenerator(drawRange bool) *InteractiveHeatmapSpeedGenerator { return &InteractiveHeatmapSpeedGenerator{ - sceneDurationMilli: int64(sceneDuration * 1000), - FunscriptPath: funscriptPath, - HeatmapPath: heatmapPath, - Width: 320, - Height: 15, - NumSegments: 150, + Width: 1280, + Height: 60, + NumSegments: 600, + DrawRange: drawRange, } } -func (g *InteractiveHeatmapSpeedGenerator) Generate() error { - funscript, err := g.LoadFunscriptData(g.FunscriptPath) +func (g *InteractiveHeatmapSpeedGenerator) Generate(funscriptPath string, heatmapPath string, sceneDuration float64) error { + funscript, err := g.LoadFunscriptData(funscriptPath, sceneDuration) if err != nil { return err @@ -79,7 +76,7 @@ func (g *InteractiveHeatmapSpeedGenerator) Generate() error { g.Funscript = funscript g.Funscript.UpdateIntensityAndSpeed() - err = g.RenderHeatmap() + err = g.RenderHeatmap(heatmapPath) if err != nil { return err @@ -90,7 +87,7 @@ func (g *InteractiveHeatmapSpeedGenerator) Generate() error { return nil } -func (g *InteractiveHeatmapSpeedGenerator) LoadFunscriptData(path string) (Script, error) { +func (g *InteractiveHeatmapSpeedGenerator) LoadFunscriptData(path string, sceneDuration float64) (Script, error) { data, err := os.ReadFile(path) if err != nil { return Script{}, err @@ -111,8 +108,9 @@ func (g *InteractiveHeatmapSpeedGenerator) LoadFunscriptData(path string) (Scrip // trim actions with negative timestamps to avoid index range errors when generating heatmap // #3181 - also trim actions that occur after the scene duration loggedBadTimestamp := false + sceneDurationMilli := int64(sceneDuration * 1000) isValid := func(x int64) bool { - return x >= 0 && x < g.sceneDurationMilli + return x >= 0 && x < sceneDurationMilli } i := 0 @@ -157,14 +155,27 @@ func (funscript *Script) UpdateIntensityAndSpeed() { } // funscript needs to have intensity updated first -func (g *InteractiveHeatmapSpeedGenerator) RenderHeatmap() error { - +func (g *InteractiveHeatmapSpeedGenerator) RenderHeatmap(heatmapPath string) error { gradient := g.Funscript.getGradientTable(g.NumSegments) img := image.NewRGBA(image.Rect(0, 0, g.Width, g.Height)) for x := 0; x < g.Width; x++ { - c := gradient.GetInterpolatedColorFor(float64(x) / float64(g.Width)) - draw.Draw(img, image.Rect(x, 0, x+1, g.Height), &image.Uniform{c}, image.Point{}, draw.Src) + xPos := float64(x) / float64(g.Width) + c := gradient.GetInterpolatedColorFor(xPos) + + y0 := 0 + y1 := g.Height + + if g.DrawRange { + yRange := gradient.GetYRange(xPos) + top := int(yRange[0] / 100.0 * float64(g.Height)) + bottom := int(yRange[1] / 100.0 * float64(g.Height)) + + y0 = g.Height - top + y1 = g.Height - bottom + } + + draw.Draw(img, image.Rect(x, y0, x+1, y1), &image.Uniform{c}, image.Point{}, draw.Src) } // add 10 minute marks @@ -178,7 +189,7 @@ func (g *InteractiveHeatmapSpeedGenerator) RenderHeatmap() error { ts += tick } - outpng, err := os.Create(g.HeatmapPath) + outpng, err := os.Create(heatmapPath) if err != nil { return err } @@ -217,27 +228,96 @@ func (gt GradientTable) GetInterpolatedColorFor(t float64) colorful.Color { return gt[len(gt)-1].Col } +func (gt GradientTable) GetYRange(t float64) [2]float64 { + for i := 0; i < len(gt)-1; i++ { + c1 := gt[i] + c2 := gt[i+1] + if c1.Pos <= t && t <= c2.Pos { + // TODO: We are in between c1 and c2. Go blend them! + return c1.YRange + } + } + + // Nothing found? Means we're at (or past) the last gradient keypoint. + return gt[len(gt)-1].YRange +} + func (funscript Script) getGradientTable(numSegments int) GradientTable { + const windowSize = 15 + const backfillThreshold = 500 + segments := make([]struct { count int intensity int + yRange [2]float64 + at int64 }, numSegments) gradient := make(GradientTable, numSegments) + posList := []int{} maxts := funscript.Actions[len(funscript.Actions)-1].At for _, a := range funscript.Actions { + posList = append(posList, a.Pos) + + if len(posList) > windowSize { + posList = posList[1:] + } + + sortedPos := make([]int, len(posList)) + copy(sortedPos, posList) + sort.Ints(sortedPos) + + topHalf := sortedPos[len(sortedPos)/2:] + bottomHalf := sortedPos[0 : len(sortedPos)/2] + + var totalBottom int + var totalTop int + + for _, value := range bottomHalf { + totalBottom += value + } + for _, value := range topHalf { + totalTop += value + } + + averageBottom := float64(totalBottom) / float64(len(bottomHalf)) + averageTop := float64(totalTop) / float64(len(topHalf)) + segment := int(float64(a.At) / float64(maxts+1) * float64(numSegments)) // #3181 - sanity check. Clamp segment to numSegments-1 if segment >= numSegments { segment = numSegments - 1 } + segments[segment].at = a.At segments[segment].count++ segments[segment].intensity += int(a.Intensity) + segments[segment].yRange[0] = averageTop + segments[segment].yRange[1] = averageBottom + } + + lastSegment := segments[0] + + // Fill in gaps in segments + for i := 0; i < numSegments; i++ { + segmentTS := int64(float64(i) / float64(numSegments)) + + // Empty segment - fill it with the previous up to backfillThreshold ms + if segments[i].count == 0 { + if segmentTS-lastSegment.at < backfillThreshold { + segments[i].count = lastSegment.count + segments[i].intensity = lastSegment.intensity + segments[i].yRange[0] = lastSegment.yRange[0] + segments[i].yRange[1] = lastSegment.yRange[1] + } + } else { + lastSegment = segments[i] + } } for i := 0; i < numSegments; i++ { gradient[i].Pos = float64(i) / float64(numSegments-1) + gradient[i].YRange = segments[i].yRange if segments[i].count > 0 { gradient[i].Col = getSegmentColor(float64(segments[i].intensity) / float64(segments[i].count)) } else { diff --git a/internal/manager/generator_sprite.go b/internal/manager/generator_sprite.go index 47110462d..fa265cd56 100644 --- a/internal/manager/generator_sprite.go +++ b/internal/manager/generator_sprite.go @@ -75,9 +75,10 @@ func NewSpriteGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, imageO SlowSeek: slowSeek, Columns: cols, g: &generate.Generator{ - Encoder: instance.FFMPEG, - LockManager: instance.ReadLockManager, - ScenePaths: instance.Paths.Scene, + Encoder: instance.FFMPEG, + FFMpegConfig: instance.Config, + LockManager: instance.ReadLockManager, + ScenePaths: instance.Paths.Scene, }, }, nil } diff --git a/internal/manager/manager.go b/internal/manager/manager.go index 9f2492ea7..a591e98ab 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -26,11 +26,9 @@ import ( "github.com/stashapp/stash/pkg/image" "github.com/stashapp/stash/pkg/job" "github.com/stashapp/stash/pkg/logger" - "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/paths" "github.com/stashapp/stash/pkg/plugin" "github.com/stashapp/stash/pkg/scene" - "github.com/stashapp/stash/pkg/scene/generate" "github.com/stashapp/stash/pkg/scraper" "github.com/stashapp/stash/pkg/session" "github.com/stashapp/stash/pkg/sqlite" @@ -100,6 +98,10 @@ type SetupInput struct { DatabaseFile string `json:"databaseFile"` // Empty to indicate default GeneratedLocation string `json:"generatedLocation"` + // Empty to indicate default + CacheLocation string `json:"cacheLocation"` + // Empty to indicate database storage for blobs + BlobsLocation string `json:"blobsLocation"` } type Manager struct { @@ -108,8 +110,9 @@ type Manager struct { Paths *paths.Paths - FFMPEG ffmpeg.FFMpeg - FFProbe ffmpeg.FFProbe + FFMPEG *ffmpeg.FFMpeg + FFProbe ffmpeg.FFProbe + StreamManager *ffmpeg.StreamManager ReadLockManager *fsutil.ReadLockManager @@ -287,19 +290,6 @@ func galleryFileFilter(ctx context.Context, f file.File) bool { return isZip(f.Base().Basename) } -type coverGenerator struct { -} - -func (g *coverGenerator) GenerateCover(ctx context.Context, scene *models.Scene, f *file.VideoFile) error { - gg := generate.Generator{ - Encoder: instance.FFMPEG, - LockManager: instance.ReadLockManager, - ScenePaths: instance.Paths.Scene, - } - - return gg.Screenshot(ctx, f.Path, scene.GetHash(instance.Config.GetVideoFileNamingAlgorithm()), f.Width, f.Duration, generate.ScreenshotOptions{}) -} - func makeScanner(db *sqlite.Database, pluginCache *plugin.Cache) *file.Scanner { return &file.Scanner{ Repository: file.Repository{ @@ -427,8 +417,11 @@ func initFFMPEG(ctx context.Context) error { } } - instance.FFMPEG = ffmpeg.FFMpeg(ffmpegPath) + instance.FFMPEG = ffmpeg.NewEncoder(ffmpegPath) instance.FFProbe = ffmpeg.FFProbe(ffprobePath) + + instance.FFMPEG.InitHWSupport(ctx) + instance.RefreshStreamManager() } return nil @@ -451,7 +444,7 @@ func (s *Manager) PostInit(ctx context.Context) error { logger.Warnf("could not set initial configuration: %v", err) } - *s.Paths = paths.NewPaths(s.Config.GetGeneratedPath()) + *s.Paths = paths.NewPaths(s.Config.GetGeneratedPath(), s.Config.GetBlobsPath()) s.RefreshConfig() s.SessionStore = session.NewStore(s.Config) s.PluginCache.RegisterSessionStore(s.SessionStore) @@ -460,6 +453,8 @@ func (s *Manager) PostInit(ctx context.Context) error { logger.Errorf("Error reading plugin configs: %s", err.Error()) } + s.SetBlobStoreOptions() + s.ScraperCache = instance.initScraperCache() writeStashIcon() @@ -491,9 +486,28 @@ func (s *Manager) PostInit(ctx context.Context) error { return err } + // Set the proxy if defined in config + if s.Config.GetProxy() != "" { + os.Setenv("HTTP_PROXY", s.Config.GetProxy()) + os.Setenv("HTTPS_PROXY", s.Config.GetProxy()) + os.Setenv("NO_PROXY", s.Config.GetNoProxy()) + logger.Info("Using HTTP Proxy") + } + return nil } +func (s *Manager) SetBlobStoreOptions() { + storageType := s.Config.GetBlobsStorage() + blobsPath := s.Config.GetBlobsPath() + + s.Database.SetBlobStoreOptions(sqlite.BlobStoreOptions{ + UseFilesystem: storageType == config.BlobStorageTypeFilesystem, + UseDatabase: storageType == config.BlobStorageTypeDatabase, + Path: blobsPath, + }) +} + func writeStashIcon() { p := FaviconProvider{ UIBox: ui.UIBox, @@ -525,7 +539,7 @@ func (s *Manager) initScraperCache() *scraper.Cache { } func (s *Manager) RefreshConfig() { - *s.Paths = paths.NewPaths(s.Config.GetGeneratedPath()) + *s.Paths = paths.NewPaths(s.Config.GetGeneratedPath(), s.Config.GetBlobsPath()) config := s.Config if config.Validate() == nil { if err := fsutil.EnsureDir(s.Paths.Generated.Screenshots); err != nil { @@ -555,6 +569,19 @@ func (s *Manager) RefreshScraperCache() { s.ScraperCache = s.initScraperCache() } +// RefreshStreamManager refreshes the stream manager. Call this when cache directory +// changes. +func (s *Manager) RefreshStreamManager() { + // shutdown existing manager if needed + if s.StreamManager != nil { + s.StreamManager.Shutdown() + s.StreamManager = nil + } + + cacheDir := s.Config.GetCachePath() + s.StreamManager = ffmpeg.NewStreamManager(cacheDir, s.FFMPEG, s.FFProbe, s.Config, s.ReadLockManager) +} + func setSetupDefaults(input *SetupInput) { if input.ConfigLocation == "" { input.ConfigLocation = filepath.Join(fsutil.GetHomeDirectory(), ".stash", "config.yml") @@ -564,6 +591,9 @@ func setSetupDefaults(input *SetupInput) { if input.GeneratedLocation == "" { input.GeneratedLocation = filepath.Join(configDir, "generated") } + if input.CacheLocation == "" { + input.CacheLocation = filepath.Join(configDir, "cache") + } if input.DatabaseFile == "" { input.DatabaseFile = filepath.Join(configDir, "stash-go.sqlite") @@ -577,24 +607,31 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error { // create the config directory if it does not exist // don't do anything if config is already set in the environment if !config.FileEnvSet() { - configDir := filepath.Dir(input.ConfigLocation) + // #3304 - if config path is relative, it breaks the ffmpeg/ffprobe + // 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) + + configDir := filepath.Dir(configFile) + if exists, _ := fsutil.DirExists(configDir); !exists { - if err := os.Mkdir(configDir, 0755); err != nil { + if err := os.MkdirAll(configDir, 0755); err != nil { return fmt.Errorf("error creating config directory: %v", err) } } - if err := fsutil.Touch(input.ConfigLocation); err != nil { + if err := fsutil.Touch(configFile); err != nil { return fmt.Errorf("error creating config file: %v", err) } - s.Config.SetConfigFile(input.ConfigLocation) + s.Config.SetConfigFile(configFile) } // create the generated directory if it does not exist if !c.HasOverride(config.Generated) { if exists, _ := fsutil.DirExists(input.GeneratedLocation); !exists { - if err := os.Mkdir(input.GeneratedLocation, 0755); err != nil { + if err := os.MkdirAll(input.GeneratedLocation, 0755); err != nil { return fmt.Errorf("error creating generated directory: %v", err) } } @@ -602,6 +639,33 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error { s.Config.Set(config.Generated, input.GeneratedLocation) } + // create the cache directory if it does not exist + if !c.HasOverride(config.Cache) { + if exists, _ := fsutil.DirExists(input.CacheLocation); !exists { + if err := os.MkdirAll(input.CacheLocation, 0755); err != nil { + return fmt.Errorf("error creating cache directory: %v", err) + } + } + + s.Config.Set(config.Cache, input.CacheLocation) + } + + // if blobs path was provided then use filesystem based blob storage + if input.BlobsLocation != "" { + if !c.HasOverride(config.BlobsPath) { + if exists, _ := fsutil.DirExists(input.BlobsLocation); !exists { + if err := os.MkdirAll(input.BlobsLocation, 0755); err != nil { + return fmt.Errorf("error creating blobs directory: %v", err) + } + } + } + + s.Config.Set(config.BlobsPath, input.BlobsLocation) + s.Config.Set(config.BlobsStorage, config.BlobStorageTypeFilesystem) + } else { + s.Config.Set(config.BlobsStorage, config.BlobStorageTypeDatabase) + } + // set the configuration if !c.HasOverride(config.Database) { s.Config.Set(config.Database, input.DatabaseFile) @@ -634,7 +698,7 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error { } func (s *Manager) validateFFMPEG() error { - if s.FFMPEG == "" || s.FFProbe == "" { + if s.FFMPEG == nil || s.FFProbe == "" { return errors.New("missing ffmpeg and/or ffprobe") } @@ -719,6 +783,11 @@ func (s *Manager) Shutdown(code int) { // stop any profiling at exit pprof.StopCPUProfile() + if s.StreamManager != nil { + s.StreamManager.Shutdown() + s.StreamManager = nil + } + // TODO: Each part of the manager needs to gracefully stop at some point // for now, we just close the database. err := s.Database.Close() diff --git a/internal/manager/manager_tasks.go b/internal/manager/manager_tasks.go index 33354073d..10bcacab0 100644 --- a/internal/manager/manager_tasks.go +++ b/internal/manager/manager_tasks.go @@ -37,9 +37,9 @@ func getScanPaths(inputPaths []string) []*config.StashConfig { return stashPaths } - var ret []*config.StashConfig + var ret config.StashConfigs for _, p := range inputPaths { - s := getStashFromDirPath(stashPaths, p) + s := stashPaths.GetStashFromDirPath(p) if s == nil { logger.Warnf("%s is not in the configured stash paths", p) continue @@ -194,11 +194,11 @@ func (s *Manager) generateScreenshot(ctx context.Context, sceneId string, at *fl return } - task := GenerateScreenshotTask{ - txnManager: s.Repository, - Scene: *scene, - ScreenshotAt: at, - fileNamingAlgorithm: config.GetInstance().GetVideoFileNamingAlgorithm(), + task := GenerateCoverTask{ + txnManager: s.Repository, + Scene: *scene, + ScreenshotAt: at, + Overwrite: true, } task.Start(ctx) @@ -336,6 +336,10 @@ func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, input StashBoxB for _, performerID := range input.PerformerIds { if id, err := strconv.Atoi(performerID); err == nil { performer, err := performerQuery.Find(ctx, id) + if err == nil { + err = performer.LoadStashIDs(ctx, performerQuery) + } + if err == nil { tasks = append(tasks, StashBoxPerformerTagTask{ performer: performer, @@ -382,6 +386,10 @@ func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, input StashBoxB } 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, StashBoxPerformerTagTask{ performer: performer, refresh: input.Refresh, diff --git a/internal/manager/repository.go b/internal/manager/repository.go index 713e017b4..41ac5f12e 100644 --- a/internal/manager/repository.go +++ b/internal/manager/repository.go @@ -35,7 +35,6 @@ type SceneReaderWriter interface { type FileReaderWriter interface { file.Store - file.Finder Query(ctx context.Context, options models.FileQueryOptions) (*models.FileQueryResult, error) GetCaptions(ctx context.Context, fileID file.ID) ([]*models.VideoCaption, error) IsPrimary(ctx context.Context, fileID file.ID) (bool, error) @@ -43,24 +42,24 @@ type FileReaderWriter interface { type FolderReaderWriter interface { file.FolderStore - Find(ctx context.Context, id file.FolderID) (*file.Folder, error) } type Repository struct { models.TxnManager - File FileReaderWriter - Folder FolderReaderWriter - Gallery GalleryReaderWriter - Image ImageReaderWriter - Movie models.MovieReaderWriter - Performer models.PerformerReaderWriter - Scene SceneReaderWriter - SceneMarker models.SceneMarkerReaderWriter - ScrapedItem models.ScrapedItemReaderWriter - Studio models.StudioReaderWriter - Tag models.TagReaderWriter - SavedFilter models.SavedFilterReaderWriter + File FileReaderWriter + Folder FolderReaderWriter + Gallery GalleryReaderWriter + GalleryChapter models.GalleryChapterReaderWriter + Image ImageReaderWriter + Movie models.MovieReaderWriter + Performer models.PerformerReaderWriter + Scene SceneReaderWriter + SceneMarker models.SceneMarkerReaderWriter + ScrapedItem models.ScrapedItemReaderWriter + Studio models.StudioReaderWriter + Tag models.TagReaderWriter + SavedFilter models.SavedFilterReaderWriter } func (r *Repository) WithTxn(ctx context.Context, fn txn.TxnFunc) error { @@ -79,19 +78,20 @@ func sqliteRepository(d *sqlite.Database) Repository { txnRepo := d.TxnRepository() return Repository{ - TxnManager: txnRepo, - File: d.File, - Folder: d.Folder, - Gallery: d.Gallery, - Image: d.Image, - Movie: txnRepo.Movie, - Performer: txnRepo.Performer, - Scene: d.Scene, - SceneMarker: txnRepo.SceneMarker, - ScrapedItem: txnRepo.ScrapedItem, - Studio: txnRepo.Studio, - Tag: txnRepo.Tag, - SavedFilter: txnRepo.SavedFilter, + TxnManager: txnRepo, + File: d.File, + Folder: d.Folder, + Gallery: d.Gallery, + GalleryChapter: txnRepo.GalleryChapter, + Image: d.Image, + Movie: txnRepo.Movie, + Performer: txnRepo.Performer, + Scene: d.Scene, + SceneMarker: txnRepo.SceneMarker, + ScrapedItem: txnRepo.ScrapedItem, + Studio: txnRepo.Studio, + Tag: txnRepo.Tag, + SavedFilter: txnRepo.SavedFilter, } } diff --git a/internal/manager/running_streams.go b/internal/manager/running_streams.go index b715c0c5c..8fa397640 100644 --- a/internal/manager/running_streams.go +++ b/internal/manager/running_streams.go @@ -5,10 +5,10 @@ import ( "errors" "io" "net/http" - "time" "github.com/stashapp/stash/internal/manager/config" "github.com/stashapp/stash/internal/static" + "github.com/stashapp/stash/pkg/ffmpeg" "github.com/stashapp/stash/pkg/fsutil" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" @@ -16,58 +16,6 @@ import ( "github.com/stashapp/stash/pkg/utils" ) -type StreamRequestContext struct { - context.Context - ResponseWriter http.ResponseWriter -} - -func NewStreamRequestContext(w http.ResponseWriter, r *http.Request) *StreamRequestContext { - return &StreamRequestContext{ - Context: r.Context(), - ResponseWriter: w, - } -} - -func (c *StreamRequestContext) Cancel() { - hj, ok := (c.ResponseWriter).(http.Hijacker) - if !ok { - return - } - - // hijack and close the connection - conn, bw, _ := hj.Hijack() - if conn != nil { - if bw != nil { - // notify end of stream - _, err := bw.WriteString("0\r\n") - if err != nil { - logger.Warnf("unable to write end of stream: %v", err) - } - _, err = bw.WriteString("\r\n") - if err != nil { - logger.Warnf("unable to write end of stream: %v", err) - } - - // flush the buffer, but don't wait indefinitely - timeout := make(chan struct{}, 1) - go func() { - _ = bw.Flush() - close(timeout) - }() - - const waitTime = time.Second - - select { - case <-timeout: - case <-time.After(waitTime): - logger.Warnf("unable to flush buffer - closing connection") - } - } - - conn.Close() - } -} - func KillRunningStreams(scene *models.Scene, fileNamingAlgo models.HashAlgorithm) { instance.ReadLockManager.Cancel(scene.Path) @@ -94,7 +42,7 @@ func (s *SceneServer) StreamSceneDirect(scene *models.Scene, w http.ResponseWrit fileNamingAlgo := config.GetInstance().GetVideoFileNamingAlgorithm() filepath := GetInstance().Paths.Scene.GetStreamPath(scene.Path, scene.GetHash(fileNamingAlgo)) - streamRequestCtx := NewStreamRequestContext(w, r) + 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 @@ -106,17 +54,6 @@ func (s *SceneServer) StreamSceneDirect(scene *models.Scene, w http.ResponseWrit func (s *SceneServer) ServeScreenshot(scene *models.Scene, w http.ResponseWriter, r *http.Request) { const defaultSceneImage = "scene/scene.svg" - if scene.Path != "" { - filepath := GetInstance().Paths.Scene.GetScreenshotPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())) - - // fall back to the scene image blob if the file isn't present - screenshotExists, _ := fsutil.FileExists(filepath) - if screenshotExists { - http.ServeFile(w, r, filepath) - return - } - } - var cover []byte readTxnErr := txn.WithReadTxn(r.Context(), s.TxnManager, func(ctx context.Context) error { cover, _ = s.SceneCoverGetter.GetCover(ctx, scene.ID) @@ -127,11 +64,21 @@ func (s *SceneServer) ServeScreenshot(scene *models.Scene, w http.ResponseWriter } if readTxnErr != nil { logger.Warnf("read transaction error on fetch screenshot: %v", readTxnErr) - http.Error(w, readTxnErr.Error(), http.StatusInternalServerError) - return } if cover == nil { + // fallback to legacy image if present + if scene.Path != "" { + filepath := GetInstance().Paths.Scene.GetLegacyScreenshotPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())) + + // fall back to the scene image blob if the file isn't present + screenshotExists, _ := fsutil.FileExists(filepath) + if screenshotExists { + http.ServeFile(w, r, filepath) + return + } + } + // fallback to default cover if none found // should always be there f, _ := static.Scene.Open(defaultSceneImage) diff --git a/internal/manager/scene.go b/internal/manager/scene.go index 30d1948d8..a653cb632 100644 --- a/internal/manager/scene.go +++ b/internal/manager/scene.go @@ -11,6 +11,52 @@ import ( "github.com/stashapp/stash/pkg/models" ) +type SceneStreamEndpoint struct { + URL string `json:"url"` + MimeType *string `json:"mime_type"` + Label *string `json:"label"` +} + +type endpointType struct { + label string + mimeType string + extension string +} + +var ( + directEndpointType = endpointType{ + label: "Direct stream", + mimeType: ffmpeg.MimeMp4Video, + extension: "", + } + mp4EndpointType = endpointType{ + label: "MP4", + mimeType: ffmpeg.MimeMp4Video, + extension: ".mp4", + } + mkvEndpointType = endpointType{ + label: "MKV", + // use mp4 mimetype to trick the client, since many clients won't try mkv + mimeType: ffmpeg.MimeMp4Video, + extension: ".mkv", + } + webmEndpointType = endpointType{ + label: "WEBM", + mimeType: ffmpeg.MimeWebmVideo, + extension: ".webm", + } + hlsEndpointType = endpointType{ + label: "HLS", + mimeType: ffmpeg.MimeHLS, + extension: ".m3u8", + } + dashEndpointType = endpointType{ + label: "DASH", + mimeType: ffmpeg.MimeDASH, + extension: ".mpd", + } +) + func GetVideoFileContainer(file *file.VideoFile) (ffmpeg.Container, error) { var container ffmpeg.Container format := file.Format @@ -30,48 +76,6 @@ func GetVideoFileContainer(file *file.VideoFile) (ffmpeg.Container, error) { return container, nil } -func includeSceneStreamPath(f *file.VideoFile, streamingResolution models.StreamingResolutionEnum, maxStreamingTranscodeSize models.StreamingResolutionEnum) bool { - // convert StreamingResolutionEnum to ResolutionEnum so we can get the min - // resolution - convertedRes := models.ResolutionEnum(streamingResolution) - - minResolution := convertedRes.GetMinResolution() - sceneResolution := f.GetMinResolution() - - // don't include if scene resolution is smaller than the streamingResolution - if sceneResolution != 0 && sceneResolution < minResolution { - return false - } - - // if we always allow everything, then return true - if maxStreamingTranscodeSize == models.StreamingResolutionEnumOriginal { - return true - } - - // convert StreamingResolutionEnum to ResolutionEnum - maxStreamingResolution := models.ResolutionEnum(maxStreamingTranscodeSize) - return maxStreamingResolution.GetMinResolution() >= minResolution -} - -type SceneStreamEndpoint struct { - URL string `json:"url"` - MimeType *string `json:"mime_type"` - Label *string `json:"label"` -} - -func makeStreamEndpoint(streamURL *url.URL, streamingResolution models.StreamingResolutionEnum, mimeType, label string) *SceneStreamEndpoint { - urlCopy := *streamURL - v := urlCopy.Query() - v.Set("resolution", streamingResolution.String()) - urlCopy.RawQuery = v.Encode() - - return &SceneStreamEndpoint{ - URL: urlCopy.String(), - MimeType: &mimeType, - Label: &label, - } -} - func GetSceneStreamPaths(scene *models.Scene, directStreamURL *url.URL, maxStreamingTranscodeSize models.StreamingResolutionEnum) ([]*SceneStreamEndpoint, error) { if scene == nil { return nil, fmt.Errorf("nil scene") @@ -82,13 +86,66 @@ func GetSceneStreamPaths(scene *models.Scene, directStreamURL *url.URL, maxStrea return nil, nil } - var ret []*SceneStreamEndpoint - mimeWebm := ffmpeg.MimeWebm - mimeHLS := ffmpeg.MimeHLS - mimeMp4 := ffmpeg.MimeMp4 + // convert StreamingResolutionEnum to ResolutionEnum + maxStreamingResolution := models.ResolutionEnum(maxStreamingTranscodeSize) + sceneResolution := pf.GetMinResolution() + includeSceneStreamPath := func(streamingResolution models.StreamingResolutionEnum) bool { + var minResolution int + if streamingResolution == models.StreamingResolutionEnumOriginal { + minResolution = sceneResolution + } else { + // convert StreamingResolutionEnum to ResolutionEnum so we can get the min + // resolution + convertedRes := models.ResolutionEnum(streamingResolution) + minResolution = convertedRes.GetMinResolution() - labelWebm := "webm" - labelHLS := "HLS" + // don't include if scene resolution is smaller than the streamingResolution + if sceneResolution != 0 && sceneResolution < minResolution { + return false + } + } + + // if we always allow everything, then return true + if maxStreamingTranscodeSize == models.StreamingResolutionEnumOriginal { + return true + } + + return maxStreamingResolution.GetMinResolution() >= minResolution + } + + makeStreamEndpoint := func(t endpointType, resolution models.StreamingResolutionEnum) *SceneStreamEndpoint { + url := *directStreamURL + url.Path += t.extension + + label := t.label + + if resolution != "" { + v := url.Query() + v.Set("resolution", resolution.String()) + url.RawQuery = v.Encode() + + switch resolution { + case models.StreamingResolutionEnumFourK: + label += " 4K (2160p)" + case models.StreamingResolutionEnumFullHd: + label += " Full HD (1080p)" + case models.StreamingResolutionEnumStandardHd: + label += " HD (720p)" + case models.StreamingResolutionEnumStandard: + label += " Standard (480p)" + case models.StreamingResolutionEnumLow: + label += " Low (240p)" + } + } + + return &SceneStreamEndpoint{ + URL: url.String(), + MimeType: &t.mimeType, + Label: &label, + } + } + + var endpoints []*SceneStreamEndpoint // direct stream should only apply when the audio codec is supported audioCodec := ffmpeg.MissingUnsupported @@ -99,99 +156,68 @@ func GetSceneStreamPaths(scene *models.Scene, directStreamURL *url.URL, maxStrea // don't care if we can't get the container container, _ := GetVideoFileContainer(pf) - replaceSuffix := func(suffix string) *url.URL { - urlCopy := *directStreamURL - urlCopy.Path += suffix - return &urlCopy - } - if HasTranscode(scene, config.GetInstance().GetVideoFileNamingAlgorithm()) || ffmpeg.IsValidAudioForContainer(audioCodec, container) { - label := "Direct stream" - ret = append(ret, &SceneStreamEndpoint{ - URL: directStreamURL.String(), - MimeType: &mimeMp4, - Label: &label, - }) + endpoints = append(endpoints, makeStreamEndpoint(directEndpointType, "")) } // only add mkv stream endpoint if the scene container is an mkv already if container == ffmpeg.Matroska { - label := "mkv" - ret = append(ret, &SceneStreamEndpoint{ - URL: replaceSuffix(".mkv").String(), - // set mkv to mp4 to trick the client, since many clients won't try mkv - MimeType: &mimeMp4, - Label: &label, - }) + endpoints = append(endpoints, makeStreamEndpoint(mkvEndpointType, "")) } - // WEBM quality transcoding options - // Note: These have the wrong mime type intentionally to allow jwplayer to selection between mp4/webm - webmLabelFourK := "WEBM 4K (2160p)" // "FOUR_K" - webmLabelFullHD := "WEBM Full HD (1080p)" // "FULL_HD" - webmLabelStandardHD := "WEBM HD (720p)" // "STANDARD_HD" - webmLabelStandard := "WEBM Standard (480p)" // "STANDARD" - webmLabelLow := "WEBM Low (240p)" // "LOW" + mp4Streams := []*SceneStreamEndpoint{} + webmStreams := []*SceneStreamEndpoint{} + hlsStreams := []*SceneStreamEndpoint{} + dashStreams := []*SceneStreamEndpoint{} - // Setup up lower quality transcoding options (MP4) - mp4LabelFourK := "MP4 4K (2160p)" // "FOUR_K" - mp4LabelFullHD := "MP4 Full HD (1080p)" // "FULL_HD" - mp4LabelStandardHD := "MP4 HD (720p)" // "STANDARD_HD" - mp4LabelStandard := "MP4 Standard (480p)" // "STANDARD" - mp4LabelLow := "MP4 Low (240p)" // "LOW" - - var webmStreams []*SceneStreamEndpoint - var mp4Streams []*SceneStreamEndpoint - - webmURL := replaceSuffix(".webm") - mp4URL := replaceSuffix(".mp4") - - if includeSceneStreamPath(pf, models.StreamingResolutionEnumFourK, maxStreamingTranscodeSize) { - webmStreams = append(webmStreams, makeStreamEndpoint(webmURL, models.StreamingResolutionEnumFourK, mimeMp4, webmLabelFourK)) - mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4URL, models.StreamingResolutionEnumFourK, mimeMp4, mp4LabelFourK)) + if includeSceneStreamPath(models.StreamingResolutionEnumOriginal) { + mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumOriginal)) + webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumOriginal)) + hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumOriginal)) + dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumOriginal)) } - if includeSceneStreamPath(pf, models.StreamingResolutionEnumFullHd, maxStreamingTranscodeSize) { - webmStreams = append(webmStreams, makeStreamEndpoint(webmURL, models.StreamingResolutionEnumFullHd, mimeMp4, webmLabelFullHD)) - mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4URL, models.StreamingResolutionEnumFullHd, mimeMp4, mp4LabelFullHD)) + if includeSceneStreamPath(models.StreamingResolutionEnumFourK) { + mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumFourK)) + webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumFourK)) + hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumFourK)) + dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumFourK)) } - if includeSceneStreamPath(pf, models.StreamingResolutionEnumStandardHd, maxStreamingTranscodeSize) { - webmStreams = append(webmStreams, makeStreamEndpoint(webmURL, models.StreamingResolutionEnumStandardHd, mimeMp4, webmLabelStandardHD)) - mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4URL, models.StreamingResolutionEnumStandardHd, mimeMp4, mp4LabelStandardHD)) + if includeSceneStreamPath(models.StreamingResolutionEnumFullHd) { + mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumFullHd)) + webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumFullHd)) + hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumFullHd)) + dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumFullHd)) } - if includeSceneStreamPath(pf, models.StreamingResolutionEnumStandard, maxStreamingTranscodeSize) { - webmStreams = append(webmStreams, makeStreamEndpoint(webmURL, models.StreamingResolutionEnumStandard, mimeMp4, webmLabelStandard)) - mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4URL, models.StreamingResolutionEnumStandard, mimeMp4, mp4LabelStandard)) + if includeSceneStreamPath(models.StreamingResolutionEnumStandardHd) { + mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumStandardHd)) + webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumStandardHd)) + hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumStandardHd)) + dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumStandardHd)) } - if includeSceneStreamPath(pf, models.StreamingResolutionEnumLow, maxStreamingTranscodeSize) { - webmStreams = append(webmStreams, makeStreamEndpoint(webmURL, models.StreamingResolutionEnumLow, mimeMp4, webmLabelLow)) - mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4URL, models.StreamingResolutionEnumLow, mimeMp4, mp4LabelLow)) + if includeSceneStreamPath(models.StreamingResolutionEnumStandard) { + mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumStandard)) + webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumStandard)) + hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumStandard)) + dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumStandard)) } - ret = append(ret, webmStreams...) - ret = append(ret, mp4Streams...) - - defaultStreams := []*SceneStreamEndpoint{ - { - URL: replaceSuffix(".webm").String(), - MimeType: &mimeWebm, - Label: &labelWebm, - }, + if includeSceneStreamPath(models.StreamingResolutionEnumLow) { + mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumLow)) + webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumLow)) + hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumLow)) + dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumLow)) } - ret = append(ret, defaultStreams...) + endpoints = append(endpoints, mp4Streams...) + endpoints = append(endpoints, webmStreams...) + endpoints = append(endpoints, hlsStreams...) + endpoints = append(endpoints, dashStreams...) - hls := SceneStreamEndpoint{ - URL: replaceSuffix(".m3u8").String(), - MimeType: &mimeHLS, - Label: &labelHLS, - } - ret = append(ret, &hls) - - return ret, nil + return endpoints, nil } // HasTranscode returns true if a transcoded video exists for the provided diff --git a/internal/manager/task/migrate_blobs.go b/internal/manager/task/migrate_blobs.go new file mode 100644 index 000000000..4cda65725 --- /dev/null +++ b/internal/manager/task/migrate_blobs.go @@ -0,0 +1,129 @@ +package task + +import ( + "context" + "fmt" + + "github.com/stashapp/stash/pkg/job" + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/txn" +) + +type BlobStoreMigrator interface { + Count(ctx context.Context) (int, error) + FindBlobs(ctx context.Context, n uint, lastChecksum string) ([]string, error) + MigrateBlob(ctx context.Context, checksum string, deleteOld bool) error +} + +type Vacuumer interface { + Vacuum(ctx context.Context) error +} + +type MigrateBlobsJob struct { + TxnManager txn.Manager + BlobStore BlobStoreMigrator + Vacuumer Vacuumer + DeleteOld bool +} + +func (j *MigrateBlobsJob) Execute(ctx context.Context, progress *job.Progress) { + var ( + count int + err error + ) + progress.ExecuteTask("Counting blobs", func() { + count, err = j.countBlobs(ctx) + progress.SetTotal(count) + }) + + if err != nil { + logger.Errorf("Error counting blobs: %s", err.Error()) + return + } + + if count == 0 { + logger.Infof("No blobs to migrate") + return + } + + logger.Infof("Migrating %d blobs", count) + + progress.ExecuteTask(fmt.Sprintf("Migrating %d blobs", count), func() { + err = j.migrateBlobs(ctx, progress) + }) + + if job.IsCancelled(ctx) { + logger.Info("Cancelled migrating blobs") + return + } + + if err != nil { + logger.Errorf("Error migrating blobs: %v", err) + return + } + + // run a vacuum to reclaim space + progress.ExecuteTask("Vacuuming database", func() { + err = j.Vacuumer.Vacuum(ctx) + if err != nil { + logger.Errorf("Error vacuuming database: %v", err) + } + }) + + logger.Infof("Finished migrating blobs") +} + +func (j *MigrateBlobsJob) countBlobs(ctx context.Context) (int, error) { + var count int + if err := txn.WithReadTxn(ctx, j.TxnManager, func(ctx context.Context) error { + var err error + count, err = j.BlobStore.Count(ctx) + return err + }); err != nil { + return 0, err + } + + return count, nil +} + +func (j *MigrateBlobsJob) migrateBlobs(ctx context.Context, progress *job.Progress) error { + lastChecksum := "" + batch, err := j.getBatch(ctx, lastChecksum) + + for len(batch) > 0 && err == nil && ctx.Err() == nil { + for _, checksum := range batch { + if ctx.Err() != nil { + return nil + } + + lastChecksum = checksum + + progress.ExecuteTask("Migrating blob "+checksum, func() { + defer progress.Increment() + + if err := txn.WithTxn(ctx, j.TxnManager, func(ctx context.Context) error { + return j.BlobStore.MigrateBlob(ctx, checksum, j.DeleteOld) + }); err != nil { + logger.Errorf("Error migrating blob %s: %v", checksum, err) + } + }) + } + + batch, err = j.getBatch(ctx, lastChecksum) + } + + return err +} + +func (j *MigrateBlobsJob) getBatch(ctx context.Context, lastChecksum string) ([]string, error) { + const batchSize = 1000 + + var batch []string + err := txn.WithReadTxn(ctx, j.TxnManager, func(ctx context.Context) error { + var err error + batch, err = j.BlobStore.FindBlobs(ctx, batchSize, lastChecksum) + return err + }) + + return batch, err +} diff --git a/internal/manager/task/migrate_scene_screenshots.go b/internal/manager/task/migrate_scene_screenshots.go new file mode 100644 index 000000000..bd758391f --- /dev/null +++ b/internal/manager/task/migrate_scene_screenshots.go @@ -0,0 +1,135 @@ +package task + +import ( + "context" + "errors" + "io" + "os" + "path/filepath" + "strings" + + "github.com/stashapp/stash/pkg/job" + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/scene" + "github.com/stashapp/stash/pkg/txn" +) + +type MigrateSceneScreenshotsJob struct { + ScreenshotsPath string + Input scene.MigrateSceneScreenshotsInput + SceneRepo scene.HashFinderCoverUpdater + TxnManager txn.Manager +} + +func (j *MigrateSceneScreenshotsJob) Execute(ctx context.Context, progress *job.Progress) { + var err error + progress.ExecuteTask("Counting files", func() { + var count int + count, err = j.countFiles(ctx) + progress.SetTotal(count) + }) + + if err != nil { + logger.Errorf("Error counting files: %s", err.Error()) + return + } + + progress.ExecuteTask("Migrating files", func() { + err = j.migrateFiles(ctx, progress) + }) + + if job.IsCancelled(ctx) { + logger.Info("Cancelled migrating scene screenshots") + return + } + + if err != nil { + logger.Errorf("Error migrating scene screenshots: %v", err) + return + } + + logger.Infof("Finished migrating scene screenshots") +} + +func (j *MigrateSceneScreenshotsJob) countFiles(ctx context.Context) (int, error) { + f, err := os.Open(j.ScreenshotsPath) + if err != nil { + return 0, err + } + defer f.Close() + + const batchSize = 1000 + ret := 0 + files, err := f.ReadDir(batchSize) + for err == nil && ctx.Err() == nil { + ret += len(files) + + files, err = f.ReadDir(batchSize) + } + + if errors.Is(err, io.EOF) { + // end of directory + return ret, nil + } + + return 0, err +} + +func (j *MigrateSceneScreenshotsJob) migrateFiles(ctx context.Context, progress *job.Progress) error { + f, err := os.Open(j.ScreenshotsPath) + if err != nil { + return err + } + defer f.Close() + + m := scene.ScreenshotMigrator{ + Options: j.Input, + SceneUpdater: j.SceneRepo, + TxnManager: j.TxnManager, + } + + const batchSize = 1000 + files, err := f.ReadDir(batchSize) + for err == nil && ctx.Err() == nil { + for _, f := range files { + if ctx.Err() != nil { + return nil + } + + progress.ExecuteTask("Migrating file "+f.Name(), func() { + defer progress.Increment() + + path := filepath.Join(j.ScreenshotsPath, f.Name()) + + // sanity check - only process files + if f.IsDir() { + logger.Warnf("Skipping directory %s", path) + return + } + + // ignore non-jpg files + if !strings.HasSuffix(f.Name(), ".jpg") { + return + } + + // ignore .thumb files + if strings.HasSuffix(f.Name(), ".thumb.jpg") { + return + } + + if err := m.MigrateScreenshots(ctx, path); err != nil { + logger.Errorf("Error migrating screenshots for %s: %v", path, err) + } + }) + } + + files, err = f.ReadDir(batchSize) + } + + if errors.Is(err, io.EOF) { + // end of directory + return nil + } + + return err +} diff --git a/internal/manager/task_autotag.go b/internal/manager/task_autotag.go index 16df5d240..0dfe59dd3 100644 --- a/internal/manager/task_autotag.go +++ b/internal/manager/task_autotag.go @@ -142,22 +142,26 @@ func (j *autoTagJob) autoTagPerformers(ctx context.Context, progress *job.Progre PerPage: &perPage, }) if err != nil { - return fmt.Errorf("error querying performers: %v", err) + return fmt.Errorf("error querying performers: %w", err) } } else { performerIdInt, err := strconv.Atoi(performerId) if err != nil { - return fmt.Errorf("error parsing performer id %s: %s", performerId, err.Error()) + return fmt.Errorf("parsing performer id %s: %w", performerId, err) } performer, err := performerQuery.Find(ctx, performerIdInt) if err != nil { - return fmt.Errorf("error finding performer id %s: %s", performerId, err.Error()) + return fmt.Errorf("finding performer id %s: %w", performerId, err) } if performer == nil { return fmt.Errorf("performer with id %s not found", performerId) } + + if err := performer.LoadAliases(ctx, j.txnManager.Performer); err != nil { + return fmt.Errorf("loading aliases for performer %d: %w", performer.ID, err) + } performers = append(performers, performer) } diff --git a/internal/manager/task_clean.go b/internal/manager/task_clean.go index f6d7304f8..b90f11be8 100644 --- a/internal/manager/task_clean.go +++ b/internal/manager/task_clean.go @@ -164,9 +164,9 @@ func (f *cleanFilter) Accept(ctx context.Context, path string, info fs.FileInfo) if info.IsDir() { fileOrFolder = "Folder" - stash = getStashFromDirPath(f.stashPaths, path) + stash = f.stashPaths.GetStashFromDirPath(path) } else { - stash = getStashFromPath(f.stashPaths, path) + stash = f.stashPaths.GetStashFromPath(path) } if stash == nil { @@ -449,21 +449,3 @@ func (h *cleanHandler) handleRelatedImages(ctx context.Context, fileDeleter *fil return nil } - -func getStashFromPath(stashes []*config.StashConfig, pathToCheck string) *config.StashConfig { - for _, f := range stashes { - if fsutil.IsPathInDir(f.Path, filepath.Dir(pathToCheck)) { - return f - } - } - return nil -} - -func getStashFromDirPath(stashes []*config.StashConfig, pathToCheck string) *config.StashConfig { - for _, f := range stashes { - if fsutil.IsPathInDir(f.Path, pathToCheck) { - return f - } - } - return nil -} diff --git a/internal/manager/task_export.go b/internal/manager/task_export.go index d75ad2eed..4c4a2dd05 100644 --- a/internal/manager/task_export.go +++ b/internal/manager/task_export.go @@ -765,6 +765,7 @@ func exportGallery(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *mode studioReader := repo.Studio performerReader := repo.Performer tagReader := repo.Tag + galleryChapterReader := repo.GalleryChapter for g := range jobChan { if err := g.LoadFiles(ctx, repo.Gallery); err != nil { @@ -821,6 +822,12 @@ func exportGallery(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *mode continue } + newGalleryJSON.Chapters, err = gallery.GetGalleryChaptersJSON(ctx, galleryChapterReader, g) + if err != nil { + logger.Errorf("[galleries] <%s> error getting gallery chapters JSON: %s", galleryHash, err.Error()) + continue + } + newGalleryJSON.Tags = tag.GetNames(tags) if t.includeDependencies { @@ -899,13 +906,13 @@ func (t *ExportTask) exportPerformer(ctx context.Context, wg *sync.WaitGroup, jo newPerformerJSON, err := performer.ToJSON(ctx, performerReader, p) if err != nil { - logger.Errorf("[performers] <%s> error getting performer JSON: %s", p.Checksum, err.Error()) + logger.Errorf("[performers] <%s> error getting performer JSON: %s", p.Name, err.Error()) continue } tags, err := repo.Tag.FindByPerformerID(ctx, p.ID) if err != nil { - logger.Errorf("[performers] <%s> error getting performer tags: %s", p.Checksum, err.Error()) + logger.Errorf("[performers] <%s> error getting performer tags: %s", p.Name, err.Error()) continue } @@ -918,7 +925,7 @@ func (t *ExportTask) exportPerformer(ctx context.Context, wg *sync.WaitGroup, jo fn := newPerformerJSON.Filename() if err := t.json.savePerformer(fn, newPerformerJSON); err != nil { - logger.Errorf("[performers] <%s> failed to save json: %s", p.Checksum, err.Error()) + logger.Errorf("[performers] <%s> failed to save json: %s", p.Name, err.Error()) } } } diff --git a/internal/manager/task_generate.go b/internal/manager/task_generate.go index 088a9ea3c..c3b4f16f7 100644 --- a/internal/manager/task_generate.go +++ b/internal/manager/task_generate.go @@ -13,28 +13,28 @@ import ( "github.com/stashapp/stash/pkg/scene" "github.com/stashapp/stash/pkg/scene/generate" "github.com/stashapp/stash/pkg/sliceutil/stringslice" - "github.com/stashapp/stash/pkg/utils" ) type GenerateMetadataInput struct { - Sprites *bool `json:"sprites"` - Previews *bool `json:"previews"` - ImagePreviews *bool `json:"imagePreviews"` + Covers bool `json:"covers"` + Sprites bool `json:"sprites"` + Previews bool `json:"previews"` + ImagePreviews bool `json:"imagePreviews"` PreviewOptions *GeneratePreviewOptionsInput `json:"previewOptions"` - Markers *bool `json:"markers"` - MarkerImagePreviews *bool `json:"markerImagePreviews"` - MarkerScreenshots *bool `json:"markerScreenshots"` - Transcodes *bool `json:"transcodes"` + Markers bool `json:"markers"` + MarkerImagePreviews bool `json:"markerImagePreviews"` + MarkerScreenshots bool `json:"markerScreenshots"` + Transcodes bool `json:"transcodes"` // Generate transcodes even if not required - ForceTranscodes *bool `json:"forceTranscodes"` - Phashes *bool `json:"phashes"` - InteractiveHeatmapsSpeeds *bool `json:"interactiveHeatmapsSpeeds"` + ForceTranscodes bool `json:"forceTranscodes"` + Phashes bool `json:"phashes"` + InteractiveHeatmapsSpeeds bool `json:"interactiveHeatmapsSpeeds"` // scene ids to generate for SceneIDs []string `json:"sceneIDs"` // marker ids to generate for MarkerIDs []string `json:"markerIDs"` // overwrite existing media - Overwrite *bool `json:"overwrite"` + Overwrite bool `json:"overwrite"` } type GeneratePreviewOptionsInput struct { @@ -61,6 +61,7 @@ type GenerateJob struct { } type totalsGenerate struct { + covers int64 sprites int64 previews int64 imagePreviews int64 @@ -77,9 +78,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) { var err error var markers []*models.SceneMarker - if j.input.Overwrite != nil { - j.overwrite = *j.input.Overwrite - } + j.overwrite = j.input.Overwrite j.fileNamingAlgo = config.GetInstance().GetVideoFileNamingAlgorithm() config := config.GetInstance() @@ -102,11 +101,12 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) { } g := &generate.Generator{ - Encoder: instance.FFMPEG, - LockManager: instance.ReadLockManager, - MarkerPaths: instance.Paths.SceneMarkers, - ScenePaths: instance.Paths.Scene, - Overwrite: j.overwrite, + Encoder: instance.FFMPEG, + FFMpegConfig: instance.Config, + LockManager: instance.ReadLockManager, + MarkerPaths: instance.Paths.SceneMarkers, + ScenePaths: instance.Paths.Scene, + Overwrite: j.overwrite, } if err := j.txnManager.WithReadTxn(ctx, func(ctx context.Context) error { @@ -142,7 +142,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) { return } - logger.Infof("Generating %d sprites %d previews %d image previews %d markers %d transcodes %d phashes %d heatmaps & speeds", totals.sprites, totals.previews, totals.imagePreviews, totals.markers, totals.transcodes, totals.phashes, totals.interactiveHeatmapSpeeds) + logger.Infof("Generating %d covers %d sprites %d previews %d image previews %d markers %d transcodes %d phashes %d heatmaps & speeds", totals.covers, totals.sprites, totals.previews, totals.imagePreviews, totals.markers, totals.transcodes, totals.phashes, totals.interactiveHeatmapSpeeds) progress.SetTotal(int(totals.tasks)) }() @@ -265,7 +265,20 @@ func getGeneratePreviewOptions(optionsInput GeneratePreviewOptionsInput) generat } func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator, scene *models.Scene, queue chan<- Task, totals *totalsGenerate) { - if utils.IsTrue(j.input.Sprites) { + if j.input.Covers { + task := &GenerateCoverTask{ + txnManager: j.txnManager, + Scene: *scene, + } + + if j.overwrite || task.required(ctx) { + totals.covers++ + totals.tasks++ + queue <- task + } + } + + if j.input.Sprites { task := &GenerateSpriteTask{ Scene: *scene, Overwrite: j.overwrite, @@ -285,10 +298,10 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator, } options := getGeneratePreviewOptions(*generatePreviewOptions) - if utils.IsTrue(j.input.Previews) { + if j.input.Previews { task := &GeneratePreviewTask{ Scene: *scene, - ImagePreview: utils.IsTrue(j.input.ImagePreviews), + ImagePreview: j.input.ImagePreviews, Options: options, Overwrite: j.overwrite, fileNamingAlgorithm: j.fileNamingAlgo, @@ -296,14 +309,13 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator, } if task.required() { - sceneHash := scene.GetHash(task.fileNamingAlgorithm) addTask := false - if j.overwrite || !task.doesVideoPreviewExist(sceneHash) { + if j.overwrite || !task.doesVideoPreviewExist() { totals.previews++ addTask = true } - if utils.IsTrue(j.input.ImagePreviews) && (j.overwrite || !task.doesImagePreviewExist(sceneHash)) { + if j.input.ImagePreviews && (j.overwrite || !task.doesImagePreviewExist()) { totals.imagePreviews++ addTask = true } @@ -315,14 +327,14 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator, } } - if utils.IsTrue(j.input.Markers) { + if j.input.Markers { task := &GenerateMarkersTask{ TxnManager: j.txnManager, Scene: scene, Overwrite: j.overwrite, fileNamingAlgorithm: j.fileNamingAlgo, - ImagePreview: utils.IsTrue(j.input.MarkerImagePreviews), - Screenshot: utils.IsTrue(j.input.MarkerScreenshots), + ImagePreview: j.input.MarkerImagePreviews, + Screenshot: j.input.MarkerScreenshots, generator: g, } @@ -336,8 +348,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator, } } - if utils.IsTrue(j.input.Transcodes) { - forceTranscode := utils.IsTrue(j.input.ForceTranscodes) + if j.input.Transcodes { + forceTranscode := j.input.ForceTranscodes task := &GenerateTranscodeTask{ Scene: *scene, Overwrite: j.overwrite, @@ -352,7 +364,7 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator, } } - if utils.IsTrue(j.input.Phashes) { + if j.input.Phashes { // generate for all files in scene for _, f := range scene.Files.List() { task := &GeneratePhashTask{ @@ -371,7 +383,7 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator, } } - if utils.IsTrue(j.input.InteractiveHeatmapsSpeeds) { + if j.input.InteractiveHeatmapsSpeeds { task := &GenerateInteractiveHeatmapSpeedTask{ Scene: *scene, Overwrite: j.overwrite, diff --git a/internal/manager/task_generate_interactive_heatmap_speed.go b/internal/manager/task_generate_interactive_heatmap_speed.go index 414584a77..564004b8e 100644 --- a/internal/manager/task_generate_interactive_heatmap_speed.go +++ b/internal/manager/task_generate_interactive_heatmap_speed.go @@ -29,10 +29,11 @@ func (t *GenerateInteractiveHeatmapSpeedTask) Start(ctx context.Context) { videoChecksum := t.Scene.GetHash(t.fileNamingAlgorithm) funscriptPath := video.GetFunscriptPath(t.Scene.Path) heatmapPath := instance.Paths.Scene.GetInteractiveHeatmapPath(videoChecksum) + drawRange := instance.Config.GetDrawFunscriptHeatmapRange() - generator := NewInteractiveHeatmapSpeedGenerator(funscriptPath, heatmapPath, t.Scene.Files.Primary().Duration) + generator := NewInteractiveHeatmapSpeedGenerator(drawRange) - err := generator.Generate() + err := generator.Generate(funscriptPath, heatmapPath, t.Scene.Files.Primary().Duration) if err != nil { logger.Errorf("error generating heatmap: %s", err.Error()) diff --git a/internal/manager/task_generate_preview.go b/internal/manager/task_generate_preview.go index 37ec51ec2..c81909417 100644 --- a/internal/manager/task_generate_preview.go +++ b/internal/manager/task_generate_preview.go @@ -20,6 +20,9 @@ type GeneratePreviewTask struct { fileNamingAlgorithm models.HashAlgorithm generator *generate.Generator + + videoPreviewExists *bool + imagePreviewExists *bool } func (t *GeneratePreviewTask) GetDescription() string { @@ -31,22 +34,24 @@ func (t *GeneratePreviewTask) Start(ctx context.Context) { return } - ffprobe := instance.FFProbe - videoFile, err := ffprobe.NewVideoFile(t.Scene.Path) - if err != nil { - logger.Errorf("error reading video file: %v", err) - return - } - videoChecksum := t.Scene.GetHash(t.fileNamingAlgorithm) - if err := t.generateVideo(videoChecksum, videoFile.VideoStreamDuration); err != nil { - logger.Errorf("error generating preview: %v", err) - logErrorOutput(err) - return + if t.Overwrite || !t.doesVideoPreviewExist() { + ffprobe := instance.FFProbe + videoFile, err := ffprobe.NewVideoFile(t.Scene.Path) + if err != nil { + logger.Errorf("error reading video file: %v", err) + return + } + + if err := t.generateVideo(videoChecksum, videoFile.VideoStreamDuration, videoFile.FrameRate); err != nil { + logger.Errorf("error generating preview: %v", err) + logErrorOutput(err) + return + } } - if t.ImagePreview { + if t.ImagePreview && (t.Overwrite || !t.doesImagePreviewExist()) { if err := t.generateWebp(videoChecksum); err != nil { logger.Errorf("error generating preview webp: %v", err) logErrorOutput(err) @@ -54,12 +59,18 @@ func (t *GeneratePreviewTask) Start(ctx context.Context) { } } -func (t GeneratePreviewTask) generateVideo(videoChecksum string, videoDuration float64) error { +func (t GeneratePreviewTask) generateVideo(videoChecksum string, videoDuration float64, videoFrameRate float64) error { videoFilename := t.Scene.Path + useVsync2 := false - if err := t.generator.PreviewVideo(context.TODO(), videoFilename, videoDuration, videoChecksum, t.Options, true); err != nil { + if videoFrameRate <= 0.01 { + logger.Errorf("[generator] Video framerate very low/high (%f) most likely vfr so using -vsync 2", videoFrameRate) + useVsync2 = true + } + + if err := t.generator.PreviewVideo(context.TODO(), videoFilename, videoDuration, videoChecksum, t.Options, false, useVsync2); err != nil { logger.Warnf("[generator] failed generating scene preview, trying fallback") - if err := t.generator.PreviewVideo(context.TODO(), videoFilename, videoDuration, videoChecksum, t.Options, true); err != nil { + if err := t.generator.PreviewVideo(context.TODO(), videoFilename, videoDuration, videoChecksum, t.Options, true, useVsync2); err != nil { return err } } @@ -77,26 +88,39 @@ func (t GeneratePreviewTask) required() bool { return false } - sceneHash := t.Scene.GetHash(t.fileNamingAlgorithm) - videoExists := t.doesVideoPreviewExist(sceneHash) - imageExists := !t.ImagePreview || t.doesImagePreviewExist(sceneHash) + if t.Overwrite { + return true + } + + videoExists := t.doesVideoPreviewExist() + imageExists := !t.ImagePreview || t.doesImagePreviewExist() return !imageExists || !videoExists } -func (t *GeneratePreviewTask) doesVideoPreviewExist(sceneChecksum string) bool { +func (t *GeneratePreviewTask) doesVideoPreviewExist() bool { + sceneChecksum := t.Scene.GetHash(t.fileNamingAlgorithm) if sceneChecksum == "" { return false } - videoExists, _ := fsutil.FileExists(instance.Paths.Scene.GetVideoPreviewPath(sceneChecksum)) - return videoExists + if t.videoPreviewExists == nil { + videoExists, _ := fsutil.FileExists(instance.Paths.Scene.GetVideoPreviewPath(sceneChecksum)) + t.videoPreviewExists = &videoExists + } + + return *t.videoPreviewExists } -func (t *GeneratePreviewTask) doesImagePreviewExist(sceneChecksum string) bool { +func (t *GeneratePreviewTask) doesImagePreviewExist() bool { + sceneChecksum := t.Scene.GetHash(t.fileNamingAlgorithm) if sceneChecksum == "" { return false } - imageExists, _ := fsutil.FileExists(instance.Paths.Scene.GetWebpPreviewPath(sceneChecksum)) - return imageExists + if t.imagePreviewExists == nil { + imageExists, _ := fsutil.FileExists(instance.Paths.Scene.GetWebpPreviewPath(sceneChecksum)) + t.imagePreviewExists = &imageExists + } + + return *t.imagePreviewExists } diff --git a/internal/manager/task_generate_screenshot.go b/internal/manager/task_generate_screenshot.go index c235d00b1..5d32f2762 100644 --- a/internal/manager/task_generate_screenshot.go +++ b/internal/manager/task_generate_screenshot.go @@ -3,25 +3,39 @@ package manager import ( "context" "fmt" - "io" - "os" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/scene" "github.com/stashapp/stash/pkg/scene/generate" ) -type GenerateScreenshotTask struct { - Scene models.Scene - ScreenshotAt *float64 - fileNamingAlgorithm models.HashAlgorithm - txnManager Repository +type GenerateCoverTask struct { + Scene models.Scene + ScreenshotAt *float64 + txnManager Repository + Overwrite bool } -func (t *GenerateScreenshotTask) Start(ctx context.Context) { +func (t *GenerateCoverTask) GetDescription() string { + return fmt.Sprintf("Generating cover for %s", t.Scene.GetTitle()) +} + +func (t *GenerateCoverTask) Start(ctx context.Context) { scenePath := t.Scene.Path + var required bool + if err := t.txnManager.WithReadTxn(ctx, func(ctx context.Context) error { + // don't generate the screenshot if it already exists + required = t.required(ctx) + return t.Scene.LoadPrimaryFile(ctx, t.txnManager.File) + }); err != nil { + logger.Error(err) + } + + if !required { + return + } + videoFile := t.Scene.Files.Primary() if videoFile == nil { return @@ -34,51 +48,32 @@ func (t *GenerateScreenshotTask) Start(ctx context.Context) { at = *t.ScreenshotAt } - checksum := t.Scene.GetHash(t.fileNamingAlgorithm) - normalPath := instance.Paths.Scene.GetScreenshotPath(checksum) - // we'll generate the screenshot, grab the generated data and set it - // in the database. We'll use SetSceneScreenshot to set the data - // which also generates the thumbnail + // in the database. logger.Debugf("Creating screenshot for %s", scenePath) g := generate.Generator{ - Encoder: instance.FFMPEG, - LockManager: instance.ReadLockManager, - ScenePaths: instance.Paths.Scene, - Overwrite: true, + Encoder: instance.FFMPEG, + FFMpegConfig: instance.Config, + LockManager: instance.ReadLockManager, + ScenePaths: instance.Paths.Scene, + Overwrite: true, } - if err := g.Screenshot(context.TODO(), videoFile.Path, checksum, videoFile.Width, videoFile.Duration, generate.ScreenshotOptions{ + coverImageData, err := g.Screenshot(context.TODO(), videoFile.Path, videoFile.Width, videoFile.Duration, generate.ScreenshotOptions{ At: &at, - }); err != nil { + }) + if err != nil { logger.Errorf("Error generating screenshot: %v", err) logErrorOutput(err) return } - f, err := os.Open(normalPath) - if err != nil { - logger.Errorf("Error reading screenshot: %s", err.Error()) - return - } - defer f.Close() - - coverImageData, err := io.ReadAll(f) - if err != nil { - logger.Errorf("Error reading screenshot: %s", err.Error()) - return - } - if err := t.txnManager.WithTxn(ctx, func(ctx context.Context) error { qb := t.txnManager.Scene updatedScene := models.NewScenePartial() - if err := scene.SetScreenshot(instance.Paths, checksum, coverImageData); err != nil { - return fmt.Errorf("error writing screenshot: %v", err) - } - // update the scene cover table if err := qb.UpdateCover(ctx, t.Scene.ID, coverImageData); err != nil { return fmt.Errorf("error setting screenshot: %v", err) @@ -95,3 +90,19 @@ func (t *GenerateScreenshotTask) Start(ctx context.Context) { logger.Error(err.Error()) } } + +// required returns true if the sprite needs to be generated +func (t GenerateCoverTask) required(ctx context.Context) bool { + if t.Overwrite { + return true + } + + // if the scene has a cover, then we don't need to generate it + hasCover, err := t.txnManager.Scene.HasCover(ctx, t.Scene.ID) + if err != nil { + logger.Errorf("Error getting cover: %v", err) + return false + } + + return !hasCover +} diff --git a/internal/manager/task_identify.go b/internal/manager/task_identify.go index 078e541ee..955dcb2b3 100644 --- a/internal/manager/task_identify.go +++ b/internal/manager/task_identify.go @@ -138,12 +138,8 @@ func (j *IdentifyJob) identifyScene(ctx context.Context, s *models.Scene, source PerformerCreator: instance.Repository.Performer, TagCreator: instance.Repository.Tag, - DefaultOptions: j.input.Options, - Sources: sources, - ScreenshotSetter: &scene.PathsCoverSetter{ - Paths: instance.Paths, - FileNamingAlgorithm: instance.Config.GetVideoFileNamingAlgorithm(), - }, + DefaultOptions: j.input.Options, + Sources: sources, SceneUpdatePostHookExecutor: j.postHookExecutor, } diff --git a/internal/manager/task_import.go b/internal/manager/task_import.go index 33d862c2e..7cefc8af0 100644 --- a/internal/manager/task_import.go +++ b/internal/manager/task_import.go @@ -487,6 +487,7 @@ func (t *ImportTask) ImportGalleries(ctx context.Context) { tagWriter := r.Tag performerWriter := r.Performer studioWriter := r.Studio + chapterWriter := r.GalleryChapter galleryImporter := &gallery.Importer{ ReaderWriter: readerWriter, @@ -499,7 +500,25 @@ func (t *ImportTask) ImportGalleries(ctx context.Context) { MissingRefBehaviour: t.MissingRefBehaviour, } - return performImport(ctx, galleryImporter, t.DuplicateBehaviour) + if err := performImport(ctx, galleryImporter, t.DuplicateBehaviour); err != nil { + return err + } + + // import the gallery chapters + for _, m := range galleryJSON.Chapters { + chapterImporter := &gallery.ChapterImporter{ + GalleryID: galleryImporter.ID, + Input: m, + MissingRefBehaviour: t.MissingRefBehaviour, + ReaderWriter: chapterWriter, + } + + if err := performImport(ctx, chapterImporter, t.DuplicateBehaviour); err != nil { + return err + } + } + + return nil }); err != nil { logger.Errorf("[galleries] <%s> import failed to commit: %s", fi.Name(), err.Error()) continue diff --git a/internal/manager/task_scan.go b/internal/manager/task_scan.go index 5a2a07653..fa31af610 100644 --- a/internal/manager/task_scan.go +++ b/internal/manager/task_scan.go @@ -21,6 +21,7 @@ import ( "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/scene" "github.com/stashapp/stash/pkg/scene/generate" + "github.com/stashapp/stash/pkg/txn" ) type scanner interface { @@ -111,9 +112,11 @@ type sceneFinder interface { // handlerRequiredFilter returns true if a File's handler needs to be executed despite the file not being updated. type handlerRequiredFilter struct { extensionConfig - SceneFinder sceneFinder - ImageFinder fileCounter - GalleryFinder galleryFinder + txnManager txn.Manager + SceneFinder sceneFinder + ImageFinder fileCounter + GalleryFinder galleryFinder + CaptionUpdater video.CaptionUpdater FolderCache *lru.LRU @@ -126,9 +129,11 @@ func newHandlerRequiredFilter(c *config.Instance) *handlerRequiredFilter { return &handlerRequiredFilter{ extensionConfig: newExtensionConfig(c), + txnManager: db, SceneFinder: db.Scene, ImageFinder: db.Image, GalleryFinder: db.Gallery, + CaptionUpdater: db.File, FolderCache: lru.New(processes * 2), videoFileNamingAlgorithm: c.GetVideoFileNamingAlgorithm(), } @@ -189,20 +194,29 @@ func (f *handlerRequiredFilter) Accept(ctx context.Context, ff file.File) bool { } if isVideoFile { - // check if the screenshot file exists - hash := scene.GetHash(ff, f.videoFileNamingAlgorithm) - ssPath := instance.Paths.Scene.GetScreenshotPath(hash) - if exists, _ := fsutil.FileExists(ssPath); !exists { - // if not, check if the file is a primary file for a scene - scenes, err := f.SceneFinder.FindByPrimaryFileID(ctx, ff.Base().ID) - if err != nil { - // just ignore - return false - } + // TODO - check if the cover exists + // hash := scene.GetHash(ff, f.videoFileNamingAlgorithm) + // ssPath := instance.Paths.Scene.GetScreenshotPath(hash) + // if exists, _ := fsutil.FileExists(ssPath); !exists { + // // if not, check if the file is a primary file for a scene + // scenes, err := f.SceneFinder.FindByPrimaryFileID(ctx, ff.Base().ID) + // if err != nil { + // // just ignore + // return false + // } - if len(scenes) > 0 { - // if it is, then it needs to be re-generated - return true + // if len(scenes) > 0 { + // // if it is, then it needs to be re-generated + // return true + // } + // } + + // clean captions - scene handler handles this as well, but + // unchanged files aren't processed by the scene handler + videoFile, _ := ff.(*file.VideoFile) + if videoFile != nil { + if err := video.CleanCaptions(ctx, videoFile, f.txnManager, f.CaptionUpdater); err != nil { + logger.Errorf("Error cleaning captions: %v", err) } } } @@ -212,7 +226,7 @@ func (f *handlerRequiredFilter) Accept(ctx context.Context, ff file.File) bool { type scanFilter struct { extensionConfig - stashPaths []*config.StashConfig + stashPaths config.StashConfigs generatedPath string videoExcludeRegex []*regexp.Regexp imageExcludeRegex []*regexp.Regexp @@ -254,6 +268,7 @@ func (f *scanFilter) Accept(ctx context.Context, path string, info fs.FileInfo) } if !info.IsDir() && !isVideoFile && !isImageFile && !isZipFile { + logger.Debugf("Skipping %s as it does not match any known file extensions", path) return false } @@ -263,9 +278,10 @@ func (f *scanFilter) Accept(ctx context.Context, path string, info fs.FileInfo) return false } - s := getStashFromDirPath(f.stashPaths, path) + s := f.stashPaths.GetStashFromDirPath(path) if s == nil { + logger.Debugf("Skipping %s as it is not in the stash library", path) return false } @@ -273,12 +289,15 @@ func (f *scanFilter) Accept(ctx context.Context, path string, info fs.FileInfo) // add a trailing separator so that it correctly matches against patterns like path/.* pathExcludeTest := path + string(filepath.Separator) if (s.ExcludeVideo || matchFileRegex(pathExcludeTest, f.videoExcludeRegex)) && (s.ExcludeImage || matchFileRegex(pathExcludeTest, f.imageExcludeRegex)) { + logger.Debugf("Skipping directory %s as it matches video and image exclusion patterns", path) return false } if isVideoFile && (s.ExcludeVideo || matchFileRegex(path, f.videoExcludeRegex)) { + logger.Debugf("Skipping %s as it matches video exclusion patterns", path) return false - } else if (isImageFile || isZipFile) && s.ExcludeImage || matchFileRegex(path, f.imageExcludeRegex) { + } else if (isImageFile || isZipFile) && (s.ExcludeImage || matchFileRegex(path, f.imageExcludeRegex)) { + logger.Debugf("Skipping %s as it matches image exclusion patterns", path) return false } @@ -329,7 +348,7 @@ func getScanHandlers(options ScanMetadataInput, taskQueue *job.TaskQueue, progre Handler: &scene.ScanHandler{ CreatorUpdater: db.Scene, PluginCache: pluginCache, - CoverGenerator: &coverGenerator{}, + CaptionUpdater: db.File, ScanGenerator: &sceneGenerators{ input: options, taskQueue: taskQueue, @@ -390,10 +409,11 @@ func (g *sceneGenerators) Generate(ctx context.Context, s *models.Scene, f *file path := f.Path config := instance.Config fileNamingAlgorithm := config.GetVideoFileNamingAlgorithm() + sequentialScanning := config.GetSequentialScanning() if t.ScanGenerateSprites { progress.AddTotal(1) - g.taskQueue.Add(fmt.Sprintf("Generating sprites for %s", path), func(ctx context.Context) { + spriteFn := func(ctx context.Context) { taskSprite := GenerateSpriteTask{ Scene: *s, Overwrite: overwrite, @@ -401,12 +421,18 @@ func (g *sceneGenerators) Generate(ctx context.Context, s *models.Scene, f *file } taskSprite.Start(ctx) progress.Increment() - }) + } + + if sequentialScanning { + spriteFn(ctx) + } else { + g.taskQueue.Add(fmt.Sprintf("Generating sprites for %s", path), spriteFn) + } } if t.ScanGeneratePhashes { progress.AddTotal(1) - g.taskQueue.Add(fmt.Sprintf("Generating phash for %s", path), func(ctx context.Context) { + phashFn := func(ctx context.Context) { taskPhash := GeneratePhashTask{ File: f, fileNamingAlgorithm: fileNamingAlgorithm, @@ -416,20 +442,27 @@ func (g *sceneGenerators) Generate(ctx context.Context, s *models.Scene, f *file } taskPhash.Start(ctx) progress.Increment() - }) + } + + if sequentialScanning { + phashFn(ctx) + } else { + g.taskQueue.Add(fmt.Sprintf("Generating phash for %s", path), phashFn) + } } if t.ScanGeneratePreviews { progress.AddTotal(1) - g.taskQueue.Add(fmt.Sprintf("Generating preview for %s", path), func(ctx context.Context) { + previewsFn := func(ctx context.Context) { options := getGeneratePreviewOptions(GeneratePreviewOptionsInput{}) g := &generate.Generator{ - Encoder: instance.FFMPEG, - LockManager: instance.ReadLockManager, - MarkerPaths: instance.Paths.SceneMarkers, - ScenePaths: instance.Paths.Scene, - Overwrite: overwrite, + Encoder: instance.FFMPEG, + FFMpegConfig: instance.Config, + LockManager: instance.ReadLockManager, + MarkerPaths: instance.Paths.SceneMarkers, + ScenePaths: instance.Paths.Scene, + Overwrite: overwrite, } taskPreview := GeneratePreviewTask{ @@ -442,6 +475,24 @@ func (g *sceneGenerators) Generate(ctx context.Context, s *models.Scene, f *file } taskPreview.Start(ctx) progress.Increment() + } + + if sequentialScanning { + previewsFn(ctx) + } else { + g.taskQueue.Add(fmt.Sprintf("Generating preview for %s", path), previewsFn) + } + } + + if t.ScanGenerateCovers { + progress.AddTotal(1) + g.taskQueue.Add(fmt.Sprintf("Generating cover for %s", path), func(ctx context.Context) { + taskCover := GenerateCoverTask{ + Scene: *s, + txnManager: instance.Repository, + } + taskCover.Start(ctx) + progress.Increment() }) } diff --git a/internal/manager/task_stash_box_tag.go b/internal/manager/task_stash_box_tag.go index a9f7fd4ad..886da242f 100644 --- a/internal/manager/task_stash_box_tag.go +++ b/internal/manager/task_stash_box_tag.go @@ -6,10 +6,10 @@ import ( "strconv" "time" - "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/scraper/stashbox" + "github.com/stashapp/stash/pkg/sliceutil/stringslice" "github.com/stashapp/stash/pkg/txn" "github.com/stashapp/stash/pkg/utils" ) @@ -50,17 +50,10 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { if t.refresh { var performerID string - txnErr := txn.WithReadTxn(ctx, instance.Repository, func(ctx context.Context) error { - stashids, _ := instance.Repository.Performer.GetStashIDs(ctx, t.performer.ID) - for _, id := range stashids { - if id.Endpoint == t.box.Endpoint { - performerID = id.StashID - } + for _, id := range t.performer.StashIDs.List() { + if id.Endpoint == t.box.Endpoint { + performerID = id.StashID } - return nil - }) - if txnErr != nil { - logger.Warnf("error while executing read transaction: %v", err) } if performerID != "" { performer, err = client.FindStashBoxPerformerByID(ctx, performerID) @@ -87,93 +80,21 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { if performer != nil { if t.performer != nil { - partial := models.NewPerformerPartial() - - if performer.Aliases != nil && !excluded["aliases"] { - partial.Aliases = models.NewOptionalString(*performer.Aliases) - } - if performer.Birthdate != nil && *performer.Birthdate != "" && !excluded["birthdate"] { - value := getDate(performer.Birthdate) - partial.Birthdate = models.NewOptionalDate(*value) - } - if performer.CareerLength != nil && !excluded["career_length"] { - partial.CareerLength = models.NewOptionalString(*performer.CareerLength) - } - if performer.Country != nil && !excluded["country"] { - partial.Country = models.NewOptionalString(*performer.Country) - } - if performer.Ethnicity != nil && !excluded["ethnicity"] { - partial.Ethnicity = models.NewOptionalString(*performer.Ethnicity) - } - if performer.EyeColor != nil && !excluded["eye_color"] { - partial.EyeColor = models.NewOptionalString(*performer.EyeColor) - } - if performer.FakeTits != nil && !excluded["fake_tits"] { - partial.FakeTits = models.NewOptionalString(*performer.FakeTits) - } - if performer.Gender != nil && !excluded["gender"] { - partial.Gender = models.NewOptionalString(*performer.Gender) - } - if performer.Height != nil && !excluded["height"] { - h, err := strconv.Atoi(*performer.Height) - if err == nil { - partial.Height = models.NewOptionalInt(h) - } - } - if performer.Weight != nil && !excluded["weight"] { - w, err := strconv.Atoi(*performer.Weight) - if err == nil { - partial.Weight = models.NewOptionalInt(w) - } - } - if performer.Instagram != nil && !excluded["instagram"] { - partial.Instagram = models.NewOptionalString(*performer.Instagram) - } - if performer.Measurements != nil && !excluded["measurements"] { - partial.Measurements = models.NewOptionalString(*performer.Measurements) - } - if excluded["name"] && performer.Name != nil { - partial.Name = models.NewOptionalString(*performer.Name) - checksum := md5.FromString(*performer.Name) - partial.Checksum = models.NewOptionalString(checksum) - } - if performer.Piercings != nil && !excluded["piercings"] { - partial.Piercings = models.NewOptionalString(*performer.Piercings) - } - if performer.Tattoos != nil && !excluded["tattoos"] { - partial.Tattoos = models.NewOptionalString(*performer.Tattoos) - } - if performer.Twitter != nil && !excluded["twitter"] { - partial.Twitter = models.NewOptionalString(*performer.Twitter) - } - if performer.URL != nil && !excluded["url"] { - partial.URL = models.NewOptionalString(*performer.URL) - } + partial := t.getPartial(performer, excluded) txnErr := txn.WithTxn(ctx, instance.Repository, func(ctx context.Context) error { r := instance.Repository _, err := r.Performer.UpdatePartial(ctx, t.performer.ID, partial) - if !t.refresh { - err = r.Performer.UpdateStashIDs(ctx, t.performer.ID, []models.StashID{ - { - Endpoint: t.box.Endpoint, - StashID: *performer.RemoteSiteID, - }, - }) - if err != nil { - return err - } - } - if len(performer.Images) > 0 && !excluded["image"] { image, err := utils.ReadImageFromURL(ctx, performer.Images[0]) - if err != nil { - return err - } - err = r.Performer.UpdateImage(ctx, t.performer.ID, image) - if err != nil { - return err + if err == nil { + err = r.Performer.UpdateImage(ctx, t.performer.ID, image) + if err != nil { + return err + } + } else { + logger.Warnf("Failed to read performer image: %v", err) } } @@ -187,15 +108,20 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { return err }) if txnErr != nil { - logger.Warnf("failure to execute partial update of performer: %v", err) + logger.Warnf("failure to execute partial update of performer: %v", txnErr) } } else if t.name != nil && performer.Name != nil { currentTime := time.Now() + var aliases []string + if performer.Aliases != nil { + aliases = stringslice.FromString(*performer.Aliases, ",") + } else { + aliases = []string{} + } newPerformer := models.Performer{ - Aliases: getString(performer.Aliases), + Aliases: models.NewRelatedStrings(aliases), Birthdate: getDate(performer.Birthdate), CareerLength: getString(performer.CareerLength), - Checksum: md5.FromString(*performer.Name), Country: getString(performer.Country), CreatedAt: currentTime, Ethnicity: getString(performer.Ethnicity), @@ -211,21 +137,18 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { Tattoos: getString(performer.Tattoos), Twitter: getString(performer.Twitter), URL: getString(performer.URL), - UpdatedAt: currentTime, - } - err := txn.WithTxn(ctx, instance.Repository, func(ctx context.Context) error { - r := instance.Repository - err := r.Performer.Create(ctx, &newPerformer) - if err != nil { - return err - } - - err = r.Performer.UpdateStashIDs(ctx, newPerformer.ID, []models.StashID{ + StashIDs: models.NewRelatedStashIDs([]models.StashID{ { Endpoint: t.box.Endpoint, StashID: *performer.RemoteSiteID, }, - }) + }), + UpdatedAt: currentTime, + } + + err := txn.WithTxn(ctx, instance.Repository, func(ctx context.Context) error { + r := instance.Repository + err := r.Performer.Create(ctx, &newPerformer) if err != nil { return err } @@ -256,6 +179,87 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { } } +func (t *StashBoxPerformerTagTask) getPartial(performer *models.ScrapedPerformer, excluded map[string]bool) models.PerformerPartial { + partial := models.NewPerformerPartial() + + if performer.Aliases != nil && !excluded["aliases"] { + partial.Aliases = &models.UpdateStrings{ + Values: stringslice.FromString(*performer.Aliases, ","), + Mode: models.RelationshipUpdateModeSet, + } + } + if performer.Birthdate != nil && *performer.Birthdate != "" && !excluded["birthdate"] { + value := getDate(performer.Birthdate) + partial.Birthdate = models.NewOptionalDate(*value) + } + if performer.CareerLength != nil && !excluded["career_length"] { + partial.CareerLength = models.NewOptionalString(*performer.CareerLength) + } + if performer.Country != nil && !excluded["country"] { + partial.Country = models.NewOptionalString(*performer.Country) + } + if performer.Ethnicity != nil && !excluded["ethnicity"] { + partial.Ethnicity = models.NewOptionalString(*performer.Ethnicity) + } + if performer.EyeColor != nil && !excluded["eye_color"] { + partial.EyeColor = models.NewOptionalString(*performer.EyeColor) + } + if performer.FakeTits != nil && !excluded["fake_tits"] { + partial.FakeTits = models.NewOptionalString(*performer.FakeTits) + } + if performer.Gender != nil && !excluded["gender"] { + partial.Gender = models.NewOptionalString(*performer.Gender) + } + if performer.Height != nil && !excluded["height"] { + h, err := strconv.Atoi(*performer.Height) + if err == nil { + partial.Height = models.NewOptionalInt(h) + } + } + if performer.Weight != nil && !excluded["weight"] { + w, err := strconv.Atoi(*performer.Weight) + if err == nil { + partial.Weight = models.NewOptionalInt(w) + } + } + if performer.Instagram != nil && !excluded["instagram"] { + partial.Instagram = models.NewOptionalString(*performer.Instagram) + } + if performer.Measurements != nil && !excluded["measurements"] { + partial.Measurements = models.NewOptionalString(*performer.Measurements) + } + if excluded["name"] && performer.Name != nil { + partial.Name = models.NewOptionalString(*performer.Name) + } + if performer.Piercings != nil && !excluded["piercings"] { + partial.Piercings = models.NewOptionalString(*performer.Piercings) + } + if performer.Tattoos != nil && !excluded["tattoos"] { + partial.Tattoos = models.NewOptionalString(*performer.Tattoos) + } + if performer.Twitter != nil && !excluded["twitter"] { + partial.Twitter = models.NewOptionalString(*performer.Twitter) + } + if performer.URL != nil && !excluded["url"] { + partial.URL = models.NewOptionalString(*performer.URL) + } + if !t.refresh { + // #3547 - need to overwrite the stash id for the endpoint, but preserve + // existing stash ids for other endpoints + partial.StashIDs = &models.UpdateStashIDs{ + StashIDs: t.performer.StashIDs.List(), + Mode: models.RelationshipUpdateModeSet, + } + + partial.StashIDs.Set(models.StashID{ + Endpoint: t.box.Endpoint, + StashID: *performer.RemoteSiteID, + }) + } + + return partial +} + func getDate(val *string) *models.Date { if val == nil { return nil diff --git a/pkg/ffmpeg/codec.go b/pkg/ffmpeg/codec.go index bb7734030..1195fdc3d 100644 --- a/pkg/ffmpeg/codec.go +++ b/pkg/ffmpeg/codec.go @@ -11,6 +11,7 @@ func (c VideoCodec) Args() []string { } var ( + // Software codec's VideoCodecLibX264 VideoCodec = "libx264" VideoCodecLibWebP VideoCodec = "libwebp" VideoCodecBMP VideoCodec = "bmp" diff --git a/pkg/ffmpeg/codec_hardware.go b/pkg/ffmpeg/codec_hardware.go new file mode 100644 index 000000000..6ab2e7870 --- /dev/null +++ b/pkg/ffmpeg/codec_hardware.go @@ -0,0 +1,207 @@ +package ffmpeg + +import ( + "bytes" + "context" + "fmt" + "regexp" + "strings" + + "github.com/stashapp/stash/pkg/logger" +) + +var ( + // Hardware codec's + VideoCodecN264 VideoCodec = "h264_nvenc" + VideoCodecI264 VideoCodec = "h264_qsv" + VideoCodecA264 VideoCodec = "h264_amf" + VideoCodecM264 VideoCodec = "h264_videotoolbox" + VideoCodecV264 VideoCodec = "h264_vaapi" + VideoCodecR264 VideoCodec = "h264_v4l2m2m" + VideoCodecO264 VideoCodec = "h264_omx" + VideoCodecIVP9 VideoCodec = "vp9_qsv" + VideoCodecVVP9 VideoCodec = "vp9_vaapi" + VideoCodecVVPX VideoCodec = "vp8_vaapi" +) + +// Tests all (given) hardware codec's +func (f *FFMpeg) InitHWSupport(ctx context.Context) { + var hwCodecSupport []VideoCodec + + for _, codec := range []VideoCodec{ + VideoCodecN264, + VideoCodecI264, + VideoCodecV264, + VideoCodecR264, + VideoCodecIVP9, + VideoCodecVVP9, + } { + var args Args + args = append(args, "-hide_banner") + args = args.LogLevel(LogLevelWarning) + args = f.hwDeviceInit(args, codec) + args = args.Format("lavfi") + args = args.Input("color=c=red") + args = args.Duration(0.1) + + videoFilter := f.hwFilterInit(codec) + // Test scaling + videoFilter = videoFilter.ScaleDimensions(-2, 160) + videoFilter = f.hwCodecFilter(videoFilter, codec) + args = append(args, CodecInit(codec)...) + args = args.VideoFilter(videoFilter) + + args = args.Format("null") + args = args.Output("-") + + cmd := f.Command(ctx, args) + + var stderr bytes.Buffer + cmd.Stderr = &stderr + + if err := cmd.Start(); err != nil { + logger.Debugf("[InitHWSupport] error starting command: %w", err) + continue + } + + if err := cmd.Wait(); err != nil { + errOutput := stderr.String() + + if len(errOutput) == 0 { + errOutput = err.Error() + } + + logger.Debugf("[InitHWSupport] Codec %s not supported. Error output:\n%s", codec, errOutput) + } else { + hwCodecSupport = append(hwCodecSupport, codec) + } + } + + outstr := "[InitHWSupport] Supported HW codecs:\n" + for _, codec := range hwCodecSupport { + outstr += fmt.Sprintf("\t%s\n", codec) + } + logger.Info(outstr) + + f.hwCodecSupport = hwCodecSupport +} + +// Prepend input for hardware encoding only +func (f *FFMpeg) hwDeviceInit(args Args, codec VideoCodec) Args { + switch codec { + case VideoCodecN264: + args = append(args, "-hwaccel_device") + args = append(args, "0") + case VideoCodecV264, + VideoCodecVVP9: + args = append(args, "-vaapi_device") + args = append(args, "/dev/dri/renderD128") + case VideoCodecI264, + VideoCodecIVP9: + args = append(args, "-init_hw_device") + args = append(args, "qsv=hw") + args = append(args, "-filter_hw_device") + args = append(args, "hw") + } + + return args +} + +// Initialise a video filter for HW encoding +func (f *FFMpeg) hwFilterInit(codec VideoCodec) VideoFilter { + var videoFilter VideoFilter + switch codec { + case VideoCodecV264, + VideoCodecVVP9: + videoFilter = videoFilter.Append("format=nv12") + videoFilter = videoFilter.Append("hwupload") + case VideoCodecN264: + videoFilter = videoFilter.Append("format=nv12") + videoFilter = videoFilter.Append("hwupload_cuda") + case VideoCodecI264, + VideoCodecIVP9: + videoFilter = videoFilter.Append("hwupload=extra_hw_frames=64") + videoFilter = videoFilter.Append("format=qsv") + } + + return videoFilter +} + +// Replace video filter scaling with hardware scaling for full hardware transcoding +func (f *FFMpeg) hwCodecFilter(args VideoFilter, codec VideoCodec) VideoFilter { + sargs := string(args) + + if strings.Contains(sargs, "scale=") { + switch codec { + case VideoCodecN264: + args = VideoFilter(strings.Replace(sargs, "scale=", "scale_cuda=", 1)) + case VideoCodecV264, + VideoCodecVVP9: + args = VideoFilter(strings.Replace(sargs, "scale=", "scale_vaapi=", 1)) + case VideoCodecI264, + VideoCodecIVP9: + // BUG: [scale_qsv]: Size values less than -1 are not acceptable. + // Fix: Replace all instances of -2 with -1 in a scale operation + re := regexp.MustCompile(`(scale=)([\d:]*)(-2)(.*)`) + sargs = re.ReplaceAllString(sargs, "scale=$2-1$4") + args = VideoFilter(strings.Replace(sargs, "scale=", "scale_qsv=", 1)) + } + } + + return args +} + +// Returns the max resolution for a given codec, or a default +func (f *FFMpeg) hwCodecMaxRes(codec VideoCodec, dW int, dH int) (int, int) { + if codec == VideoCodecN264 { + return 4096, 4096 + } + + return dW, dH +} + +// Return a maxres filter +func (f *FFMpeg) hwMaxResFilter(codec VideoCodec, width int, height int, max int) VideoFilter { + videoFilter := f.hwFilterInit(codec) + maxWidth, maxHeight := f.hwCodecMaxRes(codec, width, height) + videoFilter = videoFilter.ScaleMaxLM(width, height, max, maxWidth, maxHeight) + return f.hwCodecFilter(videoFilter, codec) +} + +// Return if a hardware accelerated for HLS is available +func (f *FFMpeg) hwCodecHLSCompatible() *VideoCodec { + for _, element := range f.hwCodecSupport { + switch element { + case VideoCodecN264, + VideoCodecI264, + VideoCodecV264, + VideoCodecR264: + return &element + } + } + return nil +} + +// Return if a hardware accelerated codec for MP4 is available +func (f *FFMpeg) hwCodecMP4Compatible() *VideoCodec { + for _, element := range f.hwCodecSupport { + switch element { + case VideoCodecN264, + VideoCodecI264: + return &element + } + } + return nil +} + +// Return if a hardware accelerated codec for WebM is available +func (f *FFMpeg) hwCodecWEBMCompatible() *VideoCodec { + for _, element := range f.hwCodecSupport { + switch element { + case VideoCodecIVP9, + VideoCodecVVP9: + return &element + } + } + return nil +} diff --git a/pkg/ffmpeg/downloader.go b/pkg/ffmpeg/downloader.go index 304797f7a..b98e20f6f 100644 --- a/pkg/ffmpeg/downloader.go +++ b/pkg/ffmpeg/downloader.go @@ -103,7 +103,13 @@ func downloadSingle(ctx context.Context, configDirectory, url string) error { return err } - resp, err := http.DefaultClient.Do(req) + transport := &http.Transport{Proxy: http.ProxyFromEnvironment} + + client := &http.Client{ + Transport: transport, + } + + resp, err := client.Do(req) if err != nil { return err } diff --git a/pkg/ffmpeg/ffmpeg.go b/pkg/ffmpeg/ffmpeg.go index 3a961bbed..58621bc70 100644 --- a/pkg/ffmpeg/ffmpeg.go +++ b/pkg/ffmpeg/ffmpeg.go @@ -9,9 +9,21 @@ import ( ) // FFMpeg provides an interface to ffmpeg. -type FFMpeg string +type FFMpeg struct { + ffmpeg string + hwCodecSupport []VideoCodec +} + +// Creates a new FFMpeg encoder +func NewEncoder(ffmpegPath string) *FFMpeg { + ret := &FFMpeg{ + ffmpeg: ffmpegPath, + } + + return ret +} // Returns an exec.Cmd that can be used to run ffmpeg using args. func (f *FFMpeg) Command(ctx context.Context, args []string) *exec.Cmd { - return stashExec.CommandContext(ctx, string(*f), args...) + return stashExec.CommandContext(ctx, string(f.ffmpeg), args...) } diff --git a/pkg/ffmpeg/filter.go b/pkg/ffmpeg/filter.go index 8b9c94122..52be57c9c 100644 --- a/pkg/ffmpeg/filter.go +++ b/pkg/ffmpeg/filter.go @@ -1,6 +1,8 @@ package ffmpeg -import "fmt" +import ( + "fmt" +) // VideoFilter represents video filter parameters to be passed to ffmpeg. type VideoFilter string @@ -57,6 +59,35 @@ func (f VideoFilter) ScaleMax(inputWidth, inputHeight, maxSize int) VideoFilter return f.ScaleDimensions(maxSize, -2) } +// ScaleMaxLM returns a VideoFilter scaling to maxSize with respect to a max size. +func (f VideoFilter) ScaleMaxLM(width int, height int, reqHeight int, maxWidth int, maxHeight int) VideoFilter { + // calculate the aspect ratio of the current resolution + aspectRatio := width / height + + // find the max height + desiredHeight := reqHeight + if desiredHeight == 0 { + desiredHeight = height + } + + // calculate the desired width based on the desired height and the aspect ratio + desiredWidth := int(desiredHeight * aspectRatio) + + // check which dimension to scale based on the maximum resolution + if desiredHeight > maxHeight || desiredWidth > maxWidth { + if desiredHeight-maxHeight > desiredWidth-maxWidth { + // scale the height down to the maximum height + return f.ScaleDimensions(-2, maxHeight) + } else { + // scale the width down to the maximum width + return f.ScaleDimensions(maxWidth, -2) + } + } + + // the current resolution can be scaled to the desired height without exceeding the maximum resolution + return f.ScaleMax(width, height, reqHeight) +} + // Fps returns a VideoFilter setting the frames per second. func (f VideoFilter) Fps(fps int) VideoFilter { return f.Append(fmt.Sprintf("fps=%v", fps)) diff --git a/pkg/ffmpeg/frame_rate.go b/pkg/ffmpeg/frame_rate.go index 07585b67e..271f6c4cb 100644 --- a/pkg/ffmpeg/frame_rate.go +++ b/pkg/ffmpeg/frame_rate.go @@ -16,7 +16,7 @@ type FrameInfo struct { // CalculateFrameRate calculates the frame rate and number of frames of the video file. // Used where the frame rate or NbFrames is missing or invalid in the ffprobe output. -func (f FFMpeg) CalculateFrameRate(ctx context.Context, v *VideoFile) (*FrameInfo, error) { +func (f *FFMpeg) CalculateFrameRate(ctx context.Context, v *VideoFile) (*FrameInfo, error) { var args Args args = append(args, "-nostats") args = args.Input(v.Path). diff --git a/pkg/ffmpeg/generate.go b/pkg/ffmpeg/generate.go index 934bd1500..5b3a888d3 100644 --- a/pkg/ffmpeg/generate.go +++ b/pkg/ffmpeg/generate.go @@ -13,7 +13,7 @@ import ( // Generate runs ffmpeg with the given args and waits for it to finish. // Returns an error if the command fails. If the command fails, the return // value will be of type *exec.ExitError. -func (f FFMpeg) Generate(ctx context.Context, args Args) error { +func (f *FFMpeg) Generate(ctx context.Context, args Args) error { cmd := f.Command(ctx, args) var stderr bytes.Buffer @@ -36,7 +36,7 @@ func (f FFMpeg) Generate(ctx context.Context, args Args) error { } // GenerateOutput runs ffmpeg with the given args and returns it standard output. -func (f FFMpeg) GenerateOutput(ctx context.Context, args []string, stdin io.Reader) ([]byte, error) { +func (f *FFMpeg) GenerateOutput(ctx context.Context, args []string, stdin io.Reader) ([]byte, error) { cmd := f.Command(ctx, args) cmd.Stdin = stdin diff --git a/pkg/ffmpeg/hls.go b/pkg/ffmpeg/hls.go deleted file mode 100644 index f3b421c52..000000000 --- a/pkg/ffmpeg/hls.go +++ /dev/null @@ -1,40 +0,0 @@ -package ffmpeg - -import ( - "fmt" - "io" - "strings" -) - -const hlsSegmentLength = 10.0 - -// WriteHLSPlaylist writes a HLS playlist to w using baseUrl as the base URL for TS streams. -func WriteHLSPlaylist(duration float64, baseUrl string, w io.Writer) { - fmt.Fprint(w, "#EXTM3U\n") - fmt.Fprint(w, "#EXT-X-VERSION:3\n") - fmt.Fprint(w, "#EXT-X-MEDIA-SEQUENCE:0\n") - fmt.Fprint(w, "#EXT-X-ALLOW-CACHE:YES\n") - fmt.Fprintf(w, "#EXT-X-TARGETDURATION:%d\n", int(hlsSegmentLength)) - fmt.Fprint(w, "#EXT-X-PLAYLIST-TYPE:VOD\n") - - leftover := duration - upTo := 0.0 - - i := strings.LastIndex(baseUrl, ".m3u8") - tsURL := baseUrl[0:i] + ".ts" - - for leftover > 0 { - thisLength := hlsSegmentLength - if leftover < thisLength { - thisLength = leftover - } - - fmt.Fprintf(w, "#EXTINF: %f,\n", thisLength) - fmt.Fprintf(w, "%s?start=%f\n", tsURL, upTo) - - leftover -= thisLength - upTo += thisLength - } - - fmt.Fprint(w, "#EXT-X-ENDLIST\n") -} diff --git a/pkg/ffmpeg/stream.go b/pkg/ffmpeg/stream.go index 1088778d1..b94c03b55 100644 --- a/pkg/ffmpeg/stream.go +++ b/pkg/ffmpeg/stream.go @@ -2,229 +2,132 @@ package ffmpeg import ( "context" - "errors" - "io" "net/http" - "os/exec" - "strings" - "syscall" + "sync" + "time" + "github.com/stashapp/stash/pkg/fsutil" "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/models" ) const ( - MimeWebm string = "video/webm" - MimeMkv string = "video/x-matroska" - MimeMp4 string = "video/mp4" - MimeHLS string = "application/vnd.apple.mpegurl" - MimeMpegts string = "video/MP2T" + MimeWebmVideo string = "video/webm" + MimeWebmAudio string = "audio/webm" + MimeMkvVideo string = "video/x-matroska" + MimeMkvAudio string = "audio/x-matroska" + MimeMp4Video string = "video/mp4" + MimeMp4Audio string = "audio/mp4" ) -// Stream represents an ongoing transcoded stream. -type Stream struct { - Stdout io.ReadCloser - Cmd *exec.Cmd - mimeType string +type StreamManager struct { + cacheDir string + encoder *FFMpeg + ffprobe FFProbe + + config StreamManagerConfig + lockManager *fsutil.ReadLockManager + + context context.Context + cancelFunc context.CancelFunc + + runningStreams map[string]*runningStream + streamsMutex sync.Mutex } -// Serve is an http handler function that serves the stream. -func (s *Stream) Serve(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", s.mimeType) - w.WriteHeader(http.StatusOK) - - logger.Infof("[stream] transcoding video file to %s", s.mimeType) - - // process killing should be handled by command context - - _, err := io.Copy(w, s.Stdout) - if err != nil && !errors.Is(err, syscall.EPIPE) { - logger.Errorf("[stream] error serving transcoded video file: %v", err) - } +type StreamManagerConfig interface { + GetMaxStreamingTranscodeSize() models.StreamingResolutionEnum + GetLiveTranscodeInputArgs() []string + GetLiveTranscodeOutputArgs() []string + GetTranscodeHardwareAcceleration() bool } -// StreamFormat represents a transcode stream format. -type StreamFormat struct { - MimeType string - codec VideoCodec - format Format - extraArgs []string - hls bool -} - -var ( - StreamFormatHLS = StreamFormat{ - codec: VideoCodecLibX264, - format: FormatMpegTS, - MimeType: MimeMpegts, - extraArgs: []string{ - "-acodec", "aac", - "-pix_fmt", "yuv420p", - "-preset", "veryfast", - "-crf", "25", - }, - hls: true, +func NewStreamManager(cacheDir string, encoder *FFMpeg, ffprobe FFProbe, config StreamManagerConfig, lockManager *fsutil.ReadLockManager) *StreamManager { + if cacheDir == "" { + logger.Warn("cache directory is not set. Live HLS/DASH transcoding will be disabled") } - StreamFormatH264 = StreamFormat{ - codec: VideoCodecLibX264, - format: FormatMP4, - MimeType: MimeMp4, - extraArgs: []string{ - "-movflags", "frag_keyframe+empty_moov", - "-pix_fmt", "yuv420p", - "-preset", "veryfast", - "-crf", "25", - }, + ctx, cancel := context.WithCancel(context.Background()) + + ret := &StreamManager{ + cacheDir: cacheDir, + encoder: encoder, + ffprobe: ffprobe, + config: config, + lockManager: lockManager, + context: ctx, + cancelFunc: cancel, + runningStreams: make(map[string]*runningStream), } - StreamFormatVP9 = StreamFormat{ - codec: VideoCodecVP9, - format: FormatWebm, - MimeType: MimeWebm, - extraArgs: []string{ - "-deadline", "realtime", - "-cpu-used", "5", - "-row-mt", "1", - "-crf", "30", - "-b:v", "0", - "-pix_fmt", "yuv420p", - }, - } - - StreamFormatVP8 = StreamFormat{ - codec: VideoCodecVPX, - format: FormatWebm, - MimeType: MimeWebm, - extraArgs: []string{ - "-deadline", "realtime", - "-cpu-used", "5", - "-crf", "12", - "-b:v", "3M", - "-pix_fmt", "yuv420p", - }, - } - - StreamFormatHEVC = StreamFormat{ - codec: VideoCodecLibX265, - format: FormatMP4, - MimeType: MimeMp4, - extraArgs: []string{ - "-movflags", "frag_keyframe", - "-preset", "veryfast", - "-crf", "30", - }, - } - - // it is very common in MKVs to have just the audio codec unsupported - // copy the video stream, transcode the audio and serve as Matroska - StreamFormatMKVAudio = StreamFormat{ - codec: VideoCodecCopy, - format: FormatMatroska, - MimeType: MimeMkv, - extraArgs: []string{ - "-c:a", "libopus", - "-b:a", "96k", - "-vbr", "on", - }, - } -) - -// TranscodeStreamOptions represents options for live transcoding a video file. -type TranscodeStreamOptions struct { - Input string - Codec StreamFormat - StartTime float64 - MaxTranscodeSize int - - // original video dimensions - VideoWidth int - VideoHeight int - - // transcode the video, remove the audio - // in some videos where the audio codec is not supported by ffmpeg - // ffmpeg fails if you try to transcode the audio - VideoOnly bool -} - -func (o TranscodeStreamOptions) getStreamArgs() Args { - var args Args - args = append(args, "-hide_banner") - args = args.LogLevel(LogLevelError) - - if o.StartTime != 0 { - args = args.Seek(o.StartTime) - } - - if o.Codec.hls { - // we only serve a fixed segment length - args = args.Duration(hlsSegmentLength) - } - - args = args.Input(o.Input) - - if o.VideoOnly { - args = args.SkipAudio() - } - - args = args.VideoCodec(o.Codec.codec) - - // don't set scale when copying video stream - if o.Codec.codec != VideoCodecCopy { - var videoFilter VideoFilter - videoFilter = videoFilter.ScaleMax(o.VideoWidth, o.VideoHeight, o.MaxTranscodeSize) - args = args.VideoFilter(videoFilter) - } - - if len(o.Codec.extraArgs) > 0 { - args = append(args, o.Codec.extraArgs...) - } - - args = append(args, - // this is needed for 5-channel ac3 files - "-ac", "2", - ) - - args = args.Format(o.Codec.format) - args = args.Output("pipe:") - - return args -} - -// GetTranscodeStream starts the live transcoding process using ffmpeg and returns a stream. -func (f *FFMpeg) GetTranscodeStream(ctx context.Context, options TranscodeStreamOptions) (*Stream, error) { - args := options.getStreamArgs() - cmd := f.Command(ctx, args) - logger.Debugf("Streaming via: %s", strings.Join(cmd.Args, " ")) - - stdout, err := cmd.StdoutPipe() - if nil != err { - logger.Error("FFMPEG stdout not available: " + err.Error()) - return nil, err - } - - stderr, err := cmd.StderrPipe() - if nil != err { - logger.Error("FFMPEG stderr not available: " + err.Error()) - return nil, err - } - - if err = cmd.Start(); err != nil { - return nil, err - } - - // stderr must be consumed or the process deadlocks go func() { - stderrData, _ := io.ReadAll(stderr) - stderrString := string(stderrData) - if len(stderrString) > 0 { - logger.Debugf("[stream] ffmpeg stderr: %s", stderrString) + for { + select { + case <-time.After(monitorInterval): + ret.monitorStreams() + case <-ctx.Done(): + return + } } }() - ret := &Stream{ - Stdout: stdout, - Cmd: cmd, - mimeType: options.Codec.MimeType, - } - return ret, nil + return ret +} + +// Shutdown shuts down the stream manager, killing any running transcoding processes and removing all cached files. +func (sm *StreamManager) Shutdown() { + sm.cancelFunc() + sm.stopAndRemoveAll() +} + +type StreamRequestContext struct { + context.Context + ResponseWriter http.ResponseWriter +} + +func NewStreamRequestContext(w http.ResponseWriter, r *http.Request) *StreamRequestContext { + return &StreamRequestContext{ + Context: r.Context(), + ResponseWriter: w, + } +} + +func (c *StreamRequestContext) Cancel() { + hj, ok := (c.ResponseWriter).(http.Hijacker) + if !ok { + return + } + + // hijack and close the connection + conn, bw, _ := hj.Hijack() + if conn != nil { + if bw != nil { + // notify end of stream + _, err := bw.WriteString("0\r\n") + if err != nil { + logger.Warnf("unable to write end of stream: %v", err) + } + _, err = bw.WriteString("\r\n") + if err != nil { + logger.Warnf("unable to write end of stream: %v", err) + } + + // flush the buffer, but don't wait indefinitely + timeout := make(chan struct{}, 1) + go func() { + _ = bw.Flush() + close(timeout) + }() + + const waitTime = time.Second + + select { + case <-timeout: + case <-time.After(waitTime): + logger.Warnf("unable to flush buffer - closing connection") + } + } + + conn.Close() + } } diff --git a/pkg/ffmpeg/stream_segmented.go b/pkg/ffmpeg/stream_segmented.go new file mode 100644 index 000000000..b12283fc9 --- /dev/null +++ b/pkg/ffmpeg/stream_segmented.go @@ -0,0 +1,867 @@ +package ffmpeg + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "math" + "net/http" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/stashapp/stash/pkg/file" + "github.com/stashapp/stash/pkg/fsutil" + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/models" + + "github.com/zencoder/go-dash/v3/mpd" +) + +const ( + MimeHLS string = "application/vnd.apple.mpegurl" + MimeMpegTS string = "video/MP2T" + MimeDASH string = "application/dash+xml" + + segmentLength = 2 + + maxSegmentWait = 15 * time.Second + monitorInterval = 200 * time.Millisecond + + // segment gap before counting a request as a seek and + // restarting the transcode process at the requested segment + maxSegmentGap = 5 + + // maximum number of segments to generate + // ahead of the currently streaming segment + maxSegmentBuffer = 15 + + // maximum idle time between segment requests before + // stopping transcode and deleting cache folder + maxIdleTime = 30 * time.Second +) + +type StreamType struct { + Name string + SegmentType *SegmentType + ServeManifest func(sm *StreamManager, w http.ResponseWriter, r *http.Request, vf *file.VideoFile, resolution string) + Args func(codec VideoCodec, segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) Args +} + +var ( + StreamTypeHLS = &StreamType{ + Name: "hls", + SegmentType: SegmentTypeTS, + ServeManifest: serveHLSManifest, + Args: func(codec VideoCodec, segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) (args Args) { + args = CodecInit(codec) + args = append(args, + "-flags", "+cgop", + "-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", segmentLength), + ) + args = args.VideoFilter(videoFilter) + if videoOnly { + args = append(args, "-an") + } else { + args = append(args, + "-c:a", "aac", + "-ac", "2", + ) + } + args = append(args, + "-sn", + "-copyts", + "-avoid_negative_ts", "disabled", + "-f", "hls", + "-start_number", fmt.Sprint(segment), + "-hls_time", fmt.Sprint(segmentLength), + "-hls_segment_type", "mpegts", + "-hls_playlist_type", "vod", + "-hls_segment_filename", filepath.Join(outputDir, ".%d.ts"), + filepath.Join(outputDir, "manifest.m3u8"), + ) + return + }, + } + StreamTypeHLSCopy = &StreamType{ + Name: "hls-copy", + SegmentType: SegmentTypeTS, + ServeManifest: serveHLSManifest, + Args: func(codec VideoCodec, segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) (args Args) { + args = CodecInit(codec) + if videoOnly { + args = append(args, "-an") + } else { + args = append(args, + "-c:a", "aac", + "-ac", "2", + ) + } + args = append(args, + "-sn", + "-copyts", + "-avoid_negative_ts", "disabled", + "-f", "hls", + "-start_number", fmt.Sprint(segment), + "-hls_time", fmt.Sprint(segmentLength), + "-hls_segment_type", "mpegts", + "-hls_playlist_type", "vod", + "-hls_segment_filename", filepath.Join(outputDir, ".%d.ts"), + filepath.Join(outputDir, "manifest.m3u8"), + ) + return + }, + } + StreamTypeDASHVideo = &StreamType{ + Name: "dash-v", + SegmentType: SegmentTypeWEBMVideo, + ServeManifest: serveDASHManifest, + Args: func(codec VideoCodec, segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) (args Args) { + // only generate the actual init segment (init_v.webm) + // when generating the first segment + init := ".init" + if segment == 0 { + init = "init" + } + + args = CodecInit(codec) + args = append(args, + "-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", segmentLength), + ) + + args = args.VideoFilter(videoFilter) + args = append(args, + "-copyts", + "-avoid_negative_ts", "disabled", + "-map", "0:v:0", + "-f", "webm_chunk", + "-chunk_start_index", fmt.Sprint(segment), + "-header", filepath.Join(outputDir, init+"_v.webm"), + filepath.Join(outputDir, ".%d_v.webm"), + ) + return + }, + } + StreamTypeDASHAudio = &StreamType{ + Name: "dash-a", + SegmentType: SegmentTypeWEBMAudio, + ServeManifest: serveDASHManifest, + Args: func(codec VideoCodec, segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) (args Args) { + // only generate the actual init segment (init_a.webm) + // when generating the first segment + init := ".init" + if segment == 0 { + init = "init" + } + args = append(args, + "-c:a", "libopus", + "-b:a", "96000", + "-ar", "48000", + "-copyts", + "-avoid_negative_ts", "disabled", + "-map", "0:a:0", + "-f", "webm_chunk", + "-chunk_start_index", fmt.Sprint(segment), + "-audio_chunk_duration", fmt.Sprint(segmentLength*1000), + "-header", filepath.Join(outputDir, init+"_a.webm"), + filepath.Join(outputDir, ".%d_a.webm"), + ) + return + }, + } +) + +type SegmentType struct { + Format string + MimeType string + MakeFilename func(segment int) string + ParseSegment func(str string) (int, error) +} + +var ( + SegmentTypeTS = &SegmentType{ + Format: "%d.ts", + MimeType: MimeMpegTS, + MakeFilename: func(segment int) string { + return fmt.Sprintf("%d.ts", segment) + }, + ParseSegment: func(str string) (int, error) { + segment, err := strconv.Atoi(str) + if err != nil || segment < 0 { + err = ErrInvalidSegment + } + return segment, err + }, + } + SegmentTypeWEBMVideo = &SegmentType{ + Format: "%d_v.webm", + MimeType: MimeWebmVideo, + MakeFilename: func(segment int) string { + if segment == -1 { + return "init_v.webm" + } else { + return fmt.Sprintf("%d_v.webm", segment) + } + }, + ParseSegment: func(str string) (int, error) { + if str == "init" { + return -1, nil + } else { + segment, err := strconv.Atoi(str) + if err != nil || segment < 0 { + err = ErrInvalidSegment + } + return segment, err + } + }, + } + SegmentTypeWEBMAudio = &SegmentType{ + Format: "%d_a.webm", + MimeType: MimeWebmAudio, + MakeFilename: func(segment int) string { + if segment == -1 { + return "init_a.webm" + } else { + return fmt.Sprintf("%d_a.webm", segment) + } + }, + ParseSegment: func(str string) (int, error) { + if str == "init" { + return -1, nil + } else { + segment, err := strconv.Atoi(str) + if err != nil || segment < 0 { + err = ErrInvalidSegment + } + return segment, err + } + }, + } +) + +var ErrInvalidSegment = errors.New("invalid segment") + +type StreamOptions struct { + StreamType *StreamType + VideoFile *file.VideoFile + Resolution string + Hash string + Segment string +} + +type transcodeProcess struct { + cmd *exec.Cmd + context context.Context + cancel context.CancelFunc + cancelled bool + outputDir string + segmentType *SegmentType + segment int +} + +type waitingSegment struct { + segmentType *SegmentType + idx int + file string + path string + accessed time.Time + available chan error + done atomic.Bool +} + +type runningStream struct { + dir string + streamType *StreamType + vf *file.VideoFile + maxTranscodeSize int + outputDir string + + waitingSegments []*waitingSegment + tp *transcodeProcess + lastAccessed time.Time + lastSegment int +} + +func (t StreamType) String() string { + return t.Name +} + +func (t StreamType) FileDir(hash string, maxTranscodeSize int) string { + if maxTranscodeSize == 0 { + return fmt.Sprintf("%s_%s", hash, t) + } else { + return fmt.Sprintf("%s_%s_%d", hash, t, maxTranscodeSize) + } +} + +func HLSGetCodec(sm *StreamManager, name string) (codec VideoCodec) { + switch name { + case "hls": + codec = VideoCodecLibX264 + if hwcodec := sm.encoder.hwCodecHLSCompatible(); hwcodec != nil && sm.config.GetTranscodeHardwareAcceleration() { + codec = *hwcodec + } + case "dash-v": + codec = VideoCodecVP9 + if hwcodec := sm.encoder.hwCodecWEBMCompatible(); hwcodec != nil && sm.config.GetTranscodeHardwareAcceleration() { + codec = *hwcodec + } + case "hls-copy": + codec = VideoCodecCopy + } + + return codec +} + +func (s *runningStream) makeStreamArgs(sm *StreamManager, segment int) Args { + extraInputArgs := sm.config.GetLiveTranscodeInputArgs() + extraOutputArgs := sm.config.GetLiveTranscodeOutputArgs() + + args := Args{"-hide_banner"} + args = args.LogLevel(LogLevelError) + + codec := HLSGetCodec(sm, s.streamType.Name) + + args = sm.encoder.hwDeviceInit(args, codec) + args = append(args, extraInputArgs...) + + if segment > 0 { + args = args.Seek(float64(segment * segmentLength)) + } + + args = args.Input(s.vf.Path) + + videoOnly := ProbeAudioCodec(s.vf.AudioCodec) == MissingUnsupported + + videoFilter := sm.encoder.hwMaxResFilter(codec, s.vf.Width, s.vf.Height, s.maxTranscodeSize) + + args = append(args, s.streamType.Args(codec, segment, videoFilter, videoOnly, s.outputDir)...) + + args = append(args, extraOutputArgs...) + + return args +} + +// checkSegments renames temp segments that have been completely generated. +// existing segments are not replaced - if a segment is generated +// multiple times, then only the first one is kept. +func (tp *transcodeProcess) checkSegments() { + doSegment := func(filename string) { + if filename != "" { + oldPath := filepath.Join(tp.outputDir, filename) + newPath := filepath.Join(tp.outputDir, filename[1:]) + if !segmentExists(newPath) { + _ = os.Rename(oldPath, newPath) + } else { + os.Remove(oldPath) + } + } + } + + processState := tp.cmd.ProcessState + var lastFilename string + for i := tp.segment; ; i++ { + filename := fmt.Sprintf("."+tp.segmentType.Format, i) + if segmentExists(filepath.Join(tp.outputDir, filename)) { + // this segment exists so the previous segment is valid + doSegment(lastFilename) + } else { + // if the transcode process has exited then + // we need to do something with the last segment + if processState != nil { + if processState.Success() { + // if the process exited successfully then + // count the last segment as valid + doSegment(lastFilename) + } else if lastFilename != "" { + // if the process exited unsuccessfully then just delete + // the last segment, it's probably incomplete + os.Remove(filepath.Join(tp.outputDir, lastFilename)) + } + } + break + } + + lastFilename = filename + tp.segment = i + } +} + +func lastSegment(vf *file.VideoFile) int { + return int(math.Ceil(vf.Duration/segmentLength)) - 1 +} + +func segmentExists(path string) bool { + exists, _ := fsutil.FileExists(path) + return exists +} + +// serveHLSManifest serves a generated HLS playlist. The URLs for the segments +// are of the form {r.URL}/%d.ts{?urlQuery} where %d is the segment index. +func serveHLSManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request, vf *file.VideoFile, resolution string) { + if sm.cacheDir == "" { + logger.Error("[transcode] cannot live transcode with HLS because cache dir is unset") + http.Error(w, "cannot live transcode with HLS because cache dir is unset", http.StatusServiceUnavailable) + return + } + + probeResult, err := sm.ffprobe.NewVideoFile(vf.Path) + if err != nil { + logger.Warnf("[transcode] error generating HLS manifest: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + baseUrl := *r.URL + baseUrl.RawQuery = "" + baseURL := baseUrl.String() + + var urlQuery string + if resolution != "" { + urlQuery = fmt.Sprintf("?resolution=%s", resolution) + } + + var buf bytes.Buffer + + fmt.Fprint(&buf, "#EXTM3U\n") + + fmt.Fprint(&buf, "#EXT-X-VERSION:3\n") + fmt.Fprint(&buf, "#EXT-X-MEDIA-SEQUENCE:0\n") + fmt.Fprintf(&buf, "#EXT-X-TARGETDURATION:%d\n", segmentLength) + fmt.Fprint(&buf, "#EXT-X-PLAYLIST-TYPE:VOD\n") + + leftover := probeResult.FileDuration + segment := 0 + + for leftover > 0 { + thisLength := float64(segmentLength) + if leftover < thisLength { + thisLength = leftover + } + + fmt.Fprintf(&buf, "#EXTINF:%f,\n", thisLength) + fmt.Fprintf(&buf, "%s/%d.ts%s\n", baseURL, segment, urlQuery) + + leftover -= thisLength + segment++ + } + + fmt.Fprint(&buf, "#EXT-X-ENDLIST\n") + + w.Header().Set("Content-Type", MimeHLS) + http.ServeContent(w, r, "", time.Time{}, bytes.NewReader(buf.Bytes())) +} + +// serveDASHManifest serves a generated DASH manifest. +func serveDASHManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request, vf *file.VideoFile, resolution string) { + if sm.cacheDir == "" { + logger.Error("[transcode] cannot live transcode with DASH because cache dir is unset") + http.Error(w, "cannot live transcode files with DASH because cache dir is unset", http.StatusServiceUnavailable) + return + } + + probeResult, err := sm.ffprobe.NewVideoFile(vf.Path) + if err != nil { + logger.Warnf("[transcode] error generating DASH manifest: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var framerate string + var videoWidth int + var videoHeight int + videoStream := probeResult.VideoStream + if videoStream != nil { + framerate = videoStream.AvgFrameRate + videoWidth = videoStream.Width + videoHeight = videoStream.Height + } else { + // extract the framerate fraction from the file framerate + // framerates 0.1% below round numbers are common, + // attempt to infer when this is the case + fileFramerate := vf.FrameRate + rate1001, off1001 := math.Modf(fileFramerate * 1.001) + var numerator int + var denominator int + switch { + case off1001 < 0.005: + numerator = int(rate1001) * 1000 + denominator = 1001 + case off1001 > 0.995: + numerator = (int(rate1001) + 1) * 1000 + denominator = 1001 + default: + numerator = int(fileFramerate * 1000) + denominator = 1000 + } + framerate = fmt.Sprintf("%d/%d", numerator, denominator) + videoHeight = vf.Height + videoWidth = vf.Width + } + + var urlQuery string + maxTranscodeSize := sm.config.GetMaxStreamingTranscodeSize().GetMaxResolution() + if resolution != "" { + maxTranscodeSize = models.StreamingResolutionEnum(resolution).GetMaxResolution() + urlQuery = fmt.Sprintf("?resolution=%s", resolution) + } + if maxTranscodeSize != 0 { + videoSize := videoHeight + if videoWidth < videoSize { + videoSize = videoWidth + } + + if maxTranscodeSize < videoSize { + scaleFactor := float64(maxTranscodeSize) / float64(videoSize) + videoWidth = int(float64(videoWidth) * scaleFactor) + videoHeight = int(float64(videoHeight) * scaleFactor) + } + } + + mediaDuration := mpd.Duration(time.Duration(probeResult.FileDuration * float64(time.Second))) + m := mpd.NewMPD(mpd.DASH_PROFILE_LIVE, mediaDuration.String(), "PT4.0S") + + baseUrl := r.URL.JoinPath("/") + baseUrl.RawQuery = "" + m.BaseURL = baseUrl.String() + + video, _ := m.AddNewAdaptationSetVideo(MimeWebmVideo, "progressive", true, 1) + + _, _ = video.SetNewSegmentTemplate(2, "init_v.webm"+urlQuery, "$Number$_v.webm"+urlQuery, 0, 1) + _, _ = video.AddNewRepresentationVideo(200000, "vp09.00.40.08", "0", framerate, int64(videoWidth), int64(videoHeight)) + + if ProbeAudioCodec(vf.AudioCodec) != MissingUnsupported { + audio, _ := m.AddNewAdaptationSetAudio(MimeWebmAudio, true, 1, "und") + _, _ = audio.SetNewSegmentTemplate(2, "init_a.webm"+urlQuery, "$Number$_a.webm"+urlQuery, 0, 1) + _, _ = audio.AddNewRepresentationAudio(48000, 96000, "opus", "1") + } + + var buf bytes.Buffer + _ = m.Write(&buf) + + w.Header().Set("Content-Type", MimeDASH) + http.ServeContent(w, r, "", time.Time{}, bytes.NewReader(buf.Bytes())) +} + +func (sm *StreamManager) ServeManifest(w http.ResponseWriter, r *http.Request, streamType *StreamType, vf *file.VideoFile, resolution string) { + streamType.ServeManifest(sm, w, r, vf, resolution) +} + +func (sm *StreamManager) serveWaitingSegment(w http.ResponseWriter, r *http.Request, segment *waitingSegment) { + select { + case <-r.Context().Done(): + break + case err := <-segment.available: + if err == nil { + logger.Tracef("[transcode] streaming segment file %s", segment.file) + w.Header().Set("Content-Type", segment.segmentType.MimeType) + // Prevent caching as segments are generated on the fly + w.Header().Add("Cache-Control", "no-cache") + http.ServeFile(w, r, segment.path) + } else if !errors.Is(err, context.Canceled) { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } + segment.done.Store(true) +} + +func (sm *StreamManager) ServeSegment(w http.ResponseWriter, r *http.Request, options StreamOptions) { + if sm.cacheDir == "" { + logger.Error("[transcode] cannot live transcode files because cache dir is unset") + http.Error(w, "cannot live transcode files because cache dir is unset", http.StatusServiceUnavailable) + return + } + + if options.Hash == "" { + http.Error(w, "invalid hash", http.StatusBadRequest) + return + } + + streamType := options.StreamType + + segment, err := streamType.SegmentType.ParseSegment(options.Segment) + // error if segment is past the end of the video + if err != nil || segment > lastSegment(options.VideoFile) { + http.Error(w, "invalid segment", http.StatusBadRequest) + return + } + + maxTranscodeSize := sm.config.GetMaxStreamingTranscodeSize().GetMaxResolution() + if options.Resolution != "" { + maxTranscodeSize = models.StreamingResolutionEnum(options.Resolution).GetMaxResolution() + } + + dir := options.StreamType.FileDir(options.Hash, maxTranscodeSize) + outputDir := filepath.Join(sm.cacheDir, dir) + + name := streamType.SegmentType.MakeFilename(segment) + file := filepath.Join(dir, name) + + sm.streamsMutex.Lock() + + stream := sm.runningStreams[dir] + if stream == nil { + stream = &runningStream{ + dir: dir, + streamType: options.StreamType, + vf: options.VideoFile, + maxTranscodeSize: maxTranscodeSize, + outputDir: outputDir, + + // initialize to cap 10 to avoid reallocations + waitingSegments: make([]*waitingSegment, 0, 10), + } + sm.runningStreams[dir] = stream + } + + now := time.Now() + stream.lastAccessed = now + if segment != -1 { + stream.lastSegment = segment + } + + waitingSegment := &waitingSegment{ + segmentType: streamType.SegmentType, + idx: segment, + file: file, + path: filepath.Join(sm.cacheDir, file), + accessed: now, + available: make(chan error, 1), + } + stream.waitingSegments = append(stream.waitingSegments, waitingSegment) + + sm.streamsMutex.Unlock() + + sm.serveWaitingSegment(w, r, waitingSegment) +} + +// assume lock is held +func (sm *StreamManager) startTranscode(stream *runningStream, segment int, done chan<- error) { + // generate segment 0 if init segment requested + if segment == -1 { + segment = 0 + } + + logger.Debugf("[transcode] starting transcode for %s at segment #%d", stream.dir, segment) + + if err := os.MkdirAll(stream.outputDir, os.ModePerm); err != nil { + logger.Errorf("[transcode] %v", err) + done <- err + return + } + + lockCtx := sm.lockManager.ReadLock(sm.context, stream.vf.Path) + + args := stream.makeStreamArgs(sm, segment) + cmd := sm.encoder.Command(lockCtx, args) + + stderr, err := cmd.StderrPipe() + if err != nil { + logger.Errorf("[transcode] ffmpeg stderr not available: %v", err) + } + + stdout, err := cmd.StdoutPipe() + if nil != err { + logger.Errorf("[transcode] ffmpeg stdout not available: %v", err) + } + + logger.Tracef("[transcode] running %s", cmd) + if err := cmd.Start(); err != nil { + lockCtx.Cancel() + err = fmt.Errorf("error starting transcode process: %w", err) + logger.Errorf("[transcode] %v", err) + done <- err + return + } + + tp := &transcodeProcess{ + cmd: cmd, + context: lockCtx, + cancel: lockCtx.Cancel, + outputDir: stream.outputDir, + segmentType: stream.streamType.SegmentType, + segment: segment, + } + stream.tp = tp + + go func() { + errStr, _ := io.ReadAll(stderr) + outStr, _ := io.ReadAll(stdout) + + errCmd := cmd.Wait() + + var err error + + // don't log error if cancelled + if !tp.cancelled { + e := string(errStr) + if e == "" { + e = string(outStr) + } + if e != "" { + err = errors.New(e) + } else { + err = errCmd + } + + if err != nil { + err = fmt.Errorf("ffmpeg error when running command <%s>: %w", strings.Join(cmd.Args, " "), err) + + var exitError *exec.ExitError + if !errors.As(err, &exitError) { + logger.Errorf("[transcode] %v", err) + } + } + } + + sm.streamsMutex.Lock() + + // make sure that cancel is called to prevent memory leaks + tp.cancel() + + // clear remaining segments after ffmpeg exit + tp.checkSegments() + + if stream.tp == tp { + stream.tp = nil + } + + sm.streamsMutex.Unlock() + + if err != nil { + done <- err + } + }() +} + +// assume lock is held +func (sm *StreamManager) stopTranscode(stream *runningStream) { + tp := stream.tp + if tp != nil { + tp.cancel() + tp.cancelled = true + } +} + +func (sm *StreamManager) checkTranscode(stream *runningStream, now time.Time) { + if len(stream.waitingSegments) == 0 && stream.lastAccessed.Add(maxIdleTime).Before(now) { + // Stream expired. Cancel the transcode process and delete the files + logger.Debugf("[transcode] stream for %s not accessed recently. Cancelling transcode and removing files", stream.dir) + + sm.stopTranscode(stream) + sm.removeTranscodeFiles(stream) + + delete(sm.runningStreams, stream.dir) + return + } + + if stream.tp != nil { + segmentType := stream.streamType.SegmentType + segment := stream.lastSegment + // if all segments up to maxSegmentBuffer exist, stop transcode + for i := segment; i < segment+maxSegmentBuffer; i++ { + if !segmentExists(filepath.Join(stream.outputDir, segmentType.MakeFilename(i))) { + return + } + } + + logger.Debugf("[transcode] stopping transcode for %s, buffer is full", stream.dir) + sm.stopTranscode(stream) + } +} + +func (s *waitingSegment) checkAvailable(now time.Time) bool { + if segmentExists(s.path) { + s.available <- nil + return true + } else if s.accessed.Add(maxSegmentWait).Before(now) { + err := fmt.Errorf("timed out waiting for segment file %s to be generated", s.file) + logger.Errorf("[transcode] %v", err) + s.available <- err + return true + } + return false +} + +// ensureTranscode will start a new transcode process if the transcode +// is more than maxSegmentGap behind the requested segment +func (sm *StreamManager) ensureTranscode(stream *runningStream, segment *waitingSegment) bool { + segmentIdx := segment.idx + tp := stream.tp + if tp == nil { + sm.startTranscode(stream, segmentIdx, segment.available) + return true + } else if segmentIdx < tp.segment || tp.segment+maxSegmentGap < segmentIdx { + // only stop the transcode process here - it will be restarted only + // after the old process exits as stream.tp will then be nil. + sm.stopTranscode(stream) + return true + } + return false +} + +// runs every monitorInterval +func (sm *StreamManager) monitorStreams() { + sm.streamsMutex.Lock() + defer sm.streamsMutex.Unlock() + + now := time.Now() + + for _, stream := range sm.runningStreams { + if stream.tp != nil { + stream.tp.checkSegments() + } + + transcodeStarted := false + temp := stream.waitingSegments[:0] + for _, segment := range stream.waitingSegments { + remove := false + if segment.done.Load() || segment.checkAvailable(now) { + remove = true + } else if !transcodeStarted { + transcodeStarted = sm.ensureTranscode(stream, segment) + } + if !remove { + temp = append(temp, segment) + } + } + stream.waitingSegments = temp + + if !transcodeStarted { + sm.checkTranscode(stream, now) + } + } +} + +// assume lock is held +func (sm *StreamManager) removeTranscodeFiles(stream *runningStream) { + path := stream.outputDir + if err := os.RemoveAll(path); err != nil { + logger.Warnf("[transcode] error removing segment directory %s: %v", path, err) + } +} + +// stopAndRemoveAll stops all current streams and removes all cache files +func (sm *StreamManager) stopAndRemoveAll() { + sm.streamsMutex.Lock() + defer sm.streamsMutex.Unlock() + + for _, stream := range sm.runningStreams { + for _, segment := range stream.waitingSegments { + if len(segment.available) == 0 { + segment.available <- context.Canceled + } + } + sm.stopTranscode(stream) + sm.removeTranscodeFiles(stream) + } + + // ensure nothing else can use the map + sm.runningStreams = nil +} diff --git a/pkg/ffmpeg/stream_transcode.go b/pkg/ffmpeg/stream_transcode.go new file mode 100644 index 000000000..7fbfc08a8 --- /dev/null +++ b/pkg/ffmpeg/stream_transcode.go @@ -0,0 +1,276 @@ +package ffmpeg + +import ( + "errors" + "io" + "net/http" + "os/exec" + "strings" + "syscall" + + "github.com/stashapp/stash/pkg/file" + "github.com/stashapp/stash/pkg/fsutil" + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/models" +) + +type StreamFormat struct { + MimeType string + Args func(codec VideoCodec, videoFilter VideoFilter, videoOnly bool) Args +} + +func CodecInit(codec VideoCodec) (args Args) { + args = args.VideoCodec(codec) + + switch codec { + // CPU Codecs + case VideoCodecLibX264: + args = append(args, + "-pix_fmt", "yuv420p", + "-preset", "veryfast", + "-crf", "25", + "-sc_threshold", "0", + ) + case VideoCodecVP9: + args = append(args, + "-pix_fmt", "yuv420p", + "-deadline", "realtime", + "-cpu-used", "5", + "-row-mt", "1", + "-crf", "30", + "-b:v", "0", + ) + // HW Codecs + case VideoCodecN264: + args = append(args, + "-rc", "vbr", + "-cq", "15", + ) + case VideoCodecI264: + args = append(args, + "-global_quality", "20", + "-preset", "faster", + ) + case VideoCodecV264: + args = append(args, + "-qp", "20", + ) + case VideoCodecA264: + args = append(args, + "-quality", "speed", + ) + case VideoCodecM264: + args = append(args, + "-prio_speed", "1", + ) + case VideoCodecO264: + args = append(args, + "-preset", "superfast", + "-crf", "25", + ) + case VideoCodecIVP9: + args = append(args, + "-global_quality", "20", + "-preset", "faster", + ) + case VideoCodecVVP9: + args = append(args, + "-qp", "20", + ) + } + + return args +} + +var ( + StreamTypeMP4 = StreamFormat{ + MimeType: MimeMp4Video, + Args: func(codec VideoCodec, videoFilter VideoFilter, videoOnly bool) (args Args) { + args = CodecInit(codec) + args = append(args, "-movflags", "frag_keyframe+empty_moov") + args = args.VideoFilter(videoFilter) + if videoOnly { + args = args.SkipAudio() + } else { + args = append(args, "-ac", "2") + } + args = args.Format(FormatMP4) + return + }, + } + StreamTypeWEBM = StreamFormat{ + MimeType: MimeWebmVideo, + Args: func(codec VideoCodec, videoFilter VideoFilter, videoOnly bool) (args Args) { + args = CodecInit(codec) + args = args.VideoFilter(videoFilter) + if videoOnly { + args = args.SkipAudio() + } else { + args = append(args, "-ac", "2") + } + args = args.Format(FormatWebm) + return + }, + } + StreamTypeMKV = StreamFormat{ + MimeType: MimeMkvVideo, + Args: func(codec VideoCodec, videoFilter VideoFilter, videoOnly bool) (args Args) { + args = CodecInit(codec) + if videoOnly { + args = args.SkipAudio() + } else { + args = args.AudioCodec(AudioCodecLibOpus) + args = append(args, + "-b:a", "96k", + "-vbr", "on", + "-ac", "2", + ) + } + args = args.Format(FormatMatroska) + return + }, + } +) + +type TranscodeOptions struct { + StreamType StreamFormat + VideoFile *file.VideoFile + Resolution string + StartTime float64 +} + +func FileGetCodec(sm *StreamManager, mimetype string) (codec VideoCodec) { + switch mimetype { + case MimeMp4Video: + codec = VideoCodecLibX264 + if hwcodec := sm.encoder.hwCodecMP4Compatible(); hwcodec != nil && sm.config.GetTranscodeHardwareAcceleration() { + codec = *hwcodec + } + case MimeWebmVideo: + codec = VideoCodecVP9 + if hwcodec := sm.encoder.hwCodecWEBMCompatible(); hwcodec != nil && sm.config.GetTranscodeHardwareAcceleration() { + codec = *hwcodec + } + case MimeMkvVideo: + codec = VideoCodecCopy + } + + return codec +} + +func (o TranscodeOptions) makeStreamArgs(sm *StreamManager) Args { + maxTranscodeSize := sm.config.GetMaxStreamingTranscodeSize().GetMaxResolution() + if o.Resolution != "" { + maxTranscodeSize = models.StreamingResolutionEnum(o.Resolution).GetMaxResolution() + } + extraInputArgs := sm.config.GetLiveTranscodeInputArgs() + extraOutputArgs := sm.config.GetLiveTranscodeOutputArgs() + + args := Args{"-hide_banner"} + args = args.LogLevel(LogLevelError) + + codec := FileGetCodec(sm, o.StreamType.MimeType) + + args = sm.encoder.hwDeviceInit(args, codec) + args = append(args, extraInputArgs...) + + if o.StartTime != 0 { + args = args.Seek(o.StartTime) + } + + args = args.Input(o.VideoFile.Path) + + videoOnly := ProbeAudioCodec(o.VideoFile.AudioCodec) == MissingUnsupported + + videoFilter := sm.encoder.hwMaxResFilter(codec, o.VideoFile.Width, o.VideoFile.Height, maxTranscodeSize) + + args = append(args, o.StreamType.Args(codec, videoFilter, videoOnly)...) + + args = append(args, extraOutputArgs...) + + args = args.Output("pipe:") + + return args +} + +func (sm *StreamManager) ServeTranscode(w http.ResponseWriter, r *http.Request, options TranscodeOptions) { + streamRequestCtx := NewStreamRequestContext(w, r) + lockCtx := sm.lockManager.ReadLock(streamRequestCtx, options.VideoFile.Path) + + // hijacking and closing the connection here causes video playback to hang in Chrome + // due to ERR_INCOMPLETE_CHUNKED_ENCODING + // We trust that the request context will be closed, so we don't need to call Cancel on the returned context here. + + handler, err := sm.getTranscodeStream(lockCtx, options) + + if err != nil { + logger.Errorf("[transcode] error transcoding video file: %v", err) + w.WriteHeader(http.StatusBadRequest) + if _, err := w.Write([]byte(err.Error())); err != nil { + logger.Warnf("[transcode] error writing response: %v", err) + } + return + } + + handler(w, r) +} + +func (sm *StreamManager) getTranscodeStream(ctx *fsutil.LockContext, options TranscodeOptions) (http.HandlerFunc, error) { + args := options.makeStreamArgs(sm) + cmd := sm.encoder.Command(ctx, args) + + stdout, err := cmd.StdoutPipe() + if nil != err { + logger.Errorf("[transcode] ffmpeg stdout not available: %v", err) + return nil, err + } + + stderr, err := cmd.StderrPipe() + if nil != err { + logger.Errorf("[transcode] ffmpeg stderr not available: %v", err) + return nil, err + } + + if err = cmd.Start(); err != nil { + return nil, err + } + ctx.AttachCommand(cmd) + + // stderr must be consumed or the process deadlocks + go func() { + errStr, _ := io.ReadAll(stderr) + + errCmd := cmd.Wait() + + var err error + + e := string(errStr) + if e != "" { + err = errors.New(e) + } else { + err = errCmd + } + + // ignore ExitErrors, the process is always forcibly killed + var exitError *exec.ExitError + if err != nil && !errors.As(err, &exitError) { + logger.Errorf("[transcode] ffmpeg error when running command <%s>: %v", strings.Join(cmd.Args, " "), err) + } + }() + + mimeType := options.StreamType.MimeType + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", mimeType) + w.WriteHeader(http.StatusOK) + + // process killing should be handled by command context + + _, err := io.Copy(w, stdout) + if err != nil && !errors.Is(err, syscall.EPIPE) { + logger.Errorf("[transcode] error serving transcoded video file: %v", err) + } + + w.(http.Flusher).Flush() + } + return handler, nil +} diff --git a/pkg/ffmpeg/transcoder/transcode.go b/pkg/ffmpeg/transcoder/transcode.go index 8be24c540..3a2d6a554 100644 --- a/pkg/ffmpeg/transcoder/transcode.go +++ b/pkg/ffmpeg/transcoder/transcode.go @@ -21,6 +21,11 @@ type TranscodeOptions struct { // Verbosity is the logging verbosity. Defaults to LogLevelError if not set. Verbosity ffmpeg.LogLevel + + // arguments added before the input argument + ExtraInputArgs []string + // arguments added before the output argument + ExtraOutputArgs []string } func (o *TranscodeOptions) setDefaults() { @@ -59,6 +64,7 @@ func Transcode(input string, options TranscodeOptions) ffmpeg.Args { var args ffmpeg.Args args = args.LogLevel(options.Verbosity).Overwrite() + args = append(args, options.ExtraInputArgs...) if options.XError { args = args.XError() @@ -92,6 +98,8 @@ func Transcode(input string, options TranscodeOptions) ffmpeg.Args { } args = args.AppendArgs(options.AudioArgs) + args = append(args, options.ExtraOutputArgs...) + args = args.Format(options.Format) args = args.Output(options.OutputPath) diff --git a/pkg/file/clean.go b/pkg/file/clean.go index cc8ebde6b..44470c5a0 100644 --- a/pkg/file/clean.go +++ b/pkg/file/clean.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "io/fs" + "os" + "path/filepath" "github.com/stashapp/stash/pkg/job" "github.com/stashapp/stash/pkg/logger" @@ -351,6 +353,22 @@ func (j *cleanJob) shouldCleanFolder(ctx context.Context, f *Folder) bool { return true } + // #3261 - handle symlinks + if info.Mode()&os.ModeSymlink == os.ModeSymlink { + finalPath, err := filepath.EvalSymlinks(path) + if err != nil { + // don't bail out if symlink is invalid + logger.Infof("Invalid symlink. Marking to clean: \"%s\"", path) + return true + } + + info, err = j.FS.Lstat(finalPath) + if err != nil { + logger.Errorf("error getting file info for %q (-> %s), not cleaning: %v", path, finalPath, err) + return false + } + } + // run through path filter, if returns false then the file should be cleaned filter := j.options.PathFilter @@ -362,7 +380,7 @@ func (j *cleanJob) deleteFile(ctx context.Context, fileID ID, fn string) { // delete associated objects fileDeleter := NewDeleter() if err := txn.WithTxn(ctx, j.Repository, func(ctx context.Context) error { - fileDeleter.RegisterHooks(ctx, j.Repository) + fileDeleter.RegisterHooks(ctx) if err := j.fireHandlers(ctx, fileDeleter, fileID); err != nil { return err @@ -379,7 +397,7 @@ func (j *cleanJob) deleteFolder(ctx context.Context, folderID FolderID, fn strin // delete associated objects fileDeleter := NewDeleter() if err := txn.WithTxn(ctx, j.Repository, func(ctx context.Context) error { - fileDeleter.RegisterHooks(ctx, j.Repository) + fileDeleter.RegisterHooks(ctx) if err := j.fireFolderHandlers(ctx, fileDeleter, folderID); err != nil { return err diff --git a/pkg/file/delete.go b/pkg/file/delete.go index c71d73428..9ee27c176 100644 --- a/pkg/file/delete.go +++ b/pkg/file/delete.go @@ -7,6 +7,7 @@ import ( "io/fs" "os" + "github.com/stashapp/stash/pkg/fsutil" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/txn" ) @@ -15,10 +16,10 @@ const deleteFileSuffix = ".delete" // RenamerRemover provides access to the Rename and Remove functions. type RenamerRemover interface { - Rename(oldpath, newpath string) error + Renamer Remove(name string) error RemoveAll(path string) error - Stat(name string) (fs.FileInfo, error) + Statter } type renamerRemoverImpl struct { @@ -44,6 +45,16 @@ func (r renamerRemoverImpl) Stat(path string) (fs.FileInfo, error) { return r.StatFn(path) } +func newRenamerRemoverImpl() renamerRemoverImpl { + return renamerRemoverImpl{ + // use fsutil.SafeMove to support cross-device moves + RenameFn: fsutil.SafeMove, + RemoveFn: os.Remove, + RemoveAllFn: os.RemoveAll, + StatFn: os.Stat, + } +} + // 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 @@ -59,25 +70,18 @@ type Deleter struct { func NewDeleter() *Deleter { return &Deleter{ - RenamerRemover: renamerRemoverImpl{ - RenameFn: os.Rename, - RemoveFn: os.Remove, - RemoveAllFn: os.RemoveAll, - StatFn: os.Stat, - }, + RenamerRemover: newRenamerRemoverImpl(), } } // RegisterHooks registers post-commit and post-rollback hooks. -func (d *Deleter) RegisterHooks(ctx context.Context, mgr txn.Manager) { - txn.AddPostCommitHook(ctx, func(ctx context.Context) error { +func (d *Deleter) RegisterHooks(ctx context.Context) { + txn.AddPostCommitHook(ctx, func(ctx context.Context) { d.Commit() - return nil }) - txn.AddPostRollbackHook(ctx, func(ctx context.Context) error { + txn.AddPostRollbackHook(ctx, func(ctx context.Context) { d.Rollback() - return nil }) } diff --git a/pkg/file/file.go b/pkg/file/file.go index 525c0f329..445edba9e 100644 --- a/pkg/file/file.go +++ b/pkg/file/file.go @@ -156,6 +156,7 @@ type Finder interface { type Getter interface { Finder FindByPath(ctx context.Context, path string) (File, error) + FindAllByPath(ctx context.Context, path string) ([]File, error) FindByFingerprint(ctx context.Context, fp Fingerprint) ([]File, error) FindByZipFileID(ctx context.Context, zipFileID ID) ([]File, error) FindAllInPaths(ctx context.Context, p []string, limit, offset int) ([]File, error) @@ -179,6 +180,11 @@ type Destroyer interface { Destroy(ctx context.Context, id ID) error } +type GetterUpdater interface { + Getter + Updater +} + type GetterDestroyer interface { Getter Destroyer diff --git a/pkg/file/folder.go b/pkg/file/folder.go index 2eb4edd12..719d1a1f9 100644 --- a/pkg/file/folder.go +++ b/pkg/file/folder.go @@ -2,7 +2,9 @@ package file import ( "context" + "fmt" "io/fs" + "path/filepath" "strconv" "time" ) @@ -30,9 +32,19 @@ func (f *Folder) Info(fs FS) (fs.FileInfo, error) { return f.info(fs, f.Path) } +type FolderFinder interface { + Find(ctx context.Context, id FolderID) (*Folder, error) +} + +// FolderPathFinder finds Folders by their path. +type FolderPathFinder interface { + FindByPath(ctx context.Context, path string) (*Folder, error) +} + // FolderGetter provides methods to find Folders. type FolderGetter interface { - FindByPath(ctx context.Context, path string) (*Folder, error) + FolderFinder + FolderPathFinder FindByZipFileID(ctx context.Context, zipFileID ID) ([]*Folder, error) FindAllInPaths(ctx context.Context, p []string, limit, offset int) ([]*Folder, error) FindByParentFolderID(ctx context.Context, parentFolderID FolderID) ([]*Folder, error) @@ -47,6 +59,11 @@ type FolderCreator interface { Create(ctx context.Context, f *Folder) error } +type FolderFinderCreator interface { + FolderPathFinder + FolderCreator +} + // FolderUpdater provides methods to update Folders. type FolderUpdater interface { Update(ctx context.Context, f *Folder) error @@ -69,3 +86,39 @@ type FolderStore interface { FolderUpdater FolderDestroyer } + +// GetOrCreateFolderHierarchy gets the folder for the given path, or creates a folder hierarchy for the given path if one if no existing folder is found. +// Does not create any folders in the file system +func GetOrCreateFolderHierarchy(ctx context.Context, fc FolderFinderCreator, path string) (*Folder, error) { + // get or create folder hierarchy + folder, err := fc.FindByPath(ctx, path) + if err != nil { + return nil, err + } + + if folder == nil { + parentPath := filepath.Dir(path) + parent, err := GetOrCreateFolderHierarchy(ctx, fc, parentPath) + if err != nil { + return nil, err + } + + now := time.Now() + + folder = &Folder{ + Path: path, + ParentFolderID: &parent.ID, + DirEntry: DirEntry{ + // leave mod time empty for now - it will be updated when the folder is scanned + }, + CreatedAt: now, + UpdatedAt: now, + } + + if err = fc.Create(ctx, folder); err != nil { + return nil, fmt.Errorf("creating folder %s: %w", path, err) + } + } + + return folder, nil +} diff --git a/pkg/file/fs.go b/pkg/file/fs.go index 0a24aaa53..09c7c7c8e 100644 --- a/pkg/file/fs.go +++ b/pkg/file/fs.go @@ -34,6 +34,26 @@ type FS interface { // OsFS is a file system backed by the OS. type OsFS struct{} +func (f *OsFS) Create(name string) (*os.File, error) { + return os.Create(name) +} + +func (f *OsFS) MkdirAll(path string, perm fs.FileMode) error { + return os.MkdirAll(path, perm) +} + +func (f *OsFS) Remove(name string) error { + return os.Remove(name) +} + +func (f *OsFS) Rename(oldpath, newpath string) error { + return os.Rename(oldpath, newpath) +} + +func (f *OsFS) RemoveAll(path string) error { + return os.RemoveAll(path) +} + func (f *OsFS) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) } diff --git a/pkg/file/move.go b/pkg/file/move.go new file mode 100644 index 000000000..3e29e328c --- /dev/null +++ b/pkg/file/move.go @@ -0,0 +1,256 @@ +package file + +import ( + "context" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + "time" + + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/txn" +) + +type Renamer interface { + Rename(oldpath, newpath string) error +} + +type Statter interface { + Stat(name string) (fs.FileInfo, error) +} + +type DirMakerStatRenamer interface { + Statter + Renamer + Mkdir(name string, perm os.FileMode) error + Remove(name string) error +} + +type folderCreatorStatRenamerImpl struct { + renamerRemoverImpl + mkDirFn func(name string, perm os.FileMode) error +} + +func (r folderCreatorStatRenamerImpl) Mkdir(name string, perm os.FileMode) error { + return r.mkDirFn(name, perm) +} + +type Mover struct { + Renamer DirMakerStatRenamer + Files GetterUpdater + Folders FolderStore + + moved map[string]string + foldersCreated []string +} + +func NewMover(fileStore GetterUpdater, folderStore FolderStore) *Mover { + return &Mover{ + Files: fileStore, + Folders: folderStore, + Renamer: &folderCreatorStatRenamerImpl{ + renamerRemoverImpl: newRenamerRemoverImpl(), + mkDirFn: os.Mkdir, + }, + } +} + +// Move moves the file to the given folder and basename. If basename is empty, then the existing basename is used. +// Assumes that the parent folder exists in the filesystem. +func (m *Mover) Move(ctx context.Context, f File, folder *Folder, basename string) error { + fBase := f.Base() + + // don't allow moving files in zip files + if fBase.ZipFileID != nil { + return fmt.Errorf("cannot move file %s, is in a zip file", fBase.Path) + } + + if basename == "" { + basename = fBase.Basename + } + + // modify the database first + + oldPath := fBase.Path + + if folder.ID == fBase.ParentFolderID && (basename == "" || basename == fBase.Basename) { + // nothing to do + return nil + } + + // ensure that the new path doesn't already exist + newPath := filepath.Join(folder.Path, basename) + if _, err := m.Renamer.Stat(newPath); !errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("file %s already exists", newPath) + } + + if err := m.transferZipFolderHierarchy(ctx, fBase.ID, oldPath, newPath); err != nil { + return fmt.Errorf("moving folder hierarchy for file %s: %w", fBase.Path, err) + } + + // move contained files if file is a zip file + zipFiles, err := m.Files.FindByZipFileID(ctx, fBase.ID) + if err != nil { + return fmt.Errorf("finding contained files in file %s: %w", fBase.Path, err) + } + for _, zf := range zipFiles { + zfBase := zf.Base() + oldZfPath := zfBase.Path + oldZfDir := filepath.Dir(oldZfPath) + + // sanity check - ignore files which aren't under oldPath + if !strings.HasPrefix(oldZfPath, oldPath) { + continue + } + + relZfDir, err := filepath.Rel(oldPath, oldZfDir) + if err != nil { + return fmt.Errorf("moving contained file %s: %w", zfBase.ID, err) + } + newZfDir := filepath.Join(newPath, relZfDir) + + // folder should have been created by moveZipFolderHierarchy + newZfFolder, err := GetOrCreateFolderHierarchy(ctx, m.Folders, newZfDir) + if err != nil { + return fmt.Errorf("getting or creating folder hierarchy: %w", err) + } + + // update file parent folder + zfBase.ParentFolderID = newZfFolder.ID + if err := m.Files.Update(ctx, zf); err != nil { + return fmt.Errorf("updating file %s: %w", oldZfPath, err) + } + } + + fBase.ParentFolderID = folder.ID + fBase.Basename = basename + fBase.UpdatedAt = time.Now() + // leave ModTime as is. It may or may not be changed by this operation + + if err := m.Files.Update(ctx, f); err != nil { + return fmt.Errorf("updating file %s: %w", oldPath, err) + } + + // then move the file + return m.moveFile(oldPath, newPath) +} + +func (m *Mover) CreateFolderHierarchy(path string) error { + info, err := m.Renamer.Stat(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + // create the parent folder + parentPath := filepath.Dir(path) + if err := m.CreateFolderHierarchy(parentPath); err != nil { + return err + } + + // create the folder + if err := m.Renamer.Mkdir(path, 0755); err != nil { + return fmt.Errorf("creating folder %s: %w", path, err) + } + + m.foldersCreated = append(m.foldersCreated, path) + } else { + return fmt.Errorf("getting info for %s: %w", path, err) + } + } else { + if !info.IsDir() { + return fmt.Errorf("%s is not a directory", path) + } + } + + return nil +} + +// transferZipFolderHierarchy creates the folder hierarchy for zipFileID under newPath, and removes +// ZipFileID from folders under oldPath. +func (m *Mover) transferZipFolderHierarchy(ctx context.Context, zipFileID ID, oldPath string, newPath string) error { + zipFolders, err := m.Folders.FindByZipFileID(ctx, zipFileID) + if err != nil { + return err + } + + for _, oldFolder := range zipFolders { + oldZfPath := oldFolder.Path + + // sanity check - ignore folders which aren't under oldPath + if !strings.HasPrefix(oldZfPath, oldPath) { + continue + } + + relZfPath, err := filepath.Rel(oldPath, oldZfPath) + if err != nil { + return err + } + newZfPath := filepath.Join(newPath, relZfPath) + + newFolder, err := GetOrCreateFolderHierarchy(ctx, m.Folders, newZfPath) + if err != nil { + return err + } + + // add ZipFileID to new folder + newFolder.ZipFileID = &zipFileID + if err = m.Folders.Update(ctx, newFolder); err != nil { + return err + } + + // remove ZipFileID from old folder + oldFolder.ZipFileID = nil + if err = m.Folders.Update(ctx, oldFolder); err != nil { + return err + } + } + + return nil +} + +func (m *Mover) moveFile(oldPath, newPath string) error { + if err := m.Renamer.Rename(oldPath, newPath); err != nil { + return fmt.Errorf("renaming file %s to %s: %w", oldPath, newPath, err) + } + + if m.moved == nil { + m.moved = make(map[string]string) + } + + m.moved[newPath] = oldPath + + return nil +} + +func (m *Mover) RegisterHooks(ctx context.Context, mgr txn.Manager) { + txn.AddPostCommitHook(ctx, func(ctx context.Context) { + m.commit() + }) + + txn.AddPostRollbackHook(ctx, func(ctx context.Context) { + m.rollback() + }) +} + +func (m *Mover) commit() { + m.moved = nil + m.foldersCreated = nil +} + +func (m *Mover) rollback() { + // move files back to their original location + for newPath, oldPath := range m.moved { + if err := m.Renamer.Rename(newPath, oldPath); err != nil { + logger.Errorf("error moving file %s back to %s: %s", newPath, oldPath, err.Error()) + } + } + + // remove folders created in reverse order + for i := len(m.foldersCreated) - 1; i >= 0; i-- { + folder := m.foldersCreated[i] + if err := m.Renamer.Remove(folder); err != nil { + logger.Errorf("error removing folder %s: %s", folder, err.Error()) + } + } +} diff --git a/pkg/file/scan.go b/pkg/file/scan.go index 31cd50af6..148f18691 100644 --- a/pkg/file/scan.go +++ b/pkg/file/scan.go @@ -14,6 +14,7 @@ import ( "github.com/remeh/sizedwaitgroup" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/txn" + "github.com/stashapp/stash/pkg/utils" ) const ( @@ -507,12 +508,11 @@ func (s *scanJob) onNewFolder(ctx context.Context, file scanFile) (*Folder, erro } } - txn.AddPostCommitHook(ctx, func(ctx context.Context) error { + txn.AddPostCommitHook(ctx, func(ctx context.Context) { // log at the end so that if anything fails above due to a locked database // error and the transaction must be retried, then we shouldn't get multiple // logs of the same thing. logger.Infof("%s doesn't exist. Creating new folder entry...", file.Path) - return nil }) if err := s.Repository.FolderStore.Create(ctx, toCreate); err != nil { @@ -574,7 +574,7 @@ func (s *scanJob) handleFile(ctx context.Context, f scanFile) error { // scan zip files with a different context that is not cancellable // cancelling while scanning zip file contents results in the scan // contents being partially completed - zipCtx := context.Background() + zipCtx := utils.ValueOnlyContext{Context: ctx} if err := s.scanZipFile(zipCtx, f); err != nil { logger.Errorf("Error scanning zip file %q: %v", f.Path, err) diff --git a/pkg/file/video/caption.go b/pkg/file/video/caption.go index 8c10d0d1c..d2f8e79a5 100644 --- a/pkg/file/video/caption.go +++ b/pkg/file/video/caption.go @@ -2,6 +2,7 @@ package video import ( "context" + "errors" "fmt" "os" "path/filepath" @@ -59,23 +60,6 @@ func IsLangInCaptions(lang string, ext string, captions []*models.VideoCaption) return false } -// CleanCaptions removes non existent/accessible language codes from captions -func CleanCaptions(scenePath string, captions []*models.VideoCaption) (cleanedCaptions []*models.VideoCaption, changed bool) { - changed = false - for _, caption := range captions { - found := false - f := caption.Path(scenePath) - if _, er := os.Stat(f); er == nil { - cleanedCaptions = append(cleanedCaptions, caption) - found = true - } - if !found { - changed = true - } - } - return -} - // getCaptionPrefix returns the prefix used to search for video files for the provided caption path func getCaptionPrefix(captionPath string) string { basename := strings.TrimSuffix(captionPath, filepath.Ext(captionPath)) // caption filename without the extension @@ -114,13 +98,22 @@ 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 - f, er := fqb.FindByPath(ctx, captionPrefix+"*") + files, er := fqb.FindAllByPath(ctx, captionPrefix+"*") if er != nil { return fmt.Errorf("searching for scene %s: %w", captionPrefix, er) } - if f != nil { // found related Scene + for _, f := range files { + // found some files + // filter out non video files + switch f.(type) { + case *file.VideoFile: + break + default: + continue + } + fileID := f.Base().ID path := f.Base().Path @@ -148,3 +141,52 @@ func AssociateCaptions(ctx context.Context, captionPath string, txnMgr txn.Manag logger.Error(err.Error()) } } + +// CleanCaptions removes non existent/accessible language codes from captions +func CleanCaptions(ctx context.Context, f *file.VideoFile, txnMgr txn.Manager, w CaptionUpdater) error { + captions, err := w.GetCaptions(ctx, f.ID) + if err != nil { + return fmt.Errorf("getting captions for file %s: %w", f.Path, err) + } + + if len(captions) == 0 { + return nil + } + + filePath := f.Path + + changed := false + var newCaptions []*models.VideoCaption + + for _, caption := range captions { + captionPath := caption.Path(filePath) + _, err := os.Stat(captionPath) + if errors.Is(err, os.ErrNotExist) { + logger.Infof("Removing non existent caption %s for %s", caption.Filename, f.Path) + changed = true + } else { + // other errors are ignored for the purposes of cleaning + newCaptions = append(newCaptions, caption) + } + } + + if changed { + fn := func(ctx context.Context) error { + return w.UpdateCaptions(ctx, f.ID, newCaptions) + } + + // possible that we are already in a transaction and txnMgr is nil + // in that case just call the function directly + if txnMgr == nil { + err = fn(ctx) + } else { + err = txn.WithTxn(ctx, txnMgr, fn) + } + + if err != nil { + return fmt.Errorf("updating captions for file %s: %w", f.Path, err) + } + } + + return nil +} diff --git a/pkg/file/walk.go b/pkg/file/walk.go index 8c7fdc5c9..a73781d45 100644 --- a/pkg/file/walk.go +++ b/pkg/file/walk.go @@ -125,7 +125,12 @@ func walkDir(f FS, path string, d fs.DirEntry, walkDirFn fs.WalkDirFunc) error { } for _, d1 := range dirs { - path1 := filepath.Join(path, d1.Name()) + name := d1.Name() + // Prevent infinite loops; this can happen with certain FS implementations (e.g. ZipFS). + if name == "" || name == "." { + continue + } + path1 := filepath.Join(path, name) if err := walkDir(f, path1, d1, walkDirFn); err != nil { if errors.Is(err, fs.SkipDir) { break diff --git a/pkg/file/zip.go b/pkg/file/zip.go index 1e61d340e..5cef1184e 100644 --- a/pkg/file/zip.go +++ b/pkg/file/zip.go @@ -2,11 +2,18 @@ package file import ( "archive/zip" + "bytes" "errors" "fmt" "io" "io/fs" "path/filepath" + + "github.com/stashapp/stash/pkg/logger" + "github.com/xWTF/chardet" + + "golang.org/x/net/html/charset" + "golang.org/x/text/transform" ) var ( @@ -40,6 +47,44 @@ func newZipFS(fs FS, path string, info fs.FileInfo) (*ZipFS, error) { return nil, err } + // Concat all Name and Comment for better detection result + var buffer bytes.Buffer + for _, f := range zipReader.File { + buffer.WriteString(f.Name) + buffer.WriteString(f.Comment) + } + buffer.WriteString(zipReader.Comment) + + // Detect encoding + d, err := chardet.NewTextDetector().DetectBest(buffer.Bytes()) + if err != nil { + // If we can't detect the encoding, just assume it's UTF8 + logger.Warnf("Unable to detect decoding for %s: %w", path, err) + } + + // If the charset is not UTF8, decode'em + if d != nil && d.Charset != "UTF-8" { + logger.Debugf("Detected non-utf8 zip charset %s (%s): %s", d.Charset, d.Language, path) + + e, _ := charset.Lookup(d.Charset) + if e == nil { + // if we can't find the encoding, just assume it's UTF8 + logger.Warnf("Failed to lookup charset %s, language %s", d.Charset, d.Language) + } else { + decoder := e.NewDecoder() + for _, f := range zipReader.File { + newName, _, err := transform.String(decoder, f.Name) + if err != nil { + reader.Close() + logger.Warnf("Failed to decode %v: %v", []byte(f.Name), err) + } else { + f.Name = newName + } + // Comments are not decoded cuz stash doesn't use that + } + } + } + return &ZipFS{ Reader: zipReader, zipFileCloser: reader, diff --git a/pkg/fsutil/file.go b/pkg/fsutil/file.go index 60a50d74f..7d91679fe 100644 --- a/pkg/fsutil/file.go +++ b/pkg/fsutil/file.go @@ -7,6 +7,8 @@ import ( "path/filepath" "regexp" "strings" + + "github.com/stashapp/stash/pkg/logger" ) // SafeMove attempts to move the file with path src to dest using os.Rename. If this fails, then it copies src to dest, then deletes src. @@ -38,7 +40,7 @@ func SafeMove(src, dst string) error { err = os.Remove(src) if err != nil { - return err + logger.Errorf("error removing old file %s during SafeMove: %v", src, err) } } diff --git a/pkg/gallery/chapter_import.go b/pkg/gallery/chapter_import.go new file mode 100644 index 000000000..e9b195ac5 --- /dev/null +++ b/pkg/gallery/chapter_import.go @@ -0,0 +1,83 @@ +package gallery + +import ( + "context" + "database/sql" + "fmt" + + "github.com/stashapp/stash/pkg/models" + "github.com/stashapp/stash/pkg/models/jsonschema" +) + +type ChapterCreatorUpdater interface { + Create(ctx context.Context, newGalleryChapter models.GalleryChapter) (*models.GalleryChapter, error) + Update(ctx context.Context, updatedGalleryChapter models.GalleryChapter) (*models.GalleryChapter, error) + FindByGalleryID(ctx context.Context, galleryID int) ([]*models.GalleryChapter, error) +} + +type ChapterImporter struct { + GalleryID int + ReaderWriter ChapterCreatorUpdater + Input jsonschema.GalleryChapter + MissingRefBehaviour models.ImportMissingRefEnum + + chapter models.GalleryChapter +} + +func (i *ChapterImporter) PreImport(ctx context.Context) error { + i.chapter = models.GalleryChapter{ + Title: i.Input.Title, + ImageIndex: i.Input.ImageIndex, + GalleryID: sql.NullInt64{Int64: int64(i.GalleryID), Valid: true}, + CreatedAt: models.SQLiteTimestamp{Timestamp: i.Input.CreatedAt.GetTime()}, + UpdatedAt: models.SQLiteTimestamp{Timestamp: i.Input.UpdatedAt.GetTime()}, + } + + return nil +} + +func (i *ChapterImporter) Name() string { + return fmt.Sprintf("%s (%d)", i.Input.Title, i.Input.ImageIndex) +} + +func (i *ChapterImporter) PostImport(ctx context.Context, id int) error { + return nil +} + +func (i *ChapterImporter) FindExistingID(ctx context.Context) (*int, error) { + existingChapters, err := i.ReaderWriter.FindByGalleryID(ctx, i.GalleryID) + + if err != nil { + return nil, err + } + + for _, m := range existingChapters { + if m.ImageIndex == i.chapter.ImageIndex { + id := m.ID + return &id, nil + } + } + + return nil, nil +} + +func (i *ChapterImporter) Create(ctx context.Context) (*int, error) { + created, err := i.ReaderWriter.Create(ctx, i.chapter) + if err != nil { + return nil, fmt.Errorf("error creating chapter: %v", err) + } + + id := created.ID + return &id, nil +} + +func (i *ChapterImporter) Update(ctx context.Context, id int) error { + chapter := i.chapter + chapter.ID = id + _, err := i.ReaderWriter.Update(ctx, chapter) + if err != nil { + return fmt.Errorf("error updating existing chapter: %v", err) + } + + return nil +} diff --git a/pkg/gallery/delete.go b/pkg/gallery/delete.go index b6c1333ba..60aee0d28 100644 --- a/pkg/gallery/delete.go +++ b/pkg/gallery/delete.go @@ -11,6 +11,8 @@ import ( func (s *Service) Destroy(ctx context.Context, i *models.Gallery, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile 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) if err != nil { @@ -39,6 +41,15 @@ func (s *Service) Destroy(ctx context.Context, i *models.Gallery, fileDeleter *i return imgsDestroyed, nil } +type ChapterDestroyer interface { + FindByGalleryID(ctx context.Context, galleryID int) ([]*models.GalleryChapter, error) + Destroy(ctx context.Context, id int) error +} + +func DestroyChapter(ctx context.Context, galleryChapter *models.GalleryChapter, qb ChapterDestroyer) error { + 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) { if err := i.LoadFiles(ctx, s.Repository); err != nil { return nil, err diff --git a/pkg/gallery/export.go b/pkg/gallery/export.go index ebd8a8604..4797d4135 100644 --- a/pkg/gallery/export.go +++ b/pkg/gallery/export.go @@ -2,6 +2,7 @@ package gallery import ( "context" + "fmt" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/json" @@ -9,6 +10,10 @@ import ( "github.com/stashapp/stash/pkg/studio" ) +type ChapterFinder interface { + FindByGalleryID(ctx context.Context, galleryID int) ([]*models.GalleryChapter, error) +} + // ToBasicJSON converts a gallery object into its JSON object equivalent. It // does not convert the relationships to other objects. func ToBasicJSON(gallery *models.Gallery) (*jsonschema.Gallery, error) { @@ -58,6 +63,30 @@ func GetStudioName(ctx context.Context, reader studio.Finder, gallery *models.Ga return "", nil } +// GetGalleryChaptersJSON returns a slice of GalleryChapter JSON representation +// objects corresponding to the provided gallery's chapters. +func GetGalleryChaptersJSON(ctx context.Context, chapterReader ChapterFinder, gallery *models.Gallery) ([]jsonschema.GalleryChapter, error) { + galleryChapters, err := chapterReader.FindByGalleryID(ctx, gallery.ID) + if err != nil { + return nil, fmt.Errorf("error getting gallery chapters: %v", err) + } + + var results []jsonschema.GalleryChapter + + for _, galleryChapter := range galleryChapters { + galleryChapterJSON := jsonschema.GalleryChapter{ + Title: galleryChapter.Title, + ImageIndex: galleryChapter.ImageIndex, + CreatedAt: json.JSONTime{Time: galleryChapter.CreatedAt.Timestamp}, + UpdatedAt: json.JSONTime{Time: galleryChapter.UpdatedAt.Timestamp}, + } + + results = append(results, galleryChapterJSON) + } + + return results, nil +} + func GetIDs(galleries []*models.Gallery) []int { var results []int for _, gallery := range galleries { diff --git a/pkg/gallery/export_test.go b/pkg/gallery/export_test.go index 13f8227f4..a424e09b0 100644 --- a/pkg/gallery/export_test.go +++ b/pkg/gallery/export_test.go @@ -22,6 +22,9 @@ const ( errStudioID = 6 // noTagsID = 11 + noChaptersID = 7 + errChaptersID = 8 + errFindByChapterID = 9 ) var ( @@ -63,6 +66,19 @@ func createFullGallery(id int) models.Gallery { } } +func createEmptyGallery(id int) models.Gallery { + return models.Gallery{ + ID: id, + Files: models.NewRelatedFiles([]file.File{ + &file.BaseFile{ + Path: path, + }, + }), + CreatedAt: createTime, + UpdatedAt: updateTime, + } +} + func createFullJSONGallery() *jsonschema.Gallery { return &jsonschema.Gallery{ Title: title, @@ -168,3 +184,109 @@ func TestGetStudioName(t *testing.T) { mockStudioReader.AssertExpectations(t) } + +const ( + validChapterID1 = 1 + validChapterID2 = 2 + + chapterTitle1 = "chapterTitle1" + chapterTitle2 = "chapterTitle2" + + chapterImageIndex1 = 10 + chapterImageIndex2 = 50 +) + +type galleryChaptersTestScenario struct { + input models.Gallery + expected []jsonschema.GalleryChapter + err bool +} + +var getGalleryChaptersJSONScenarios = []galleryChaptersTestScenario{ + { + createEmptyGallery(galleryID), + []jsonschema.GalleryChapter{ + { + Title: chapterTitle1, + ImageIndex: chapterImageIndex1, + CreatedAt: json.JSONTime{ + Time: createTime, + }, + UpdatedAt: json.JSONTime{ + Time: updateTime, + }, + }, + { + Title: chapterTitle2, + ImageIndex: chapterImageIndex2, + CreatedAt: json.JSONTime{ + Time: createTime, + }, + UpdatedAt: json.JSONTime{ + Time: updateTime, + }, + }, + }, + false, + }, + { + createEmptyGallery(noChaptersID), + nil, + false, + }, + { + createEmptyGallery(errChaptersID), + nil, + true, + }, +} + +var validChapters = []*models.GalleryChapter{ + { + ID: validChapterID1, + Title: chapterTitle1, + ImageIndex: chapterImageIndex1, + CreatedAt: models.SQLiteTimestamp{ + Timestamp: createTime, + }, + UpdatedAt: models.SQLiteTimestamp{ + Timestamp: updateTime, + }, + }, + { + ID: validChapterID2, + Title: chapterTitle2, + ImageIndex: chapterImageIndex2, + CreatedAt: models.SQLiteTimestamp{ + Timestamp: createTime, + }, + UpdatedAt: models.SQLiteTimestamp{ + Timestamp: updateTime, + }, + }, +} + +func TestGetGalleryChaptersJSON(t *testing.T) { + mockChapterReader := &mocks.GalleryChapterReaderWriter{} + + chaptersErr := errors.New("error getting gallery chapters") + + mockChapterReader.On("FindByGalleryID", testCtx, galleryID).Return(validChapters, nil).Once() + mockChapterReader.On("FindByGalleryID", testCtx, noChaptersID).Return(nil, nil).Once() + mockChapterReader.On("FindByGalleryID", testCtx, errChaptersID).Return(nil, chaptersErr).Once() + + for i, s := range getGalleryChaptersJSONScenarios { + gallery := s.input + json, err := GetGalleryChaptersJSON(testCtx, mockChapterReader, &gallery) + + switch { + case !s.err && err != nil: + t.Errorf("[%d] unexpected error: %s", i, err.Error()) + case s.err && err == nil: + t.Errorf("[%d] expected error not returned", i) + default: + assert.Equal(t, s.expected, json, "[%d]", i) + } + } + +} diff --git a/pkg/gallery/import.go b/pkg/gallery/import.go index c3bd83527..753717d65 100644 --- a/pkg/gallery/import.go +++ b/pkg/gallery/import.go @@ -24,6 +24,7 @@ type Importer struct { Input jsonschema.Gallery MissingRefBehaviour models.ImportMissingRefEnum + ID int gallery models.Gallery } diff --git a/pkg/gallery/service.go b/pkg/gallery/service.go index 4698277b7..acf70763f 100644 --- a/pkg/gallery/service.go +++ b/pkg/gallery/service.go @@ -31,6 +31,13 @@ type ImageService interface { DestroyZipImages(ctx context.Context, zipFile file.File, fileDeleter *image.FileDeleter, deleteGenerated bool) ([]*models.Image, error) } +type ChapterRepository interface { + ChapterFinder + ChapterDestroyer + + Update(ctx context.Context, updatedObject models.GalleryChapter) (*models.GalleryChapter, error) +} + type Service struct { Repository Repository ImageFinder ImageFinder diff --git a/pkg/hash/oshash/oshash.go b/pkg/hash/oshash/oshash.go index 2c42afd2a..a5271dd31 100644 --- a/pkg/hash/oshash/oshash.go +++ b/pkg/hash/oshash/oshash.go @@ -46,15 +46,16 @@ func oshash(size int64, head []byte, tail []byte) (string, error) { return fmt.Sprintf("%016x", result), nil } -// FromFilePath calculates the hash reading from src. +// FromReader calculates the hash reading from src. func FromReader(src io.ReadSeeker, fileSize int64) (string, error) { - if fileSize <= 0 { - return "", fmt.Errorf("cannot calculate oshash for empty file (size %d)", fileSize) + if fileSize <= 8 { + return "", fmt.Errorf("cannot calculate oshash where size < 8 (%d)", fileSize) } fileChunkSize := chunkSize if fileSize < fileChunkSize { - fileChunkSize = fileSize + // Must be a multiple of 8. + fileChunkSize = (fileSize / 8) * 8 } head := make([]byte, fileChunkSize) @@ -67,7 +68,7 @@ func FromReader(src io.ReadSeeker, fileSize int64) (string, error) { } // seek to the end of the file - the chunk size - _, err = src.Seek(-fileChunkSize, 2) + _, err = src.Seek(-fileChunkSize, io.SeekEnd) if err != nil { return "", err } diff --git a/pkg/hash/oshash/oshash_internal_test.go b/pkg/hash/oshash/oshash_internal_test.go deleted file mode 100644 index 20cdf989e..000000000 --- a/pkg/hash/oshash/oshash_internal_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package oshash - -import ( - "math/rand" - "testing" -) - -// Note that the public API returns "" instead. -func TestOshashEmpty(t *testing.T) { - var size int64 - head := make([]byte, chunkSize) - tail := make([]byte, chunkSize) - want := "0000000000000000" - got, err := oshash(size, head, tail) - if err != nil { - t.Errorf("TestOshashEmpty: Error from oshash: %v", err) - } - if got != want { - t.Errorf("TestOshashEmpty: oshash(0, 0, 0) = %q; want %q", got, want) - } -} - -// As oshash sums byte values, causing collisions is trivial. -func TestOshashCollisions(t *testing.T) { - buf1 := []byte("this is dumb") - buf2 := []byte("dumb is this") - size := int64(len(buf1)) - head := make([]byte, chunkSize) - - tail1 := make([]byte, chunkSize) - copy(tail1[len(tail1)-len(buf1):], buf1) - hash1, err := oshash(size, head, tail1) - if err != nil { - t.Errorf("TestOshashCollisions: Error from oshash: %v", err) - } - - tail2 := make([]byte, chunkSize) - copy(tail2[len(tail2)-len(buf2):], buf2) - hash2, err := oshash(size, head, tail2) - if err != nil { - t.Errorf("TestOshashCollisions: Error from oshash: %v", err) - } - - if hash1 != hash2 { - t.Errorf("TestOshashCollisions: oshash(n, k, ... %v) =! oshash(n, k, ... %v)", buf1, buf2) - } -} - -func BenchmarkOsHash(b *testing.B) { - src := rand.NewSource(9999) - r := rand.New(src) - - size := int64(1234567890) - - head := make([]byte, 1024*64) - _, err := r.Read(head) - if err != nil { - b.Errorf("unable to generate head array: %v", err) - } - - tail := make([]byte, 1024*64) - _, err = r.Read(tail) - if err != nil { - b.Errorf("unable to generate tail array: %v", err) - } - - b.ResetTimer() - - for n := 0; n < b.N; n++ { - _, err := oshash(size, head, tail) - if err != nil { - b.Errorf("unexpected error: %v", err) - } - } -} diff --git a/pkg/hash/oshash/oshash_test.go b/pkg/hash/oshash/oshash_test.go new file mode 100644 index 000000000..9ef59b46d --- /dev/null +++ b/pkg/hash/oshash/oshash_test.go @@ -0,0 +1,111 @@ +package oshash + +import ( + "bytes" + "math/rand" + "testing" +) + +func BenchmarkOsHash(b *testing.B) { + src := rand.NewSource(9999) + r := rand.New(src) + + size := int64(1234567890) + + head := make([]byte, 1024*64) + _, err := r.Read(head) + if err != nil { + b.Errorf("unable to generate head array: %v", err) + } + + tail := make([]byte, 1024*64) + _, err = r.Read(tail) + if err != nil { + b.Errorf("unable to generate tail array: %v", err) + } + + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _, err := oshash(size, head, tail) + if err != nil { + b.Errorf("unexpected error: %v", err) + } + } +} + +func TestFromReader(t *testing.T) { + makeByteArray := func(base []byte, mag int) []byte { + ret := base + for i := 0; i < mag; i++ { + ret = append(ret, ret...) + } + return ret + } + + makeTailArray := func(base []byte, tail []byte) []byte { + ret := base + t := make([]byte, chunkSize) + copy(t[len(t)-len(tail):], tail) + ret = append(ret, t...) + return ret + } + + tests := []struct { + name string + data []byte + want string + wantErr bool + }{ + { + "empty", + []byte{}, + "", + true, + }, + { + "regular", + makeByteArray([]byte("this is a test"), 15), + "6a0eba04654d0b9b", + false, + }, + { + "< chunk size", + []byte("hello world"), + "d3e392dee38cd4df", + false, + }, + { + "< 8", + []byte("hello"), + "", + true, + }, + { + "identical #1", + makeTailArray(make([]byte, chunkSize), []byte("this is dumb")), + "d5d6ddd820756920", + false, + }, + { + "identical #2", + makeTailArray(make([]byte, chunkSize), []byte("dumb is this")), + "d5d6ddd820756920", + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := bytes.NewReader(tt.data) + + got, err := FromReader(r, int64(len(tt.data))) + if (err != nil) != tt.wantErr { + t.Errorf("FromReader() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("FromReader() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/hash/videophash/phash.go b/pkg/hash/videophash/phash.go index 8438d9553..0cbefc2ae 100644 --- a/pkg/hash/videophash/phash.go +++ b/pkg/hash/videophash/phash.go @@ -23,7 +23,7 @@ const ( rows = 5 ) -func Generate(encoder ffmpeg.FFMpeg, videoFile *file.VideoFile) (*uint64, error) { +func Generate(encoder *ffmpeg.FFMpeg, videoFile *file.VideoFile) (*uint64, error) { sprite, err := generateSprite(encoder, videoFile) if err != nil { return nil, err @@ -37,7 +37,7 @@ func Generate(encoder ffmpeg.FFMpeg, videoFile *file.VideoFile) (*uint64, error) return &hashValue, nil } -func generateSpriteScreenshot(encoder ffmpeg.FFMpeg, input string, t float64) (image.Image, error) { +func generateSpriteScreenshot(encoder *ffmpeg.FFMpeg, input string, t float64) (image.Image, error) { options := transcoder.ScreenshotOptions{ Width: screenshotSize, OutputPath: "-", @@ -76,7 +76,7 @@ func combineImages(images []image.Image) image.Image { return montage } -func generateSprite(encoder ffmpeg.FFMpeg, videoFile *file.VideoFile) (image.Image, error) { +func generateSprite(encoder *ffmpeg.FFMpeg, videoFile *file.VideoFile) (image.Image, error) { logger.Infof("[generator] generating phash sprite for %s", videoFile.Path) // Generate sprite image offset by 5% on each end to avoid intro/outros diff --git a/pkg/image/export.go b/pkg/image/export.go index afb811a80..fb6ad0fa0 100644 --- a/pkg/image/export.go +++ b/pkg/image/export.go @@ -15,6 +15,7 @@ import ( func ToBasicJSON(image *models.Image) *jsonschema.Image { newImageJSON := jsonschema.Image{ Title: image.Title, + URL: image.URL, CreatedAt: json.JSONTime{Time: image.CreatedAt}, UpdatedAt: json.JSONTime{Time: image.UpdatedAt}, } @@ -23,6 +24,10 @@ func ToBasicJSON(image *models.Image) *jsonschema.Image { newImageJSON.Rating = *image.Rating } + if image.Date != nil { + newImageJSON.Date = image.Date.String() + } + newImageJSON.Organized = image.Organized newImageJSON.OCounter = image.OCounter diff --git a/pkg/image/export_test.go b/pkg/image/export_test.go index 6350b7302..7f3393d6f 100644 --- a/pkg/image/export_test.go +++ b/pkg/image/export_test.go @@ -25,6 +25,9 @@ const ( var ( title = "title" rating = 5 + url = "http://a.com" + date = "2001-01-01" + dateObj = models.NewDate(date) organized = true ocounter = 2 ) @@ -52,6 +55,8 @@ func createFullImage(id int) models.Image { Title: title, OCounter: ocounter, Rating: &rating, + Date: &dateObj, + URL: url, Organized: organized, CreatedAt: createTime, UpdatedAt: updateTime, @@ -63,6 +68,8 @@ func createFullJSONImage() *jsonschema.Image { Title: title, OCounter: ocounter, Rating: rating, + Date: date, + URL: url, Organized: organized, Files: []string{path}, CreatedAt: json.JSONTime{ diff --git a/pkg/image/import.go b/pkg/image/import.go index 018dbf9fb..b5e54e594 100644 --- a/pkg/image/import.go +++ b/pkg/image/import.go @@ -85,6 +85,13 @@ func (i *Importer) imageJSONToImage(imageJSON jsonschema.Image) models.Image { if imageJSON.Rating != 0 { newImage.Rating = &imageJSON.Rating } + if imageJSON.URL != "" { + newImage.URL = imageJSON.URL + } + if imageJSON.Date != "" { + d := models.NewDate(imageJSON.Date) + newImage.Date = &d + } return newImage } diff --git a/pkg/image/query.go b/pkg/image/query.go index 45f1cb687..82125d65d 100644 --- a/pkg/image/query.go +++ b/pkg/image/query.go @@ -7,11 +7,6 @@ import ( "github.com/stashapp/stash/pkg/models" ) -const ( - coverFilename = "cover.jpg" - coverFilenameSearchString = "%" + coverFilename -) - type Queryer interface { Query(ctx context.Context, options models.ImageQueryOptions) (*models.ImageQueryResult, error) } @@ -102,9 +97,9 @@ func FindByGalleryID(ctx context.Context, r Queryer, galleryID int, sortBy strin }, &findFilter) } -func FindGalleryCover(ctx context.Context, r Queryer, galleryID int) (*models.Image, error) { +func FindGalleryCover(ctx context.Context, r Queryer, galleryID int, galleryCoverRegex string) (*models.Image, error) { const useCoverJpg = true - img, err := findGalleryCover(ctx, r, galleryID, useCoverJpg) + img, err := findGalleryCover(ctx, r, galleryID, useCoverJpg, galleryCoverRegex) if err != nil { return nil, err } @@ -114,10 +109,10 @@ func FindGalleryCover(ctx context.Context, r Queryer, galleryID int) (*models.Im } // return the first image in the gallery - return findGalleryCover(ctx, r, galleryID, !useCoverJpg) + return findGalleryCover(ctx, r, galleryID, !useCoverJpg, galleryCoverRegex) } -func findGalleryCover(ctx context.Context, r Queryer, galleryID int, useCoverJpg bool) (*models.Image, error) { +func findGalleryCover(ctx context.Context, r Queryer, galleryID int, useCoverJpg bool, galleryCoverRegex string) (*models.Image, error) { // try to find cover.jpg in the gallery perPage := 1 sortBy := "path" @@ -138,8 +133,8 @@ func findGalleryCover(ctx context.Context, r Queryer, galleryID int, useCoverJpg if useCoverJpg { imageFilter.Path = &models.StringCriterionInput{ - Value: coverFilenameSearchString, - Modifier: models.CriterionModifierEquals, + Value: "(?i)" + galleryCoverRegex, + Modifier: models.CriterionModifierMatchesRegex, } } diff --git a/pkg/image/scan.go b/pkg/image/scan.go index c8d02c26f..4c5280f6b 100644 --- a/pkg/image/scan.go +++ b/pkg/image/scan.go @@ -143,15 +143,13 @@ func (h *ScanHandler) Handle(ctx context.Context, f file.File, oldFile file.File if h.ScanConfig.IsGenerateThumbnails() { // do this after the commit so that the transaction isn't held up - txn.AddPostCommitHook(ctx, func(ctx context.Context) error { + txn.AddPostCommitHook(ctx, func(ctx context.Context) { for _, s := range existing { if err := h.ThumbnailGenerator.GenerateThumbnail(ctx, s, imageFile); err != nil { // just log if cover generation fails. We can try again on rescan logger.Errorf("Error generating thumbnail for %s: %v", imageFile.Path, err) } } - - return nil }) } diff --git a/pkg/image/thumbnail.go b/pkg/image/thumbnail.go index 9fc720a76..80c2139cc 100644 --- a/pkg/image/thumbnail.go +++ b/pkg/image/thumbnail.go @@ -32,7 +32,7 @@ type ThumbnailGenerator interface { } type ThumbnailEncoder struct { - ffmpeg ffmpeg.FFMpeg + ffmpeg *ffmpeg.FFMpeg vips *vipsEncoder } @@ -43,7 +43,7 @@ func GetVipsPath() string { return vipsPath } -func NewThumbnailEncoder(ffmpegEncoder ffmpeg.FFMpeg) ThumbnailEncoder { +func NewThumbnailEncoder(ffmpegEncoder *ffmpeg.FFMpeg) ThumbnailEncoder { ret := ThumbnailEncoder{ ffmpeg: ffmpegEncoder, } diff --git a/pkg/job/context.go b/pkg/job/context.go deleted file mode 100644 index e53625e23..000000000 --- a/pkg/job/context.go +++ /dev/null @@ -1,22 +0,0 @@ -package job - -import ( - "context" - "time" -) - -type valueOnlyContext struct { - context.Context -} - -func (valueOnlyContext) Deadline() (deadline time.Time, ok bool) { - return -} - -func (valueOnlyContext) Done() <-chan struct{} { - return nil -} - -func (valueOnlyContext) Err() error { - return nil -} diff --git a/pkg/job/manager.go b/pkg/job/manager.go index ce5fd4f9d..4ad9b1880 100644 --- a/pkg/job/manager.go +++ b/pkg/job/manager.go @@ -7,6 +7,7 @@ import ( "time" "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/utils" ) const maxGraveyardSize = 10 @@ -178,7 +179,7 @@ func (m *Manager) dispatch(ctx context.Context, j *Job) (done chan struct{}) { j.StartTime = &t j.Status = StatusRunning - ctx, cancelFunc := context.WithCancel(valueOnlyContext{ctx}) + ctx, cancelFunc := context.WithCancel(utils.ValueOnlyContext{Context: ctx}) j.cancelFunc = cancelFunc done = make(chan struct{}) diff --git a/pkg/match/path.go b/pkg/match/path.go index 68f0b7047..b4f202a5f 100644 --- a/pkg/match/path.go +++ b/pkg/match/path.go @@ -7,6 +7,7 @@ import ( "regexp" "strings" "unicode" + "unicode/utf8" "github.com/stashapp/stash/pkg/gallery" "github.com/stashapp/stash/pkg/image" @@ -30,6 +31,7 @@ var separatorRE = regexp.MustCompile(separatorPattern) type PerformerAutoTagQueryer interface { Query(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) QueryForAutoTag(ctx context.Context, words []string) ([]*models.Performer, error) + models.AliasLoader } type StudioAutoTagQueryer interface { @@ -76,7 +78,7 @@ func getPathWords(path string, trimExt bool) []string { // remove any single letter words var ret []string for _, w := range words { - if len(w) > 1 { + if utf8.RuneCountInString(w) > 1 { // #1450 - we need to open up the criteria for matching so that we // can match where path has no space between subject names - // ie name = "foo bar" - path = "foobar" @@ -168,8 +170,27 @@ func PathToPerformers(ctx context.Context, path string, reader PerformerAutoTagQ var ret []*models.Performer for _, p := range performers { - // TODO - commenting out alias handling until both sides work correctly - if nameMatchesPath(p.Name, path) != -1 { // || nameMatchesPath(p.Aliases.String, path) { + matches := false + if nameMatchesPath(p.Name, path) != -1 { + matches = true + } + + // TODO - disabled alias matching until we can get finer + // control over the matching + // if !matches { + // if err := p.LoadAliases(ctx, reader); err != nil { + // return nil, err + // } + + // for _, alias := range p.Aliases.List() { + // if nameMatchesPath(alias, path) != -1 { + // matches = true + // break + // } + // } + // } + + if matches { ret = append(ret, p) } } diff --git a/pkg/models/find_filter.go b/pkg/models/find_filter.go index f684ca065..9934a9ea9 100644 --- a/pkg/models/find_filter.go +++ b/pkg/models/find_filter.go @@ -96,15 +96,15 @@ func (ff FindFilterType) GetPage() int { func (ff FindFilterType) GetPageSize() int { const defaultPerPage = 25 const minPerPage = 0 - const maxPerPage = 1000 if ff.PerPage == nil { return defaultPerPage } - if *ff.PerPage > maxPerPage { - return maxPerPage - } else if *ff.PerPage < minPerPage { + // removed the maxPerPage check. We already all -1 to indicate all results + // so there is no conceivable reason we should limit the page size + + if *ff.PerPage < minPerPage { // negative page sizes should return all results // this is a sanity check in case GetPageSize is // called with a negative page size. diff --git a/pkg/models/gallery.go b/pkg/models/gallery.go index fa95cc559..61ee2a72d 100644 --- a/pkg/models/gallery.go +++ b/pkg/models/gallery.go @@ -31,6 +31,8 @@ type GalleryFilterType struct { Organized *bool `json:"organized"` // Filter by average image resolution AverageResolution *ResolutionCriterionInput `json:"average_resolution"` + // Filter to only include scenes which have chapters. `true` or `false` + HasChapters *string `json:"has_chapters"` // Filter to only include galleries with this studio Studios *HierarchicalMultiCriterionInput `json:"studios"` // Filter to only include galleries with these tags diff --git a/pkg/models/gallery_chapter.go b/pkg/models/gallery_chapter.go new file mode 100644 index 000000000..b0c2d2b8d --- /dev/null +++ b/pkg/models/gallery_chapter.go @@ -0,0 +1,20 @@ +package models + +import "context" + +type GalleryChapterReader interface { + Find(ctx context.Context, id int) (*GalleryChapter, error) + FindMany(ctx context.Context, ids []int) ([]*GalleryChapter, error) + FindByGalleryID(ctx context.Context, galleryID int) ([]*GalleryChapter, error) +} + +type GalleryChapterWriter interface { + Create(ctx context.Context, newGalleryChapter GalleryChapter) (*GalleryChapter, error) + Update(ctx context.Context, updatedGalleryChapter GalleryChapter) (*GalleryChapter, error) + Destroy(ctx context.Context, id int) error +} + +type GalleryChapterReaderWriter interface { + GalleryChapterReader + GalleryChapterWriter +} diff --git a/pkg/models/generate.go b/pkg/models/generate.go index 85685e078..2fc66248c 100644 --- a/pkg/models/generate.go +++ b/pkg/models/generate.go @@ -7,16 +7,17 @@ import ( ) type GenerateMetadataOptions struct { - Sprites *bool `json:"sprites"` - Previews *bool `json:"previews"` - ImagePreviews *bool `json:"imagePreviews"` + Covers bool `json:"covers"` + Sprites bool `json:"sprites"` + Previews bool `json:"previews"` + ImagePreviews bool `json:"imagePreviews"` PreviewOptions *GeneratePreviewOptions `json:"previewOptions"` - Markers *bool `json:"markers"` - MarkerImagePreviews *bool `json:"markerImagePreviews"` - MarkerScreenshots *bool `json:"markerScreenshots"` - Transcodes *bool `json:"transcodes"` - Phashes *bool `json:"phashes"` - InteractiveHeatmapsSpeeds *bool `json:"interactiveHeatmapsSpeeds"` + Markers bool `json:"markers"` + MarkerImagePreviews bool `json:"markerImagePreviews"` + MarkerScreenshots bool `json:"markerScreenshots"` + Transcodes bool `json:"transcodes"` + Phashes bool `json:"phashes"` + InteractiveHeatmapsSpeeds bool `json:"interactiveHeatmapsSpeeds"` } type GeneratePreviewOptions struct { diff --git a/pkg/models/image.go b/pkg/models/image.go index 2b908dcb6..774e0536a 100644 --- a/pkg/models/image.go +++ b/pkg/models/image.go @@ -18,6 +18,10 @@ type ImageFilterType struct { Rating *IntCriterionInput `json:"rating"` // Filter by rating expressed as 1-100 Rating100 *IntCriterionInput `json:"rating100"` + // Filter by date + Date *DateCriterionInput `json:"date"` + // Filter by url + URL *StringCriterionInput `json:"url"` // Filter by organized Organized *bool `json:"organized"` // Filter by o-counter diff --git a/pkg/models/jsonschema/gallery.go b/pkg/models/jsonschema/gallery.go index 596e7c610..ca399624e 100644 --- a/pkg/models/jsonschema/gallery.go +++ b/pkg/models/jsonschema/gallery.go @@ -10,22 +10,30 @@ import ( "github.com/stashapp/stash/pkg/models/json" ) -type Gallery struct { - ZipFiles []string `json:"zip_files,omitempty"` - FolderPath string `json:"folder_path,omitempty"` +type GalleryChapter struct { Title string `json:"title,omitempty"` - URL string `json:"url,omitempty"` - Date string `json:"date,omitempty"` - Details string `json:"details,omitempty"` - Rating int `json:"rating,omitempty"` - Organized bool `json:"organized,omitempty"` - Studio string `json:"studio,omitempty"` - Performers []string `json:"performers,omitempty"` - Tags []string `json:"tags,omitempty"` + ImageIndex int `json:"image_index,omitempty"` CreatedAt json.JSONTime `json:"created_at,omitempty"` UpdatedAt json.JSONTime `json:"updated_at,omitempty"` } +type Gallery struct { + ZipFiles []string `json:"zip_files,omitempty"` + FolderPath string `json:"folder_path,omitempty"` + Title string `json:"title,omitempty"` + URL string `json:"url,omitempty"` + Date string `json:"date,omitempty"` + Details string `json:"details,omitempty"` + Rating int `json:"rating,omitempty"` + Organized bool `json:"organized,omitempty"` + Chapters []GalleryChapter `json:"chapters,omitempty"` + Studio string `json:"studio,omitempty"` + Performers []string `json:"performers,omitempty"` + Tags []string `json:"tags,omitempty"` + CreatedAt json.JSONTime `json:"created_at,omitempty"` + UpdatedAt json.JSONTime `json:"updated_at,omitempty"` +} + func (s Gallery) Filename(basename string, hash string) string { ret := fsutil.SanitiseBasename(basename) diff --git a/pkg/models/jsonschema/image.go b/pkg/models/jsonschema/image.go index 364daa0cf..1862ffc82 100644 --- a/pkg/models/jsonschema/image.go +++ b/pkg/models/jsonschema/image.go @@ -13,6 +13,8 @@ type Image struct { Title string `json:"title,omitempty"` Studio string `json:"studio,omitempty"` Rating int `json:"rating,omitempty"` + URL string `json:"url,omitempty"` + Date string `json:"date,omitempty"` Organized bool `json:"organized,omitempty"` OCounter int `json:"o_counter,omitempty"` Galleries []GalleryRef `json:"galleries,omitempty"` diff --git a/pkg/models/jsonschema/performer.go b/pkg/models/jsonschema/performer.go index e4f5de2cb..c0996a1a5 100644 --- a/pkg/models/jsonschema/performer.go +++ b/pkg/models/jsonschema/performer.go @@ -2,63 +2,97 @@ package jsonschema import ( "fmt" + "io" "os" 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" + "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) +type StringOrStringList []string + +func (s *StringOrStringList) UnmarshalJSON(data []byte) error { + var stringList []string + var stringVal string + + err := jsoniter.Unmarshal(data, &stringList) + if err == nil { + *s = stringList + return nil + } + + err = jsoniter.Unmarshal(data, &stringVal) + if err == nil { + *s = stringslice.FromString(stringVal, ",") + return nil + } + + return err +} + type Performer struct { - Name string `json:"name,omitempty"` - Gender string `json:"gender,omitempty"` - URL string `json:"url,omitempty"` - Twitter string `json:"twitter,omitempty"` - Instagram string `json:"instagram,omitempty"` - Birthdate string `json:"birthdate,omitempty"` - Ethnicity string `json:"ethnicity,omitempty"` - Country string `json:"country,omitempty"` - EyeColor string `json:"eye_color,omitempty"` + Name string `json:"name,omitempty"` + Disambiguation string `json:"disambiguation,omitempty"` + Gender string `json:"gender,omitempty"` + URL string `json:"url,omitempty"` + Twitter string `json:"twitter,omitempty"` + Instagram string `json:"instagram,omitempty"` + Birthdate string `json:"birthdate,omitempty"` + Ethnicity string `json:"ethnicity,omitempty"` + Country string `json:"country,omitempty"` + EyeColor string `json:"eye_color,omitempty"` // this should be int, but keeping string for backwards compatibility - Height string `json:"height,omitempty"` - Measurements string `json:"measurements,omitempty"` - FakeTits string `json:"fake_tits,omitempty"` - CareerLength string `json:"career_length,omitempty"` - Tattoos string `json:"tattoos,omitempty"` - Piercings string `json:"piercings,omitempty"` - Aliases string `json:"aliases,omitempty"` - Favorite bool `json:"favorite,omitempty"` - Tags []string `json:"tags,omitempty"` - Image string `json:"image,omitempty"` - CreatedAt json.JSONTime `json:"created_at,omitempty"` - UpdatedAt json.JSONTime `json:"updated_at,omitempty"` - Rating int `json:"rating,omitempty"` - Details string `json:"details,omitempty"` - DeathDate string `json:"death_date,omitempty"` - HairColor string `json:"hair_color,omitempty"` - Weight int `json:"weight,omitempty"` - StashIDs []models.StashID `json:"stash_ids,omitempty"` - IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"` + Height string `json:"height,omitempty"` + Measurements string `json:"measurements,omitempty"` + FakeTits string `json:"fake_tits,omitempty"` + CareerLength string `json:"career_length,omitempty"` + Tattoos string `json:"tattoos,omitempty"` + Piercings string `json:"piercings,omitempty"` + Aliases StringOrStringList `json:"aliases,omitempty"` + Favorite bool `json:"favorite,omitempty"` + Tags []string `json:"tags,omitempty"` + Image string `json:"image,omitempty"` + CreatedAt json.JSONTime `json:"created_at,omitempty"` + UpdatedAt json.JSONTime `json:"updated_at,omitempty"` + Rating int `json:"rating,omitempty"` + Details string `json:"details,omitempty"` + DeathDate string `json:"death_date,omitempty"` + HairColor string `json:"hair_color,omitempty"` + Weight int `json:"weight,omitempty"` + StashIDs []models.StashID `json:"stash_ids,omitempty"` + IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"` } func (s Performer) Filename() string { - return fsutil.SanitiseBasename(s.Name) + ".json" + name := s.Name + if s.Disambiguation != "" { + name += "_" + s.Disambiguation + } + return fsutil.SanitiseBasename(name) + ".json" } func LoadPerformerFile(filePath string) (*Performer, error) { - var performer Performer file, err := os.Open(filePath) if err != nil { return nil, err } defer file.Close() + + return loadPerformer(file) +} + +func loadPerformer(r io.ReadSeeker) (*Performer, error) { var json = jsoniter.ConfigCompatibleWithStandardLibrary - jsonParser := json.NewDecoder(file) - err = jsonParser.Decode(&performer) - if err != nil { + jsonParser := json.NewDecoder(r) + + var performer Performer + if err := jsonParser.Decode(&performer); err != nil { return nil, err } + return &performer, nil } diff --git a/pkg/models/jsonschema/performer_test.go b/pkg/models/jsonschema/performer_test.go new file mode 100644 index 000000000..978fa9fd0 --- /dev/null +++ b/pkg/models/jsonschema/performer_test.go @@ -0,0 +1,52 @@ +package jsonschema + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_loadPerformer(t *testing.T) { + tests := []struct { + name string + input string + want Performer + wantErr bool + }{ + { + name: "alias list", + input: ` +{ + "aliases": ["alias1", "alias2"] +}`, + want: Performer{ + Aliases: []string{"alias1", "alias2"}, + }, + wantErr: false, + }, + { + name: "alias string list", + input: ` +{ + "aliases": "alias1, alias2" +}`, + want: Performer{ + Aliases: []string{"alias1", "alias2"}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := strings.NewReader(tt.input) + got, err := loadPerformer(r) + if (err != nil) != tt.wantErr { + t.Errorf("loadPerformer() error = %v, wantErr %v", err, tt.wantErr) + return + } + + assert.Equal(t, &tt.want, got) + }) + } +} diff --git a/pkg/models/mocks/GalleryChapterReaderWriter.go b/pkg/models/mocks/GalleryChapterReaderWriter.go new file mode 100644 index 000000000..8541d5b41 --- /dev/null +++ b/pkg/models/mocks/GalleryChapterReaderWriter.go @@ -0,0 +1,144 @@ +// Code generated by mockery v2.10.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + models "github.com/stashapp/stash/pkg/models" + mock "github.com/stretchr/testify/mock" +) + +// GalleryChapterReaderWriter is an autogenerated mock type for the GalleryChapterReaderWriter type +type GalleryChapterReaderWriter struct { + mock.Mock +} + +// Create provides a mock function with given fields: ctx, newGalleryChapter +func (_m *GalleryChapterReaderWriter) Create(ctx context.Context, newGalleryChapter models.GalleryChapter) (*models.GalleryChapter, error) { + ret := _m.Called(ctx, newGalleryChapter) + + var r0 *models.GalleryChapter + if rf, ok := ret.Get(0).(func(context.Context, models.GalleryChapter) *models.GalleryChapter); ok { + r0 = rf(ctx, newGalleryChapter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.GalleryChapter) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, models.GalleryChapter) error); ok { + r1 = rf(ctx, newGalleryChapter) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Destroy provides a mock function with given fields: ctx, id +func (_m *GalleryChapterReaderWriter) Destroy(ctx context.Context, id int) error { + ret := _m.Called(ctx, id) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Find provides a mock function with given fields: ctx, id +func (_m *GalleryChapterReaderWriter) Find(ctx context.Context, id int) (*models.GalleryChapter, error) { + ret := _m.Called(ctx, id) + + var r0 *models.GalleryChapter + if rf, ok := ret.Get(0).(func(context.Context, int) *models.GalleryChapter); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.GalleryChapter) + } + } + + 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 +} + +// FindByGalleryID provides a mock function with given fields: ctx, galleryID +func (_m *GalleryChapterReaderWriter) FindByGalleryID(ctx context.Context, galleryID int) ([]*models.GalleryChapter, error) { + ret := _m.Called(ctx, galleryID) + + var r0 []*models.GalleryChapter + if rf, ok := ret.Get(0).(func(context.Context, int) []*models.GalleryChapter); ok { + r0 = rf(ctx, galleryID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.GalleryChapter) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, galleryID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindMany provides a mock function with given fields: ctx, ids +func (_m *GalleryChapterReaderWriter) FindMany(ctx context.Context, ids []int) ([]*models.GalleryChapter, error) { + ret := _m.Called(ctx, ids) + + var r0 []*models.GalleryChapter + if rf, ok := ret.Get(0).(func(context.Context, []int) []*models.GalleryChapter); ok { + r0 = rf(ctx, ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.GalleryChapter) + } + } + + 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 +} + +// Update provides a mock function with given fields: ctx, updatedGalleryChapter +func (_m *GalleryChapterReaderWriter) Update(ctx context.Context, updatedGalleryChapter models.GalleryChapter) (*models.GalleryChapter, error) { + ret := _m.Called(ctx, updatedGalleryChapter) + + var r0 *models.GalleryChapter + if rf, ok := ret.Get(0).(func(context.Context, models.GalleryChapter) *models.GalleryChapter); ok { + r0 = rf(ctx, updatedGalleryChapter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.GalleryChapter) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, models.GalleryChapter) error); ok { + r1 = rf(ctx, updatedGalleryChapter) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/pkg/models/mocks/MovieReaderWriter.go b/pkg/models/mocks/MovieReaderWriter.go index c125fc7b1..3131d31d6 100644 --- a/pkg/models/mocks/MovieReaderWriter.go +++ b/pkg/models/mocks/MovieReaderWriter.go @@ -137,20 +137,6 @@ func (_m *MovieReaderWriter) Destroy(ctx context.Context, id int) error { return r0 } -// DestroyImages provides a mock function with given fields: ctx, movieID -func (_m *MovieReaderWriter) DestroyImages(ctx context.Context, movieID int) error { - ret := _m.Called(ctx, movieID) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int) error); ok { - r0 = rf(ctx, movieID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // Find provides a mock function with given fields: ctx, id func (_m *MovieReaderWriter) Find(ctx context.Context, id int) (*models.Movie, error) { ret := _m.Called(ctx, id) @@ -335,6 +321,27 @@ func (_m *MovieReaderWriter) GetFrontImage(ctx context.Context, movieID int) ([] return r0, r1 } +// HasBackImage provides a mock function with given fields: ctx, movieID +func (_m *MovieReaderWriter) HasBackImage(ctx context.Context, movieID int) (bool, error) { + ret := _m.Called(ctx, movieID) + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, int) bool); ok { + r0 = rf(ctx, movieID) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, movieID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Query provides a mock function with given fields: ctx, movieFilter, findFilter func (_m *MovieReaderWriter) Query(ctx context.Context, movieFilter *models.MovieFilterType, findFilter *models.FindFilterType) ([]*models.Movie, int, error) { ret := _m.Called(ctx, movieFilter, findFilter) @@ -388,6 +395,34 @@ func (_m *MovieReaderWriter) Update(ctx context.Context, updatedMovie models.Mov return r0, r1 } +// UpdateBackImage provides a mock function with given fields: ctx, movieID, backImage +func (_m *MovieReaderWriter) UpdateBackImage(ctx context.Context, movieID int, backImage []byte) error { + ret := _m.Called(ctx, movieID, backImage) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int, []byte) error); ok { + r0 = rf(ctx, movieID, backImage) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateFrontImage provides a mock function with given fields: ctx, movieID, frontImage +func (_m *MovieReaderWriter) UpdateFrontImage(ctx context.Context, movieID int, frontImage []byte) error { + ret := _m.Called(ctx, movieID, frontImage) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int, []byte) error); ok { + r0 = rf(ctx, movieID, frontImage) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // UpdateFull provides a mock function with given fields: ctx, updatedMovie func (_m *MovieReaderWriter) UpdateFull(ctx context.Context, updatedMovie models.Movie) (*models.Movie, error) { ret := _m.Called(ctx, updatedMovie) @@ -410,17 +445,3 @@ func (_m *MovieReaderWriter) UpdateFull(ctx context.Context, updatedMovie models return r0, r1 } - -// UpdateImages provides a mock function with given fields: ctx, movieID, frontImage, backImage -func (_m *MovieReaderWriter) UpdateImages(ctx context.Context, movieID int, frontImage []byte, backImage []byte) error { - ret := _m.Called(ctx, movieID, frontImage, backImage) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int, []byte, []byte) error); ok { - r0 = rf(ctx, movieID, frontImage, backImage) - } else { - r0 = ret.Error(0) - } - - return r0 -} diff --git a/pkg/models/mocks/PerformerReaderWriter.go b/pkg/models/mocks/PerformerReaderWriter.go index cf1d965fa..b579ab562 100644 --- a/pkg/models/mocks/PerformerReaderWriter.go +++ b/pkg/models/mocks/PerformerReaderWriter.go @@ -305,6 +305,29 @@ func (_m *PerformerReaderWriter) FindMany(ctx context.Context, ids []int) ([]*mo return r0, r1 } +// GetAliases provides a mock function with given fields: ctx, relatedID +func (_m *PerformerReaderWriter) GetAliases(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 +} + // GetImage provides a mock function with given fields: ctx, performerID func (_m *PerformerReaderWriter) GetImage(ctx context.Context, performerID int) ([]byte, error) { ret := _m.Called(ctx, performerID) @@ -351,13 +374,13 @@ func (_m *PerformerReaderWriter) GetStashIDs(ctx context.Context, relatedID int) return r0, r1 } -// GetTagIDs provides a mock function with given fields: ctx, performerID -func (_m *PerformerReaderWriter) GetTagIDs(ctx context.Context, performerID int) ([]int, error) { - ret := _m.Called(ctx, performerID) +// GetTagIDs provides a mock function with given fields: ctx, relatedID +func (_m *PerformerReaderWriter) GetTagIDs(ctx context.Context, relatedID int) ([]int, error) { + ret := _m.Called(ctx, relatedID) var r0 []int if rf, ok := ret.Get(0).(func(context.Context, int) []int); ok { - r0 = rf(ctx, performerID) + r0 = rf(ctx, relatedID) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]int) @@ -366,7 +389,7 @@ func (_m *PerformerReaderWriter) GetTagIDs(ctx context.Context, performerID int) var r1 error if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { - r1 = rf(ctx, performerID) + r1 = rf(ctx, relatedID) } else { r1 = ret.Error(1) } @@ -404,6 +427,27 @@ func (_m *PerformerReaderWriter) Query(ctx context.Context, performerFilter *mod return r0, r1, r2 } +// QueryCount provides a mock function with given fields: ctx, galleryFilter, findFilter +func (_m *PerformerReaderWriter) QueryCount(ctx context.Context, galleryFilter *models.PerformerFilterType, findFilter *models.FindFilterType) (int, error) { + ret := _m.Called(ctx, galleryFilter, findFilter) + + var r0 int + if rf, ok := ret.Get(0).(func(context.Context, *models.PerformerFilterType, *models.FindFilterType) int); ok { + r0 = rf(ctx, galleryFilter, findFilter) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *models.PerformerFilterType, *models.FindFilterType) error); ok { + r1 = rf(ctx, galleryFilter, findFilter) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // QueryForAutoTag provides a mock function with given fields: ctx, words func (_m *PerformerReaderWriter) QueryForAutoTag(ctx context.Context, words []string) ([]*models.Performer, error) { ret := _m.Called(ctx, words) @@ -477,31 +521,3 @@ func (_m *PerformerReaderWriter) UpdatePartial(ctx context.Context, id int, upda return r0, r1 } - -// UpdateStashIDs provides a mock function with given fields: ctx, performerID, stashIDs -func (_m *PerformerReaderWriter) UpdateStashIDs(ctx context.Context, performerID int, stashIDs []models.StashID) error { - ret := _m.Called(ctx, performerID, stashIDs) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int, []models.StashID) error); ok { - r0 = rf(ctx, performerID, stashIDs) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// UpdateTags provides a mock function with given fields: ctx, performerID, tagIDs -func (_m *PerformerReaderWriter) UpdateTags(ctx context.Context, performerID int, tagIDs []int) error { - ret := _m.Called(ctx, performerID, tagIDs) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int, []int) error); ok { - r0 = rf(ctx, performerID, tagIDs) - } else { - r0 = ret.Error(0) - } - - return r0 -} diff --git a/pkg/models/mocks/SceneMarkerReaderWriter.go b/pkg/models/mocks/SceneMarkerReaderWriter.go index 695a54391..ef6e9cc78 100644 --- a/pkg/models/mocks/SceneMarkerReaderWriter.go +++ b/pkg/models/mocks/SceneMarkerReaderWriter.go @@ -14,6 +14,50 @@ type SceneMarkerReaderWriter struct { mock.Mock } +// All provides a mock function with given fields: ctx +func (_m *SceneMarkerReaderWriter) All(ctx context.Context) ([]*models.SceneMarker, error) { + ret := _m.Called(ctx) + + var r0 []*models.SceneMarker + if rf, ok := ret.Get(0).(func(context.Context) []*models.SceneMarker); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.SceneMarker) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Count provides a mock function with given fields: ctx +func (_m *SceneMarkerReaderWriter) Count(ctx context.Context) (int, error) { + ret := _m.Called(ctx) + + var r0 int + if rf, ok := ret.Get(0).(func(context.Context) int); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CountByTagID provides a mock function with given fields: ctx, tagID func (_m *SceneMarkerReaderWriter) CountByTagID(ctx context.Context, tagID int) (int, error) { ret := _m.Called(ctx, tagID) diff --git a/pkg/models/mocks/SceneReaderWriter.go b/pkg/models/mocks/SceneReaderWriter.go index 74ad7dc4b..5f7191827 100644 --- a/pkg/models/mocks/SceneReaderWriter.go +++ b/pkg/models/mocks/SceneReaderWriter.go @@ -235,20 +235,6 @@ func (_m *SceneReaderWriter) Destroy(ctx context.Context, id int) error { return r0 } -// DestroyCover provides a mock function with given fields: ctx, sceneID -func (_m *SceneReaderWriter) DestroyCover(ctx context.Context, sceneID int) error { - ret := _m.Called(ctx, sceneID) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int) error); ok { - r0 = rf(ctx, sceneID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // Duration provides a mock function with given fields: ctx func (_m *SceneReaderWriter) Duration(ctx context.Context) (float64, error) { ret := _m.Called(ctx) @@ -638,20 +624,20 @@ func (_m *SceneReaderWriter) GetTagIDs(ctx context.Context, relatedID int) ([]in return r0, r1 } -// SaveActivity provides a mock function with given fields: ctx, id, resumeTime, playDuration -func (_m *SceneReaderWriter) SaveActivity(ctx context.Context, id int, resumeTime *float64, playDuration *float64) (bool, error) { - ret := _m.Called(ctx, id, resumeTime, playDuration) +// HasCover provides a mock function with given fields: ctx, sceneID +func (_m *SceneReaderWriter) HasCover(ctx context.Context, sceneID int) (bool, error) { + ret := _m.Called(ctx, sceneID) var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, int, *float64, *float64) bool); ok { - r0 = rf(ctx, id, resumeTime, playDuration) + if rf, ok := ret.Get(0).(func(context.Context, int) bool); ok { + r0 = rf(ctx, sceneID) } else { r0 = ret.Get(0).(bool) } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, int, *float64, *float64) error); ok { - r1 = rf(ctx, id, resumeTime, playDuration) + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, sceneID) } else { r1 = ret.Error(1) } @@ -659,8 +645,8 @@ func (_m *SceneReaderWriter) SaveActivity(ctx context.Context, id int, resumeTim return r0, r1 } -// IncrementWatchCount provides a mock function with given fields: ctx, id -func (_m *SceneReaderWriter) IncrementWatchCount(ctx context.Context, id int) (int, error) { +// IncrementOCounter provides a mock function with given fields: ctx, id +func (_m *SceneReaderWriter) IncrementOCounter(ctx context.Context, id int) (int, error) { ret := _m.Called(ctx, id) var r0 int @@ -680,8 +666,8 @@ func (_m *SceneReaderWriter) IncrementWatchCount(ctx context.Context, id int) (i return r0, r1 } -// IncrementOCounter provides a mock function with given fields: ctx, id -func (_m *SceneReaderWriter) IncrementOCounter(ctx context.Context, id int) (int, error) { +// IncrementWatchCount provides a mock function with given fields: ctx, id +func (_m *SceneReaderWriter) IncrementWatchCount(ctx context.Context, id int) (int, error) { ret := _m.Called(ctx, id) var r0 int @@ -745,6 +731,27 @@ func (_m *SceneReaderWriter) ResetOCounter(ctx context.Context, id int) (int, er return r0, r1 } +// SaveActivity provides a mock function with given fields: ctx, id, resumeTime, playDuration +func (_m *SceneReaderWriter) SaveActivity(ctx context.Context, id int, resumeTime *float64, playDuration *float64) (bool, error) { + ret := _m.Called(ctx, id, resumeTime, playDuration) + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, int, *float64, *float64) bool); ok { + r0 = rf(ctx, id, resumeTime, playDuration) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int, *float64, *float64) error); ok { + r1 = rf(ctx, id, resumeTime, playDuration) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Size provides a mock function with given fields: ctx func (_m *SceneReaderWriter) Size(ctx context.Context) (float64, error) { ret := _m.Called(ctx) diff --git a/pkg/models/mocks/StudioReaderWriter.go b/pkg/models/mocks/StudioReaderWriter.go index 043bfdecc..8868efcc8 100644 --- a/pkg/models/mocks/StudioReaderWriter.go +++ b/pkg/models/mocks/StudioReaderWriter.go @@ -95,20 +95,6 @@ func (_m *StudioReaderWriter) Destroy(ctx context.Context, id int) error { return r0 } -// DestroyImage provides a mock function with given fields: ctx, studioID -func (_m *StudioReaderWriter) DestroyImage(ctx context.Context, studioID int) error { - ret := _m.Called(ctx, studioID) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int) error); ok { - r0 = rf(ctx, studioID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // Find provides a mock function with given fields: ctx, id func (_m *StudioReaderWriter) Find(ctx context.Context, id int) (*models.Studio, error) { ret := _m.Called(ctx, id) diff --git a/pkg/models/mocks/TagReaderWriter.go b/pkg/models/mocks/TagReaderWriter.go index 1a53adf05..d67f4f00e 100644 --- a/pkg/models/mocks/TagReaderWriter.go +++ b/pkg/models/mocks/TagReaderWriter.go @@ -95,20 +95,6 @@ func (_m *TagReaderWriter) Destroy(ctx context.Context, id int) error { return r0 } -// DestroyImage provides a mock function with given fields: ctx, tagID -func (_m *TagReaderWriter) DestroyImage(ctx context.Context, tagID int) error { - ret := _m.Called(ctx, tagID) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int) error); ok { - r0 = rf(ctx, tagID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // Find provides a mock function with given fields: ctx, id func (_m *TagReaderWriter) Find(ctx context.Context, id int) (*models.Tag, error) { ret := _m.Called(ctx, id) diff --git a/pkg/models/mocks/transaction.go b/pkg/models/mocks/transaction.go index f2c4c9c49..8ce7176b2 100644 --- a/pkg/models/mocks/transaction.go +++ b/pkg/models/mocks/transaction.go @@ -44,16 +44,17 @@ func (*TxnManager) Reset() error { func NewTxnRepository() models.Repository { return models.Repository{ - TxnManager: &TxnManager{}, - Gallery: &GalleryReaderWriter{}, - Image: &ImageReaderWriter{}, - Movie: &MovieReaderWriter{}, - Performer: &PerformerReaderWriter{}, - Scene: &SceneReaderWriter{}, - SceneMarker: &SceneMarkerReaderWriter{}, - ScrapedItem: &ScrapedItemReaderWriter{}, - Studio: &StudioReaderWriter{}, - Tag: &TagReaderWriter{}, - SavedFilter: &SavedFilterReaderWriter{}, + TxnManager: &TxnManager{}, + Gallery: &GalleryReaderWriter{}, + GalleryChapter: &GalleryChapterReaderWriter{}, + Image: &ImageReaderWriter{}, + Movie: &MovieReaderWriter{}, + Performer: &PerformerReaderWriter{}, + Scene: &SceneReaderWriter{}, + SceneMarker: &SceneMarkerReaderWriter{}, + ScrapedItem: &ScrapedItemReaderWriter{}, + Studio: &StudioReaderWriter{}, + Tag: &TagReaderWriter{}, + SavedFilter: &SavedFilterReaderWriter{}, } } diff --git a/pkg/models/model_gallery_chapter.go b/pkg/models/model_gallery_chapter.go new file mode 100644 index 000000000..308fdbe6c --- /dev/null +++ b/pkg/models/model_gallery_chapter.go @@ -0,0 +1,24 @@ +package models + +import ( + "database/sql" +) + +type GalleryChapter struct { + ID int `db:"id" json:"id"` + Title string `db:"title" json:"title"` + ImageIndex int `db:"image_index" json:"image_index"` + GalleryID sql.NullInt64 `db:"gallery_id,omitempty" json:"gallery_id"` + CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"` + UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"` +} + +type GalleryChapters []*GalleryChapter + +func (m *GalleryChapters) Append(o interface{}) { + *m = append(*m, o.(*GalleryChapter)) +} + +func (m *GalleryChapters) New() interface{} { + return &GalleryChapter{} +} diff --git a/pkg/models/model_image.go b/pkg/models/model_image.go index dcece55bb..42425c455 100644 --- a/pkg/models/model_image.go +++ b/pkg/models/model_image.go @@ -16,10 +16,12 @@ type Image struct { Title string `json:"title"` // Rating expressed in 1-100 scale - Rating *int `json:"rating"` - Organized bool `json:"organized"` - OCounter int `json:"o_counter"` - StudioID *int `json:"studio_id"` + Rating *int `json:"rating"` + Organized bool `json:"organized"` + OCounter int `json:"o_counter"` + StudioID *int `json:"studio_id"` + URL string `json:"url"` + Date *Date `json:"date"` // transient - not persisted Files RelatedImageFiles @@ -117,6 +119,8 @@ type ImagePartial struct { Title OptionalString // Rating expressed in 1-100 scale Rating OptionalInt + URL OptionalString + Date OptionalDate Organized OptionalBool OCounter OptionalInt StudioID OptionalInt diff --git a/pkg/models/model_movie.go b/pkg/models/model_movie.go index 756a6c936..00b87ad0f 100644 --- a/pkg/models/model_movie.go +++ b/pkg/models/model_movie.go @@ -22,6 +22,10 @@ type Movie struct { URL sql.NullString `db:"url" json:"url"` CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"` UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"` + + // TODO - this is only here because of database code in the models package + FrontImageBlob sql.NullString `db:"front_image_blob" json:"-"` + BackImageBlob sql.NullString `db:"back_image_blob" json:"-"` } type MoviePartial struct { diff --git a/pkg/models/model_performer.go b/pkg/models/model_performer.go index 18c864fc4..fd52a7674 100644 --- a/pkg/models/model_performer.go +++ b/pkg/models/model_performer.go @@ -1,33 +1,31 @@ package models import ( + "context" "time" - - "github.com/stashapp/stash/pkg/hash/md5" ) type Performer struct { - ID int `json:"id"` - Checksum string `json:"checksum"` - Name string `json:"name"` - Gender GenderEnum `json:"gender"` - URL string `json:"url"` - Twitter string `json:"twitter"` - Instagram string `json:"instagram"` - Birthdate *Date `json:"birthdate"` - Ethnicity string `json:"ethnicity"` - Country string `json:"country"` - EyeColor string `json:"eye_color"` - Height *int `json:"height"` - Measurements string `json:"measurements"` - FakeTits string `json:"fake_tits"` - CareerLength string `json:"career_length"` - Tattoos string `json:"tattoos"` - Piercings string `json:"piercings"` - Aliases string `json:"aliases"` - Favorite bool `json:"favorite"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID int `json:"id"` + Name string `json:"name"` + Disambiguation string `json:"disambiguation"` + Gender GenderEnum `json:"gender"` + URL string `json:"url"` + Twitter string `json:"twitter"` + Instagram string `json:"instagram"` + Birthdate *Date `json:"birthdate"` + Ethnicity string `json:"ethnicity"` + Country string `json:"country"` + EyeColor string `json:"eye_color"` + Height *int `json:"height"` + Measurements string `json:"measurements"` + FakeTits string `json:"fake_tits"` + CareerLength string `json:"career_length"` + Tattoos string `json:"tattoos"` + Piercings string `json:"piercings"` + Favorite bool `json:"favorite"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // Rating expressed in 1-100 scale Rating *int `json:"rating"` Details string `json:"details"` @@ -35,32 +33,69 @@ type Performer struct { HairColor string `json:"hair_color"` Weight *int `json:"weight"` IgnoreAutoTag bool `json:"ignore_auto_tag"` + + Aliases RelatedStrings `json:"aliases"` + TagIDs RelatedIDs `json:"tag_ids"` + StashIDs RelatedStashIDs `json:"stash_ids"` +} + +func (s *Performer) LoadAliases(ctx context.Context, l AliasLoader) error { + return s.Aliases.load(func() ([]string, error) { + return l.GetAliases(ctx, s.ID) + }) +} + +func (s *Performer) LoadTagIDs(ctx context.Context, l TagIDLoader) error { + return s.TagIDs.load(func() ([]int, error) { + return l.GetTagIDs(ctx, s.ID) + }) +} + +func (s *Performer) LoadStashIDs(ctx context.Context, l StashIDLoader) error { + return s.StashIDs.load(func() ([]StashID, error) { + return l.GetStashIDs(ctx, s.ID) + }) +} + +func (s *Performer) LoadRelationships(ctx context.Context, l PerformerReader) error { + if err := s.LoadAliases(ctx, l); err != nil { + return err + } + + if err := s.LoadTagIDs(ctx, l); err != nil { + return err + } + + if err := s.LoadStashIDs(ctx, l); err != nil { + return err + } + + return nil } // PerformerPartial represents part of a Performer object. It is used to update // the database entry. type PerformerPartial struct { - ID int - Checksum OptionalString - Name OptionalString - Gender OptionalString - URL OptionalString - Twitter OptionalString - Instagram OptionalString - Birthdate OptionalDate - Ethnicity OptionalString - Country OptionalString - EyeColor OptionalString - Height OptionalInt - Measurements OptionalString - FakeTits OptionalString - CareerLength OptionalString - Tattoos OptionalString - Piercings OptionalString - Aliases OptionalString - Favorite OptionalBool - CreatedAt OptionalTime - UpdatedAt OptionalTime + ID int + Name OptionalString + Disambiguation OptionalString + Gender OptionalString + URL OptionalString + Twitter OptionalString + Instagram OptionalString + Birthdate OptionalDate + Ethnicity OptionalString + Country OptionalString + EyeColor OptionalString + Height OptionalInt + Measurements OptionalString + FakeTits OptionalString + CareerLength OptionalString + Tattoos OptionalString + Piercings OptionalString + Favorite OptionalBool + CreatedAt OptionalTime + UpdatedAt OptionalTime // Rating expressed in 1-100 scale Rating OptionalInt Details OptionalString @@ -68,12 +103,15 @@ type PerformerPartial struct { HairColor OptionalString Weight OptionalInt IgnoreAutoTag OptionalBool + + Aliases *UpdateStrings + TagIDs *UpdateIDs + StashIDs *UpdateStashIDs } func NewPerformer(name string) *Performer { currentTime := time.Now() return &Performer{ - Checksum: md5.FromString(name), Name: name, CreatedAt: currentTime, UpdatedAt: currentTime, diff --git a/pkg/models/model_scraped_item.go b/pkg/models/model_scraped_item.go index 8475b0b35..fa25bcb7e 100644 --- a/pkg/models/model_scraped_item.go +++ b/pkg/models/model_scraped_item.go @@ -18,24 +18,25 @@ func (ScrapedStudio) IsScrapedContent() {} // A performer from a scraping operation... type ScrapedPerformer struct { // Set if performer matched - StoredID *string `json:"stored_id"` - Name *string `json:"name"` - Gender *string `json:"gender"` - URL *string `json:"url"` - Twitter *string `json:"twitter"` - Instagram *string `json:"instagram"` - Birthdate *string `json:"birthdate"` - Ethnicity *string `json:"ethnicity"` - Country *string `json:"country"` - EyeColor *string `json:"eye_color"` - Height *string `json:"height"` - Measurements *string `json:"measurements"` - FakeTits *string `json:"fake_tits"` - CareerLength *string `json:"career_length"` - Tattoos *string `json:"tattoos"` - Piercings *string `json:"piercings"` - Aliases *string `json:"aliases"` - Tags []*ScrapedTag `json:"tags"` + StoredID *string `json:"stored_id"` + Name *string `json:"name"` + Disambiguation *string `json:"disambiguation"` + Gender *string `json:"gender"` + URL *string `json:"url"` + Twitter *string `json:"twitter"` + Instagram *string `json:"instagram"` + Birthdate *string `json:"birthdate"` + Ethnicity *string `json:"ethnicity"` + Country *string `json:"country"` + EyeColor *string `json:"eye_color"` + Height *string `json:"height"` + Measurements *string `json:"measurements"` + FakeTits *string `json:"fake_tits"` + CareerLength *string `json:"career_length"` + Tattoos *string `json:"tattoos"` + Piercings *string `json:"piercings"` + Aliases *string `json:"aliases"` + Tags []*ScrapedTag `json:"tags"` // This should be a base64 encoded data URL Image *string `json:"image"` Images []string `json:"images"` diff --git a/pkg/models/model_studio.go b/pkg/models/model_studio.go index 51a8d332c..989415293 100644 --- a/pkg/models/model_studio.go +++ b/pkg/models/model_studio.go @@ -19,6 +19,8 @@ type Studio struct { Rating sql.NullInt64 `db:"rating" json:"rating"` Details sql.NullString `db:"details" json:"details"` IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"` + // TODO - this is only here because of database code in the models package + ImageBlob sql.NullString `db:"image_blob" json:"-"` } type StudioPartial struct { diff --git a/pkg/models/model_tag.go b/pkg/models/model_tag.go index 043f84bd6..b12574155 100644 --- a/pkg/models/model_tag.go +++ b/pkg/models/model_tag.go @@ -6,12 +6,14 @@ import ( ) type Tag struct { - ID int `db:"id" json:"id"` - Name string `db:"name" json:"name"` // TODO make schema not null - Description sql.NullString `db:"description" json:"description"` - IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"` - CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"` - UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"` + ID int `db:"id" json:"id"` + Name string `db:"name" json:"name"` // TODO make schema not null + Description sql.NullString `db:"description" json:"description"` + IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"` + // TODO - this is only here because of database code in the models package + ImageBlob sql.NullString `db:"image_blob" json:"-"` + CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"` + UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"` } type TagPartial struct { diff --git a/pkg/models/movie.go b/pkg/models/movie.go index 8d58d70dd..aac1aa759 100644 --- a/pkg/models/movie.go +++ b/pkg/models/movie.go @@ -39,6 +39,7 @@ type MovieReader interface { Query(ctx context.Context, movieFilter *MovieFilterType, findFilter *FindFilterType) ([]*Movie, int, error) GetFrontImage(ctx context.Context, movieID int) ([]byte, error) GetBackImage(ctx context.Context, movieID int) ([]byte, error) + HasBackImage(ctx context.Context, movieID int) (bool, error) FindByPerformerID(ctx context.Context, performerID int) ([]*Movie, error) CountByPerformerID(ctx context.Context, performerID int) (int, error) FindByStudioID(ctx context.Context, studioID int) ([]*Movie, error) @@ -50,8 +51,8 @@ type MovieWriter interface { Update(ctx context.Context, updatedMovie MoviePartial) (*Movie, error) UpdateFull(ctx context.Context, updatedMovie Movie) (*Movie, error) Destroy(ctx context.Context, id int) error - UpdateImages(ctx context.Context, movieID int, frontImage []byte, backImage []byte) error - DestroyImages(ctx context.Context, movieID int) error + UpdateFrontImage(ctx context.Context, movieID int, frontImage []byte) error + UpdateBackImage(ctx context.Context, movieID int, backImage []byte) error } type MovieReaderWriter interface { diff --git a/pkg/models/paths/paths.go b/pkg/models/paths/paths.go index 612d5e2b7..ed35bca56 100644 --- a/pkg/models/paths/paths.go +++ b/pkg/models/paths/paths.go @@ -11,14 +11,17 @@ type Paths struct { Scene *scenePaths SceneMarkers *sceneMarkerPaths + Blobs string } -func NewPaths(generatedPath string) Paths { +func NewPaths(generatedPath string, blobsPath string) Paths { p := Paths{} p.Generated = newGeneratedPaths(generatedPath) p.Scene = newScenePaths(p) p.SceneMarkers = newSceneMarkerPaths(p) + p.Blobs = blobsPath + return p } diff --git a/pkg/models/paths/paths_scenes.go b/pkg/models/paths/paths_scenes.go index d54fbfb59..003e63304 100644 --- a/pkg/models/paths/paths_scenes.go +++ b/pkg/models/paths/paths_scenes.go @@ -17,14 +17,10 @@ func newScenePaths(p Paths) *scenePaths { return &sp } -func (sp *scenePaths) GetScreenshotPath(checksum string) string { +func (sp *scenePaths) GetLegacyScreenshotPath(checksum string) string { return filepath.Join(sp.Screenshots, checksum+".jpg") } -func (sp *scenePaths) GetThumbnailScreenshotPath(checksum string) string { - return filepath.Join(sp.Screenshots, checksum+".thumb.jpg") -} - func (sp *scenePaths) GetTranscodePath(checksum string) string { return filepath.Join(sp.Transcodes, checksum+".mp4") } diff --git a/pkg/models/performer.go b/pkg/models/performer.go index d5b6ea55c..de0f278e1 100644 --- a/pkg/models/performer.go +++ b/pkg/models/performer.go @@ -62,11 +62,12 @@ type GenderCriterionInput struct { } type PerformerFilterType struct { - And *PerformerFilterType `json:"AND"` - Or *PerformerFilterType `json:"OR"` - Not *PerformerFilterType `json:"NOT"` - Name *StringCriterionInput `json:"name"` - Details *StringCriterionInput `json:"details"` + And *PerformerFilterType `json:"AND"` + Or *PerformerFilterType `json:"OR"` + Not *PerformerFilterType `json:"NOT"` + Name *StringCriterionInput `json:"name"` + Disambiguation *StringCriterionInput `json:"disambiguation"` + Details *StringCriterionInput `json:"details"` // Filter by favorite FilterFavorites *bool `json:"filter_favorites"` // Filter by birth year @@ -159,9 +160,11 @@ type PerformerReader interface { // support the query needed QueryForAutoTag(ctx context.Context, words []string) ([]*Performer, error) Query(ctx context.Context, performerFilter *PerformerFilterType, findFilter *FindFilterType) ([]*Performer, int, error) + QueryCount(ctx context.Context, galleryFilter *PerformerFilterType, findFilter *FindFilterType) (int, error) + AliasLoader GetImage(ctx context.Context, performerID int) ([]byte, error) StashIDLoader - GetTagIDs(ctx context.Context, performerID int) ([]int, error) + TagIDLoader } type PerformerWriter interface { @@ -171,8 +174,6 @@ type PerformerWriter interface { Destroy(ctx context.Context, id int) error UpdateImage(ctx context.Context, performerID int, image []byte) error DestroyImage(ctx context.Context, performerID int) error - UpdateStashIDs(ctx context.Context, performerID int, stashIDs []StashID) error - UpdateTags(ctx context.Context, performerID int, tagIDs []int) error } type PerformerReaderWriter interface { diff --git a/pkg/models/relationships.go b/pkg/models/relationships.go index 91453c629..b3afcad9e 100644 --- a/pkg/models/relationships.go +++ b/pkg/models/relationships.go @@ -42,6 +42,10 @@ type FileLoader interface { GetFiles(ctx context.Context, relatedID int) ([]file.File, error) } +type AliasLoader interface { + GetAliases(ctx context.Context, relatedID int) ([]string, error) +} + // RelatedIDs represents a list of related IDs. // TODO - this can be made generic type RelatedIDs struct { @@ -481,3 +485,61 @@ func (r *RelatedFiles) loadPrimary(fn func() (file.File, error)) error { return nil } + +// RelatedStrings represents a list of related strings. +// TODO - this can be made generic +type RelatedStrings struct { + list []string +} + +// NewRelatedStrings returns a loaded RelatedStrings object with the provided values. +// Loaded will return true when called on the returned object if the provided slice is not nil. +func NewRelatedStrings(values []string) RelatedStrings { + return RelatedStrings{ + list: values, + } +} + +// Loaded returns true if the related IDs have been loaded. +func (r RelatedStrings) Loaded() bool { + return r.list != nil +} + +func (r RelatedStrings) mustLoaded() { + if !r.Loaded() { + panic("list has not been loaded") + } +} + +// List returns the related values. Panics if the relationship has not been loaded. +func (r RelatedStrings) List() []string { + r.mustLoaded() + + return r.list +} + +// Add adds the provided values to the list. Panics if the relationship has not been loaded. +func (r *RelatedStrings) Add(values ...string) { + r.mustLoaded() + + r.list = append(r.list, values...) +} + +func (r *RelatedStrings) load(fn func() ([]string, error)) error { + if r.Loaded() { + return nil + } + + values, err := fn() + if err != nil { + return err + } + + if values == nil { + values = []string{} + } + + r.list = values + + return nil +} diff --git a/pkg/models/repository.go b/pkg/models/repository.go index 7a9e14af5..898f7f25f 100644 --- a/pkg/models/repository.go +++ b/pkg/models/repository.go @@ -14,16 +14,17 @@ type TxnManager interface { type Repository struct { TxnManager - File file.Store - Folder file.FolderStore - Gallery GalleryReaderWriter - Image ImageReaderWriter - Movie MovieReaderWriter - Performer PerformerReaderWriter - Scene SceneReaderWriter - SceneMarker SceneMarkerReaderWriter - ScrapedItem ScrapedItemReaderWriter - Studio StudioReaderWriter - Tag TagReaderWriter - SavedFilter SavedFilterReaderWriter + File file.Store + Folder file.FolderStore + Gallery GalleryReaderWriter + GalleryChapter GalleryChapterReaderWriter + Image ImageReaderWriter + Movie MovieReaderWriter + Performer PerformerReaderWriter + Scene SceneReaderWriter + SceneMarker SceneMarkerReaderWriter + ScrapedItem ScrapedItemReaderWriter + Studio StudioReaderWriter + Tag TagReaderWriter + SavedFilter SavedFilterReaderWriter } diff --git a/pkg/models/scene.go b/pkg/models/scene.go index 3d6842943..55a27606a 100644 --- a/pkg/models/scene.go +++ b/pkg/models/scene.go @@ -176,6 +176,7 @@ type SceneReader interface { All(ctx context.Context) ([]*Scene, error) Query(ctx context.Context, options SceneQueryOptions) (*SceneQueryResult, error) GetCover(ctx context.Context, sceneID int) ([]byte, error) + HasCover(ctx context.Context, sceneID int) (bool, error) } type SceneWriter interface { @@ -189,7 +190,6 @@ type SceneWriter interface { IncrementWatchCount(ctx context.Context, id int) (int, error) Destroy(ctx context.Context, id int) error UpdateCover(ctx context.Context, sceneID int, cover []byte) error - DestroyCover(ctx context.Context, sceneID int) error } type SceneReaderWriter interface { diff --git a/pkg/models/scene_marker.go b/pkg/models/scene_marker.go index 3251f6a00..2ae8c3343 100644 --- a/pkg/models/scene_marker.go +++ b/pkg/models/scene_marker.go @@ -36,6 +36,8 @@ type SceneMarkerReader interface { CountByTagID(ctx context.Context, tagID int) (int, error) GetMarkerStrings(ctx context.Context, q *string, sort *string) ([]*MarkerStringsResultType, error) Wall(ctx context.Context, q *string) ([]*SceneMarker, error) + Count(ctx context.Context) (int, error) + All(ctx context.Context) ([]*SceneMarker, error) Query(ctx context.Context, sceneMarkerFilter *SceneMarkerFilterType, findFilter *FindFilterType) ([]*SceneMarker, int, error) GetTagIDs(ctx context.Context, imageID int) ([]int, error) } diff --git a/pkg/models/stash_ids.go b/pkg/models/stash_ids.go index 9c9cfc56a..fcc2bdec0 100644 --- a/pkg/models/stash_ids.go +++ b/pkg/models/stash_ids.go @@ -21,6 +21,18 @@ func (u *UpdateStashIDs) AddUnique(v StashID) { u.StashIDs = append(u.StashIDs, v) } +// Set sets or replaces the stash id for the endpoint in the provided value. +func (u *UpdateStashIDs) Set(v StashID) { + for i, vv := range u.StashIDs { + if vv.Endpoint == v.Endpoint { + u.StashIDs[i] = v + return + } + } + + u.StashIDs = append(u.StashIDs, v) +} + 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 diff --git a/pkg/models/studio.go b/pkg/models/studio.go index 26443edf7..7ccf33be0 100644 --- a/pkg/models/studio.go +++ b/pkg/models/studio.go @@ -66,7 +66,6 @@ type StudioWriter interface { UpdateFull(ctx context.Context, updatedStudio Studio) (*Studio, error) Destroy(ctx context.Context, id int) error UpdateImage(ctx context.Context, studioID int, image []byte) error - DestroyImage(ctx context.Context, studioID int) error UpdateStashIDs(ctx context.Context, studioID int, stashIDs []StashID) error UpdateAliases(ctx context.Context, studioID int, aliases []string) error } diff --git a/pkg/models/tag.go b/pkg/models/tag.go index 440d147d3..601dfcc16 100644 --- a/pkg/models/tag.go +++ b/pkg/models/tag.go @@ -74,7 +74,6 @@ type TagWriter interface { UpdateFull(ctx context.Context, updatedTag Tag) (*Tag, error) Destroy(ctx context.Context, id int) error UpdateImage(ctx context.Context, tagID int, image []byte) error - DestroyImage(ctx context.Context, tagID int) error UpdateAliases(ctx context.Context, tagID int, aliases []string) error Merge(ctx context.Context, source []int, destination int) error UpdateParentTags(ctx context.Context, tagID int, parentIDs []int) error diff --git a/pkg/models/update.go b/pkg/models/update.go index ecc9314ec..fbfab3d30 100644 --- a/pkg/models/update.go +++ b/pkg/models/update.go @@ -63,3 +63,8 @@ func (u *UpdateIDs) IDStrings() []string { return intslice.IntSliceToStringSlice(u.IDs) } + +type UpdateStrings struct { + Values []string `json:"values"` + Mode RelationshipUpdateMode `json:"mode"` +} diff --git a/pkg/movie/export.go b/pkg/movie/export.go index 2af697a49..23851f42f 100644 --- a/pkg/movie/export.go +++ b/pkg/movie/export.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/json" "github.com/stashapp/stash/pkg/models/jsonschema" @@ -64,7 +65,7 @@ func ToJSON(ctx context.Context, reader ImageGetter, studioReader studio.Finder, frontImage, err := reader.GetFrontImage(ctx, movie.ID) if err != nil { - return nil, fmt.Errorf("error getting movie front image: %v", err) + logger.Errorf("Error getting movie front image: %v", err) } if len(frontImage) > 0 { @@ -73,7 +74,7 @@ func ToJSON(ctx context.Context, reader ImageGetter, studioReader studio.Finder, backImage, err := reader.GetBackImage(ctx, movie.ID) if err != nil { - return nil, fmt.Errorf("error getting movie back image: %v", err) + logger.Errorf("Error getting movie back image: %v", err) } if len(backImage) > 0 { diff --git a/pkg/movie/export_test.go b/pkg/movie/export_test.go index 007383902..898400127 100644 --- a/pkg/movie/export_test.go +++ b/pkg/movie/export_test.go @@ -161,13 +161,15 @@ func initTestTable() { }, { createFullMovie(errFrontImageID, studioID), - nil, - true, + createFullJSONMovie(studioName, "", backImage), + // failure to get front image should not cause error + false, }, { createFullMovie(errBackImageID, studioID), - nil, - true, + createFullJSONMovie(studioName, frontImage, ""), + // failure to get back image should not cause error + false, }, { createFullMovie(errStudioMovieID, errStudioID), diff --git a/pkg/movie/import.go b/pkg/movie/import.go index 461df0f84..75bc28d4a 100644 --- a/pkg/movie/import.go +++ b/pkg/movie/import.go @@ -12,10 +12,15 @@ import ( "github.com/stashapp/stash/pkg/utils" ) +type ImageUpdater interface { + UpdateFrontImage(ctx context.Context, movieID int, frontImage []byte) error + UpdateBackImage(ctx context.Context, movieID int, backImage []byte) error +} + type NameFinderCreatorUpdater interface { NameFinderCreator UpdateFull(ctx context.Context, updatedMovie models.Movie) (*models.Movie, error) - UpdateImages(ctx context.Context, movieID int, frontImage []byte, backImage []byte) error + ImageUpdater } type Importer struct { @@ -126,8 +131,14 @@ func (i *Importer) createStudio(ctx context.Context, name string) (int, error) { func (i *Importer) PostImport(ctx context.Context, id int) error { if len(i.frontImageData) > 0 { - if err := i.ReaderWriter.UpdateImages(ctx, id, i.frontImageData, i.backImageData); err != nil { - return fmt.Errorf("error setting movie images: %v", err) + if err := i.ReaderWriter.UpdateFrontImage(ctx, id, i.frontImageData); err != nil { + return fmt.Errorf("error setting movie front image: %v", err) + } + } + + if len(i.backImageData) > 0 { + if err := i.ReaderWriter.UpdateBackImage(ctx, id, i.backImageData); err != nil { + return fmt.Errorf("error setting movie back image: %v", err) } } diff --git a/pkg/movie/import_test.go b/pkg/movie/import_test.go index 26b6d9f27..c33d4baa2 100644 --- a/pkg/movie/import_test.go +++ b/pkg/movie/import_test.go @@ -162,8 +162,9 @@ func TestImporterPostImport(t *testing.T) { updateMovieImageErr := errors.New("UpdateImages error") - readerWriter.On("UpdateImages", testCtx, movieID, frontImageBytes, backImageBytes).Return(nil).Once() - readerWriter.On("UpdateImages", testCtx, errImageID, frontImageBytes, backImageBytes).Return(updateMovieImageErr).Once() + readerWriter.On("UpdateFrontImage", testCtx, movieID, frontImageBytes).Return(nil).Once() + readerWriter.On("UpdateBackImage", testCtx, movieID, backImageBytes).Return(nil).Once() + readerWriter.On("UpdateFrontImage", testCtx, errImageID, frontImageBytes).Return(updateMovieImageErr).Once() err := i.PostImport(testCtx, movieID) assert.Nil(t, err) diff --git a/pkg/performer/export.go b/pkg/performer/export.go index 90e50cb69..4b46fd901 100644 --- a/pkg/performer/export.go +++ b/pkg/performer/export.go @@ -5,40 +5,42 @@ import ( "fmt" "strconv" + "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/json" "github.com/stashapp/stash/pkg/models/jsonschema" "github.com/stashapp/stash/pkg/utils" ) -type ImageStashIDGetter interface { +type ImageAliasStashIDGetter interface { GetImage(ctx context.Context, performerID int) ([]byte, error) + models.AliasLoader models.StashIDLoader } // ToJSON converts a Performer object into its JSON equivalent. -func ToJSON(ctx context.Context, reader ImageStashIDGetter, performer *models.Performer) (*jsonschema.Performer, error) { +func ToJSON(ctx context.Context, reader ImageAliasStashIDGetter, performer *models.Performer) (*jsonschema.Performer, error) { newPerformerJSON := jsonschema.Performer{ - Name: performer.Name, - Gender: performer.Gender.String(), - URL: performer.URL, - Ethnicity: performer.Ethnicity, - Country: performer.Country, - EyeColor: performer.EyeColor, - Measurements: performer.Measurements, - FakeTits: performer.FakeTits, - CareerLength: performer.CareerLength, - Tattoos: performer.Tattoos, - Piercings: performer.Piercings, - Aliases: performer.Aliases, - Twitter: performer.Twitter, - Instagram: performer.Instagram, - Favorite: performer.Favorite, - Details: performer.Details, - HairColor: performer.HairColor, - IgnoreAutoTag: performer.IgnoreAutoTag, - CreatedAt: json.JSONTime{Time: performer.CreatedAt}, - UpdatedAt: json.JSONTime{Time: performer.UpdatedAt}, + Name: performer.Name, + Disambiguation: performer.Disambiguation, + Gender: performer.Gender.String(), + URL: performer.URL, + Ethnicity: performer.Ethnicity, + Country: performer.Country, + EyeColor: performer.EyeColor, + Measurements: performer.Measurements, + FakeTits: performer.FakeTits, + CareerLength: performer.CareerLength, + Tattoos: performer.Tattoos, + Piercings: performer.Piercings, + Twitter: performer.Twitter, + Instagram: performer.Instagram, + Favorite: performer.Favorite, + Details: performer.Details, + HairColor: performer.HairColor, + IgnoreAutoTag: performer.IgnoreAutoTag, + CreatedAt: json.JSONTime{Time: performer.CreatedAt}, + UpdatedAt: json.JSONTime{Time: performer.UpdatedAt}, } if performer.Birthdate != nil { @@ -59,27 +61,27 @@ func ToJSON(ctx context.Context, reader ImageStashIDGetter, performer *models.Pe newPerformerJSON.Weight = *performer.Weight } + if err := performer.LoadAliases(ctx, reader); err != nil { + return nil, fmt.Errorf("loading performer aliases: %w", err) + } + + newPerformerJSON.Aliases = performer.Aliases.List() + + if err := performer.LoadStashIDs(ctx, reader); err != nil { + return nil, fmt.Errorf("loading performer stash ids: %w", err) + } + + newPerformerJSON.StashIDs = performer.StashIDs.List() + image, err := reader.GetImage(ctx, performer.ID) if err != nil { - return nil, fmt.Errorf("error getting performers image: %v", err) + logger.Errorf("Error getting performer image: %v", err) } if len(image) > 0 { newPerformerJSON.Image = utils.GetBase64StringFromData(image) } - stashIDs, _ := reader.GetStashIDs(ctx, performer.ID) - var ret []models.StashID - for _, stashID := range stashIDs { - newJoin := models.StashID{ - StashID: stashID.StashID, - Endpoint: stashID.Endpoint, - } - ret = append(ret, newJoin) - } - - newPerformerJSON.StashIDs = ret - return &newPerformerJSON, nil } diff --git a/pkg/performer/export_test.go b/pkg/performer/export_test.go index d3ee15d46..f65693e3f 100644 --- a/pkg/performer/export_test.go +++ b/pkg/performer/export_test.go @@ -4,7 +4,6 @@ import ( "errors" "strconv" - "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/json" "github.com/stashapp/stash/pkg/models/jsonschema" @@ -22,30 +21,31 @@ const ( ) const ( - performerName = "testPerformer" - url = "url" - aliases = "aliases" - careerLength = "careerLength" - country = "country" - ethnicity = "ethnicity" - eyeColor = "eyeColor" - fakeTits = "fakeTits" - gender = "gender" - instagram = "instagram" - measurements = "measurements" - piercings = "piercings" - tattoos = "tattoos" - twitter = "twitter" - details = "details" - hairColor = "hairColor" + performerName = "testPerformer" + disambiguation = "disambiguation" + url = "url" + careerLength = "careerLength" + country = "country" + ethnicity = "ethnicity" + eyeColor = "eyeColor" + fakeTits = "fakeTits" + gender = "gender" + instagram = "instagram" + measurements = "measurements" + piercings = "piercings" + tattoos = "tattoos" + twitter = "twitter" + details = "details" + hairColor = "hairColor" autoTagIgnored = true ) var ( - rating = 5 - height = 123 - weight = 60 + aliases = []string{"alias1", "alias2"} + rating = 5 + height = 123 + weight = 60 ) var imageBytes = []byte("imageBytes") @@ -70,33 +70,35 @@ var ( func createFullPerformer(id int, name string) *models.Performer { return &models.Performer{ - ID: id, - Name: name, - Checksum: md5.FromString(name), - URL: url, - Aliases: aliases, - Birthdate: &birthDate, - CareerLength: careerLength, - Country: country, - Ethnicity: ethnicity, - EyeColor: eyeColor, - FakeTits: fakeTits, - Favorite: true, - Gender: gender, - Height: &height, - Instagram: instagram, - Measurements: measurements, - Piercings: piercings, - Tattoos: tattoos, - Twitter: twitter, - CreatedAt: createTime, - UpdatedAt: updateTime, - Rating: &rating, - Details: details, - DeathDate: &deathDate, - HairColor: hairColor, - Weight: &weight, - IgnoreAutoTag: autoTagIgnored, + ID: id, + Name: name, + Disambiguation: disambiguation, + URL: url, + Aliases: models.NewRelatedStrings(aliases), + Birthdate: &birthDate, + CareerLength: careerLength, + Country: country, + Ethnicity: ethnicity, + EyeColor: eyeColor, + FakeTits: fakeTits, + Favorite: true, + Gender: gender, + Height: &height, + Instagram: instagram, + Measurements: measurements, + Piercings: piercings, + Tattoos: tattoos, + Twitter: twitter, + CreatedAt: createTime, + UpdatedAt: updateTime, + Rating: &rating, + Details: details, + DeathDate: &deathDate, + HairColor: hairColor, + Weight: &weight, + IgnoreAutoTag: autoTagIgnored, + TagIDs: models.NewRelatedIDs([]int{}), + StashIDs: models.NewRelatedStashIDs(stashIDs), } } @@ -105,49 +107,53 @@ func createEmptyPerformer(id int) models.Performer { ID: id, CreatedAt: createTime, UpdatedAt: updateTime, + Aliases: models.NewRelatedStrings([]string{}), + TagIDs: models.NewRelatedIDs([]int{}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{}), } } func createFullJSONPerformer(name string, image string) *jsonschema.Performer { return &jsonschema.Performer{ - Name: name, - URL: url, - Aliases: aliases, - Birthdate: birthDate.String(), - CareerLength: careerLength, - Country: country, - Ethnicity: ethnicity, - EyeColor: eyeColor, - FakeTits: fakeTits, - Favorite: true, - Gender: gender, - Height: strconv.Itoa(height), - Instagram: instagram, - Measurements: measurements, - Piercings: piercings, - Tattoos: tattoos, - Twitter: twitter, + Name: name, + Disambiguation: disambiguation, + URL: url, + Aliases: aliases, + Birthdate: birthDate.String(), + CareerLength: careerLength, + Country: country, + Ethnicity: ethnicity, + EyeColor: eyeColor, + FakeTits: fakeTits, + Favorite: true, + Gender: gender, + Height: strconv.Itoa(height), + Instagram: instagram, + Measurements: measurements, + Piercings: piercings, + Tattoos: tattoos, + Twitter: twitter, CreatedAt: json.JSONTime{ Time: createTime, }, UpdatedAt: json.JSONTime{ Time: updateTime, }, - Rating: rating, - Image: image, - Details: details, - DeathDate: deathDate.String(), - HairColor: hairColor, - Weight: weight, - StashIDs: []models.StashID{ - stashID, - }, + Rating: rating, + Image: image, + Details: details, + DeathDate: deathDate.String(), + HairColor: hairColor, + Weight: weight, + StashIDs: stashIDs, IgnoreAutoTag: autoTagIgnored, } } func createEmptyJSONPerformer() *jsonschema.Performer { return &jsonschema.Performer{ + Aliases: []string{}, + StashIDs: []models.StashID{}, CreatedAt: json.JSONTime{ Time: createTime, }, @@ -179,8 +185,9 @@ func initTestTable() { }, { *createFullPerformer(errImageID, performerName), - nil, - true, + createFullJSONPerformer(performerName, ""), + // failure to get image should not cause an error + false, }, } } @@ -196,9 +203,6 @@ func TestToJSON(t *testing.T) { mockPerformerReader.On("GetImage", testCtx, noImageID).Return(nil, nil).Once() mockPerformerReader.On("GetImage", testCtx, errImageID).Return(nil, imageErr).Once() - mockPerformerReader.On("GetStashIDs", testCtx, performerID).Return(stashIDs, nil).Once() - mockPerformerReader.On("GetStashIDs", testCtx, noImageID).Return(nil, nil).Once() - for i, s := range scenarios { tag := s.input json, err := ToJSON(testCtx, mockPerformerReader, &tag) diff --git a/pkg/performer/import.go b/pkg/performer/import.go index 62c1d1b95..beebab35d 100644 --- a/pkg/performer/import.go +++ b/pkg/performer/import.go @@ -6,7 +6,6 @@ import ( "strconv" "strings" - "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/jsonschema" @@ -18,9 +17,7 @@ import ( type NameFinderCreatorUpdater interface { NameFinderCreator Update(ctx context.Context, updatedPerformer *models.Performer) error - UpdateTags(ctx context.Context, performerID int, tagIDs []int) error UpdateImage(ctx context.Context, performerID int, image []byte) error - UpdateStashIDs(ctx context.Context, performerID int, stashIDs []models.StashID) error } type Importer struct { @@ -32,8 +29,6 @@ type Importer struct { ID int performer models.Performer imageData []byte - - tags []*models.Tag } func (i *Importer) PreImport(ctx context.Context) error { @@ -62,7 +57,9 @@ func (i *Importer) populateTags(ctx context.Context) error { return err } - i.tags = tags + for _, p := range tags { + i.performer.TagIDs.Add(p.ID) + } } return nil @@ -120,28 +117,12 @@ func createTags(ctx context.Context, tagWriter tag.NameFinderCreator, names []st } func (i *Importer) PostImport(ctx context.Context, id int) error { - if len(i.tags) > 0 { - var tagIDs []int - for _, t := range i.tags { - tagIDs = append(tagIDs, t.ID) - } - if err := i.ReaderWriter.UpdateTags(ctx, id, tagIDs); err != nil { - return fmt.Errorf("failed to associate tags: %v", err) - } - } - if len(i.imageData) > 0 { if err := i.ReaderWriter.UpdateImage(ctx, id, i.imageData); err != nil { return fmt.Errorf("error setting performer image: %v", err) } } - if len(i.Input.StashIDs) > 0 { - if err := i.ReaderWriter.UpdateStashIDs(ctx, id, i.Input.StashIDs); err != nil { - return fmt.Errorf("error setting stash id: %v", err) - } - } - return nil } @@ -150,8 +131,27 @@ func (i *Importer) Name() string { } func (i *Importer) FindExistingID(ctx context.Context) (*int, error) { - const nocase = false - existing, err := i.ReaderWriter.FindByNames(ctx, []string{i.Name()}, nocase) + // use disambiguation as well + performerFilter := models.PerformerFilterType{ + Name: &models.StringCriterionInput{ + Value: i.Input.Name, + Modifier: models.CriterionModifierEquals, + }, + } + + if i.Input.Disambiguation != "" { + performerFilter.Disambiguation = &models.StringCriterionInput{ + Value: i.Input.Disambiguation, + Modifier: models.CriterionModifierEquals, + } + } + + pp := 1 + findFilter := models.FindFilterType{ + PerPage: &pp, + } + + existing, _, err := i.ReaderWriter.Query(ctx, &performerFilter, &findFilter) if err != nil { return nil, err } @@ -186,30 +186,31 @@ func (i *Importer) Update(ctx context.Context, id int) error { } func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Performer { - checksum := md5.FromString(performerJSON.Name) - newPerformer := models.Performer{ - Name: performerJSON.Name, - Checksum: checksum, - Gender: models.GenderEnum(performerJSON.Gender), - URL: performerJSON.URL, - Ethnicity: performerJSON.Ethnicity, - Country: performerJSON.Country, - EyeColor: performerJSON.EyeColor, - Measurements: performerJSON.Measurements, - FakeTits: performerJSON.FakeTits, - CareerLength: performerJSON.CareerLength, - Tattoos: performerJSON.Tattoos, - Piercings: performerJSON.Piercings, - Aliases: performerJSON.Aliases, - Twitter: performerJSON.Twitter, - Instagram: performerJSON.Instagram, - Details: performerJSON.Details, - HairColor: performerJSON.HairColor, - Favorite: performerJSON.Favorite, - IgnoreAutoTag: performerJSON.IgnoreAutoTag, - CreatedAt: performerJSON.CreatedAt.GetTime(), - UpdatedAt: performerJSON.UpdatedAt.GetTime(), + Name: performerJSON.Name, + Disambiguation: performerJSON.Disambiguation, + Gender: models.GenderEnum(performerJSON.Gender), + URL: performerJSON.URL, + Ethnicity: performerJSON.Ethnicity, + Country: performerJSON.Country, + EyeColor: performerJSON.EyeColor, + Measurements: performerJSON.Measurements, + FakeTits: performerJSON.FakeTits, + CareerLength: performerJSON.CareerLength, + Tattoos: performerJSON.Tattoos, + Piercings: performerJSON.Piercings, + Aliases: models.NewRelatedStrings(performerJSON.Aliases), + Twitter: performerJSON.Twitter, + Instagram: performerJSON.Instagram, + Details: performerJSON.Details, + HairColor: performerJSON.HairColor, + Favorite: performerJSON.Favorite, + IgnoreAutoTag: performerJSON.IgnoreAutoTag, + CreatedAt: performerJSON.CreatedAt.GetTime(), + UpdatedAt: performerJSON.UpdatedAt.GetTime(), + + TagIDs: models.NewRelatedIDs([]int{}), + StashIDs: models.NewRelatedStashIDs(performerJSON.StashIDs), } if performerJSON.Birthdate != "" { diff --git a/pkg/performer/import_test.go b/pkg/performer/import_test.go index 08f2c5b0c..5cfd9c90d 100644 --- a/pkg/performer/import_test.go +++ b/pkg/performer/import_test.go @@ -6,7 +6,6 @@ import ( "github.com/stretchr/testify/mock" - "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/jsonschema" "github.com/stashapp/stash/pkg/models/mocks" @@ -60,7 +59,6 @@ func TestImporterPreImport(t *testing.T) { assert.Nil(t, err) expectedPerformer := *createFullPerformer(0, performerName) - expectedPerformer.Checksum = md5.FromString(performerName) assert.Equal(t, expectedPerformer, i.performer) } @@ -87,7 +85,7 @@ func TestImporterPreImportWithTag(t *testing.T) { err := i.PreImport(testCtx) assert.Nil(t, err) - assert.Equal(t, existingTagID, i.tags[0].ID) + assert.Equal(t, existingTagID, i.performer.TagIDs.List()[0]) i.Input.Tags = []string{existingTagErr} err = i.PreImport(testCtx) @@ -124,7 +122,7 @@ func TestImporterPreImportWithMissingTag(t *testing.T) { i.MissingRefBehaviour = models.ImportMissingRefEnumCreate err = i.PreImport(testCtx) assert.Nil(t, err) - assert.Equal(t, existingTagID, i.tags[0].ID) + assert.Equal(t, existingTagID, i.performer.TagIDs.List()[0]) tagReaderWriter.AssertExpectations(t) } @@ -181,14 +179,28 @@ func TestImporterFindExistingID(t *testing.T) { }, } + pp := 1 + findFilter := &models.FindFilterType{ + PerPage: &pp, + } + + performerFilter := func(name string) *models.PerformerFilterType { + return &models.PerformerFilterType{ + Name: &models.StringCriterionInput{ + Value: name, + Modifier: models.CriterionModifierEquals, + }, + } + } + errFindByNames := errors.New("FindByNames error") - readerWriter.On("FindByNames", testCtx, []string{performerName}, false).Return(nil, nil).Once() - readerWriter.On("FindByNames", testCtx, []string{existingPerformerName}, false).Return([]*models.Performer{ + readerWriter.On("Query", testCtx, performerFilter(performerName), findFilter).Return(nil, 0, nil).Once() + readerWriter.On("Query", testCtx, performerFilter(existingPerformerName), findFilter).Return([]*models.Performer{ { ID: existingPerformerID, }, - }, nil).Once() - readerWriter.On("FindByNames", testCtx, []string{performerNameErr}, false).Return(nil, errFindByNames).Once() + }, 1, nil).Once() + readerWriter.On("Query", testCtx, performerFilter(performerNameErr), findFilter).Return(nil, 0, errFindByNames).Once() id, err := i.FindExistingID(testCtx) assert.Nil(t, id) @@ -207,32 +219,6 @@ func TestImporterFindExistingID(t *testing.T) { readerWriter.AssertExpectations(t) } -func TestImporterPostImportUpdateTags(t *testing.T) { - readerWriter := &mocks.PerformerReaderWriter{} - - i := Importer{ - ReaderWriter: readerWriter, - tags: []*models.Tag{ - { - ID: existingTagID, - }, - }, - } - - updateErr := errors.New("UpdateTags error") - - readerWriter.On("UpdateTags", testCtx, performerID, []int{existingTagID}).Return(nil).Once() - readerWriter.On("UpdateTags", testCtx, errTagsID, mock.AnythingOfType("[]int")).Return(updateErr).Once() - - err := i.PostImport(testCtx, performerID) - assert.Nil(t, err) - - err = i.PostImport(testCtx, errTagsID) - assert.NotNil(t, err) - - readerWriter.AssertExpectations(t) -} - func TestCreate(t *testing.T) { readerWriter := &mocks.PerformerReaderWriter{} diff --git a/pkg/performer/query.go b/pkg/performer/query.go new file mode 100644 index 000000000..d790c6d52 --- /dev/null +++ b/pkg/performer/query.go @@ -0,0 +1,27 @@ +package performer + +import ( + "context" + "strconv" + + "github.com/stashapp/stash/pkg/models" +) + +type Queryer interface { + Query(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) +} + +type CountQueryer interface { + QueryCount(ctx context.Context, galleryFilter *models.PerformerFilterType, findFilter *models.FindFilterType) (int, error) +} + +func CountByStudioID(ctx context.Context, r CountQueryer, id int) (int, error) { + filter := &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(id)}, + Modifier: models.CriterionModifierIncludes, + }, + } + + return r.QueryCount(ctx, filter, nil) +} diff --git a/pkg/performer/update.go b/pkg/performer/update.go index ed10246fa..d846eb6ce 100644 --- a/pkg/performer/update.go +++ b/pkg/performer/update.go @@ -8,5 +8,6 @@ import ( type NameFinderCreator interface { FindByNames(ctx context.Context, names []string, nocase bool) ([]*models.Performer, error) + Query(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) Create(ctx context.Context, newPerformer *models.Performer) error } diff --git a/pkg/plugin/config.go b/pkg/plugin/config.go index 2a00c3ced..eac7289a8 100644 --- a/pkg/plugin/config.go +++ b/pkg/plugin/config.go @@ -56,6 +56,35 @@ type Config struct { // The hooks configurations for hooks registered by this plugin. Hooks []*HookConfig `yaml:"hooks"` + + // Javascript files that will be injected into the stash UI. + UI UIConfig `yaml:"ui"` +} + +type UIConfig struct { + // Javascript files that will be injected into the stash UI. + Javascript []string `yaml:"javascript"` + + // CSS files that will be injected into the stash UI. + CSS []string `yaml:"css"` +} + +func (c UIConfig) getCSSFiles(parent Config) []string { + ret := make([]string, len(c.CSS)) + for i, v := range c.CSS { + ret[i] = filepath.Join(parent.getConfigPath(), v) + } + + return ret +} + +func (c UIConfig) getJavascriptFiles(parent Config) []string { + ret := make([]string, len(c.Javascript)) + for i, v := range c.Javascript { + ret[i] = filepath.Join(parent.getConfigPath(), v) + } + + return ret } func (c Config) getPluginTasks(includePlugin bool) []*PluginTask { @@ -121,6 +150,10 @@ func (c Config) toPlugin() *Plugin { Version: c.Version, Tasks: c.getPluginTasks(false), Hooks: c.getPluginHooks(false), + UI: PluginUI{ + Javascript: c.UI.getJavascriptFiles(c), + CSS: c.UI.getCSSFiles(c), + }, } } diff --git a/pkg/plugin/hooks.go b/pkg/plugin/hooks.go index a60e44e6c..fc91765b8 100644 --- a/pkg/plugin/hooks.go +++ b/pkg/plugin/hooks.go @@ -34,6 +34,10 @@ const ( GalleryUpdatePost HookTriggerEnum = "Gallery.Update.Post" GalleryDestroyPost HookTriggerEnum = "Gallery.Destroy.Post" + GalleryChapterCreatePost HookTriggerEnum = "GalleryChapter.Create.Post" + GalleryChapterUpdatePost HookTriggerEnum = "GalleryChapter.Update.Post" + GalleryChapterDestroyPost HookTriggerEnum = "GalleryChapter.Destroy.Post" + MovieCreatePost HookTriggerEnum = "Movie.Create.Post" MovieUpdatePost HookTriggerEnum = "Movie.Update.Post" MovieDestroyPost HookTriggerEnum = "Movie.Destroy.Post" @@ -69,6 +73,10 @@ var AllHookTriggerEnum = []HookTriggerEnum{ GalleryUpdatePost, GalleryDestroyPost, + GalleryChapterCreatePost, + GalleryChapterUpdatePost, + GalleryChapterDestroyPost, + MovieCreatePost, MovieUpdatePost, MovieDestroyPost, @@ -106,6 +114,10 @@ func (e HookTriggerEnum) IsValid() bool { GalleryUpdatePost, GalleryDestroyPost, + GalleryChapterCreatePost, + GalleryChapterUpdatePost, + GalleryChapterDestroyPost, + MovieCreatePost, MovieUpdatePost, MovieDestroyPost, diff --git a/pkg/plugin/plugins.go b/pkg/plugin/plugins.go index 5f74b1d8b..8e9354d0b 100644 --- a/pkg/plugin/plugins.go +++ b/pkg/plugin/plugins.go @@ -31,6 +31,15 @@ type Plugin struct { Version *string `json:"version"` Tasks []*PluginTask `json:"tasks"` Hooks []*PluginHook `json:"hooks"` + UI PluginUI `json:"ui"` +} + +type PluginUI struct { + // Javascript files that will be injected into the stash UI. + Javascript []string `json:"javascript"` + + // CSS files that will be injected into the stash UI. + CSS []string `json:"css"` } type ServerConfig interface { @@ -201,9 +210,8 @@ func (c Cache) ExecutePostHooks(ctx context.Context, id int, hookType HookTrigge } func (c Cache) RegisterPostHooks(ctx context.Context, id int, hookType HookTriggerEnum, input interface{}, inputFields []string) { - txn.AddPostCommitHook(ctx, func(ctx context.Context) error { + txn.AddPostCommitHook(ctx, func(ctx context.Context) { c.ExecutePostHooks(ctx, id, hookType, input, inputFields) - return nil }) } diff --git a/pkg/scene/create.go b/pkg/scene/create.go index 83fd5e56c..c2345d2ef 100644 --- a/pkg/scene/create.go +++ b/pkg/scene/create.go @@ -7,10 +7,8 @@ import ( "time" "github.com/stashapp/stash/pkg/file" - "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/plugin" - "github.com/stashapp/stash/pkg/txn" ) func (s *Service) Create(ctx context.Context, input *models.Scene, fileIDs []file.ID, coverImage []byte) (*models.Scene, error) { @@ -55,18 +53,6 @@ func (s *Service) Create(ctx context.Context, input *models.Scene, fileIDs []fil if err := s.Repository.UpdateCover(ctx, ret.ID, coverImage); err != nil { return nil, fmt.Errorf("setting cover on new scene: %w", err) } - - // only update the cover image if provided and everything else was successful - // only do this if there is a file associated - if len(fileIDs) > 0 { - txn.AddPostCommitHook(ctx, func(ctx context.Context) error { - if err := SetScreenshot(s.Paths, ret.GetHash(s.Config.GetVideoFileNamingAlgorithm()), coverImage); err != nil { - logger.Errorf("Error setting screenshot: %v", err) - } - - return nil - }) - } } s.PluginCache.RegisterPostHooks(ctx, ret.ID, plugin.SceneCreatePost, nil, nil) diff --git a/pkg/scene/delete.go b/pkg/scene/delete.go index 622b54377..f7fdda0c9 100644 --- a/pkg/scene/delete.go +++ b/pkg/scene/delete.go @@ -38,18 +38,6 @@ func (d *FileDeleter) MarkGeneratedFiles(scene *models.Scene) error { var files []string - thumbPath := d.Paths.Scene.GetThumbnailScreenshotPath(sceneHash) - exists, _ = fsutil.FileExists(thumbPath) - if exists { - files = append(files, thumbPath) - } - - normalPath := d.Paths.Scene.GetScreenshotPath(sceneHash) - exists, _ = fsutil.FileExists(normalPath) - if exists { - files = append(files, normalPath) - } - streamPreviewPath := d.Paths.Scene.GetVideoPreviewPath(sceneHash) exists, _ = fsutil.FileExists(streamPreviewPath) if exists { diff --git a/pkg/scene/export.go b/pkg/scene/export.go index f7426c8bd..f076a14b7 100644 --- a/pkg/scene/export.go +++ b/pkg/scene/export.go @@ -6,6 +6,7 @@ import ( "math" "strconv" + "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/json" "github.com/stashapp/stash/pkg/models/jsonschema" @@ -64,7 +65,7 @@ func ToBasicJSON(ctx context.Context, reader CoverGetter, scene *models.Scene) ( cover, err := reader.GetCover(ctx, scene.ID) if err != nil { - return nil, fmt.Errorf("error getting scene cover: %v", err) + logger.Errorf("Error getting scene cover: %v", err) } if len(cover) > 0 { diff --git a/pkg/scene/export_test.go b/pkg/scene/export_test.go index 623e399a1..684e92db0 100644 --- a/pkg/scene/export_test.go +++ b/pkg/scene/export_test.go @@ -178,8 +178,9 @@ var scenarios = []basicTestScenario{ }, { createFullScene(errImageID), - nil, - true, + createFullJSONScene(""), + // failure to get image should not cause an error + false, }, } diff --git a/pkg/scene/generate/generator.go b/pkg/scene/generate/generator.go index a76c7ce84..49568fb2a 100644 --- a/pkg/scene/generate/generator.go +++ b/pkg/scene/generate/generator.go @@ -38,21 +38,24 @@ type ScenePaths interface { GetVideoPreviewPath(checksum string) string GetWebpPreviewPath(checksum string) string - GetScreenshotPath(checksum string) string - GetThumbnailScreenshotPath(checksum string) string - GetSpriteImageFilePath(checksum string) string GetSpriteVttFilePath(checksum string) string GetTranscodePath(checksum string) string } +type FFMpegConfig interface { + GetTranscodeInputArgs() []string + GetTranscodeOutputArgs() []string +} + type Generator struct { - Encoder ffmpeg.FFMpeg - LockManager *fsutil.ReadLockManager - MarkerPaths MarkerPaths - ScenePaths ScenePaths - Overwrite bool + Encoder *ffmpeg.FFMpeg + FFMpegConfig FFMpegConfig + LockManager *fsutil.ReadLockManager + MarkerPaths MarkerPaths + ScenePaths ScenePaths + Overwrite bool } type generateFn func(lockCtx *fsutil.LockContext, tmpFn string) error @@ -100,6 +103,26 @@ func (g Generator) generateFile(lockCtx *fsutil.LockContext, p Paths, pattern st return nil } +// generateBytes performs a generate operation by generating a temporary file using p and pattern, returns the contents, then deletes it. +func (g Generator) generateBytes(lockCtx *fsutil.LockContext, p Paths, pattern string, generateFn generateFn) ([]byte, error) { + tmpFile, err := g.tempFile(p, pattern) // tmp output in case the process ends abruptly + if err != nil { + return nil, err + } + + tmpFn := tmpFile.Name() + defer func() { + _ = os.Remove(tmpFn) + }() + + if err := generateFn(lockCtx, tmpFn); err != nil { + return nil, err + } + + defer os.Remove(tmpFn) + return os.ReadFile(tmpFn) +} + // generate runs ffmpeg with the given args and waits for it to finish. // Returns an error if the command fails. If the command fails, the return // value will be of type *exec.ExitError. diff --git a/pkg/scene/generate/preview.go b/pkg/scene/generate/preview.go index ff7167f1d..ceefd617c 100644 --- a/pkg/scene/generate/preview.go +++ b/pkg/scene/generate/preview.go @@ -68,7 +68,7 @@ func (g PreviewOptions) getStepSizeAndOffset(videoDuration float64) (stepSize fl return } -func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration float64, hash string, options PreviewOptions, fallback bool) error { +func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration float64, hash string, options PreviewOptions, fallback bool, useVsync2 bool) error { lockCtx := g.LockManager.ReadLock(ctx, input) defer lockCtx.Cancel() @@ -81,7 +81,7 @@ func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration logger.Infof("[generator] generating video preview for %s", input) - if err := g.generateFile(lockCtx, g.ScenePaths, mp4Pattern, output, g.previewVideo(input, videoDuration, options, fallback)); err != nil { + if err := g.generateFile(lockCtx, g.ScenePaths, mp4Pattern, output, g.previewVideo(input, videoDuration, options, fallback, useVsync2)); err != nil { return err } @@ -90,10 +90,10 @@ func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration return nil } -func (g *Generator) previewVideo(input string, videoDuration float64, options PreviewOptions, fallback bool) generateFn { +func (g *Generator) previewVideo(input string, videoDuration float64, options PreviewOptions, fallback bool, useVsync2 bool) generateFn { // #2496 - generate a single preview video for videos shorter than segments * segment duration if videoDuration < options.SegmentDuration*float64(options.Segments) { - return g.previewVideoSingle(input, videoDuration, options, fallback) + return g.previewVideoSingle(input, videoDuration, options, fallback, useVsync2) } return func(lockCtx *fsutil.LockContext, tmpFn string) error { @@ -131,7 +131,7 @@ func (g *Generator) previewVideo(input string, videoDuration float64, options Pr Preset: options.Preset, } - if err := g.previewVideoChunk(lockCtx, input, chunkOptions, fallback); err != nil { + if err := g.previewVideoChunk(lockCtx, input, chunkOptions, fallback, useVsync2); err != nil { return err } } @@ -150,7 +150,7 @@ func (g *Generator) previewVideo(input string, videoDuration float64, options Pr } } -func (g *Generator) previewVideoSingle(input string, videoDuration float64, options PreviewOptions, fallback bool) generateFn { +func (g *Generator) previewVideoSingle(input string, videoDuration float64, options PreviewOptions, fallback bool, useVsync2 bool) generateFn { return func(lockCtx *fsutil.LockContext, tmpFn string) error { chunkOptions := previewChunkOptions{ StartTime: 0, @@ -160,7 +160,7 @@ func (g *Generator) previewVideoSingle(input string, videoDuration float64, opti Preset: options.Preset, } - return g.previewVideoChunk(lockCtx, input, chunkOptions, fallback) + return g.previewVideoChunk(lockCtx, input, chunkOptions, fallback, useVsync2) } } @@ -172,7 +172,7 @@ type previewChunkOptions struct { Preset string } -func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, fn string, options previewChunkOptions, fallback bool) error { +func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, fn string, options previewChunkOptions, fallback bool, useVsync2 bool) error { var videoFilter ffmpeg.VideoFilter videoFilter = videoFilter.ScaleWidth(scenePreviewWidth) @@ -189,6 +189,10 @@ func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, fn string, opt "-strict", "-2", ) + if useVsync2 { + videoArgs = append(videoArgs, "-vsync", "2") + } + trimOptions := transcoder.TranscodeOptions{ OutputPath: options.OutputPath, StartTime: options.StartTime, @@ -199,6 +203,9 @@ func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, fn string, opt VideoCodec: ffmpeg.VideoCodecLibX264, VideoArgs: videoArgs, + + ExtraInputArgs: g.FFMpegConfig.GetTranscodeInputArgs(), + ExtraOutputArgs: g.FFMpegConfig.GetTranscodeOutputArgs(), } if options.Audio { @@ -299,6 +306,9 @@ func (g Generator) previewVideoToImage(input string) generateFn { VideoCodec: ffmpeg.VideoCodecLibWebP, VideoArgs: videoArgs, + + ExtraInputArgs: g.FFMpegConfig.GetTranscodeInputArgs(), + ExtraOutputArgs: g.FFMpegConfig.GetTranscodeOutputArgs(), } args := transcoder.Transcode(input, encodeOptions) diff --git a/pkg/scene/generate/screenshot.go b/pkg/scene/generate/screenshot.go index 41ecc8fe8..917a481ef 100644 --- a/pkg/scene/generate/screenshot.go +++ b/pkg/scene/generate/screenshot.go @@ -9,8 +9,8 @@ import ( ) const ( - thumbnailWidth = 320 - thumbnailQuality = 5 + // thumbnailWidth = 320 + // thumbnailQuality = 5 screenshotQuality = 2 @@ -21,17 +21,10 @@ type ScreenshotOptions struct { At *float64 } -func (g Generator) Screenshot(ctx context.Context, input string, hash string, videoWidth int, videoDuration float64, options ScreenshotOptions) error { +func (g Generator) Screenshot(ctx context.Context, input string, videoWidth int, videoDuration float64, options ScreenshotOptions) ([]byte, error) { lockCtx := g.LockManager.ReadLock(ctx, input) defer lockCtx.Cancel() - output := g.ScenePaths.GetScreenshotPath(hash) - if !g.Overwrite { - if exists, _ := fsutil.FileExists(output); exists { - return nil - } - } - logger.Infof("Creating screenshot for %s", input) at := screenshotDurationProportion * videoDuration @@ -39,46 +32,16 @@ func (g Generator) Screenshot(ctx context.Context, input string, hash string, vi at = *options.At } - if err := g.generateFile(lockCtx, g.ScenePaths, jpgPattern, output, g.screenshot(input, screenshotOptions{ + ret, err := g.generateBytes(lockCtx, g.ScenePaths, jpgPattern, g.screenshot(input, screenshotOptions{ Time: at, Quality: screenshotQuality, // default Width is video width - })); err != nil { - return err + })) + if err != nil { + return nil, err } - logger.Debug("created screenshot: ", output) - - return nil -} - -func (g Generator) Thumbnail(ctx context.Context, input string, hash string, videoDuration float64, options ScreenshotOptions) error { - lockCtx := g.LockManager.ReadLock(ctx, input) - defer lockCtx.Cancel() - - output := g.ScenePaths.GetThumbnailScreenshotPath(hash) - if !g.Overwrite { - if exists, _ := fsutil.FileExists(output); exists { - return nil - } - } - - at := screenshotDurationProportion * videoDuration - if options.At != nil { - at = *options.At - } - - if err := g.generateFile(lockCtx, g.ScenePaths, jpgPattern, output, g.screenshot(input, screenshotOptions{ - Time: at, - Quality: thumbnailQuality, - Width: thumbnailWidth, - })); err != nil { - return err - } - - logger.Debug("created thumbnail: ", output) - - return nil + return ret, nil } type screenshotOptions struct { diff --git a/pkg/scene/generate/transcode.go b/pkg/scene/generate/transcode.go index b772a735b..6528f91da 100644 --- a/pkg/scene/generate/transcode.go +++ b/pkg/scene/generate/transcode.go @@ -86,6 +86,9 @@ func (g Generator) transcode(input string, options TranscodeOptions) generateFn VideoCodec: ffmpeg.VideoCodecLibX264, VideoArgs: videoArgs, AudioCodec: ffmpeg.AudioCodecAAC, + + ExtraInputArgs: g.FFMpegConfig.GetTranscodeInputArgs(), + ExtraOutputArgs: g.FFMpegConfig.GetTranscodeOutputArgs(), }) return g.generate(lockCtx, args) @@ -117,6 +120,9 @@ func (g Generator) transcodeVideo(input string, options TranscodeOptions) genera VideoCodec: ffmpeg.VideoCodecLibX264, VideoArgs: videoArgs, AudioArgs: audioArgs, + + ExtraInputArgs: g.FFMpegConfig.GetTranscodeInputArgs(), + ExtraOutputArgs: g.FFMpegConfig.GetTranscodeOutputArgs(), }) return g.generate(lockCtx, args) diff --git a/pkg/scene/merge.go b/pkg/scene/merge.go index d14f9621f..238d5233c 100644 --- a/pkg/scene/merge.go +++ b/pkg/scene/merge.go @@ -123,7 +123,7 @@ func (s *Service) mergeSceneMarkers(ctx context.Context, dest *models.Scene, src } if len(toRename) > 0 { - txn.AddPostCommitHook(ctx, func(ctx context.Context) error { + txn.AddPostCommitHook(ctx, func(ctx context.Context) { // rename the files if they exist for _, e := range toRename { srcExists, _ := fsutil.FileExists(e.src) @@ -135,8 +135,6 @@ func (s *Service) mergeSceneMarkers(ctx context.Context, dest *models.Scene, src } } } - - return nil }) } diff --git a/pkg/scene/migrate_hash.go b/pkg/scene/migrate_hash.go index 7f39d5370..09b25297f 100644 --- a/pkg/scene/migrate_hash.go +++ b/pkg/scene/migrate_hash.go @@ -16,14 +16,6 @@ func MigrateHash(p *paths.Paths, oldHash string, newHash string) { migrateSceneFiles(oldPath, newPath) scenePaths := p.Scene - oldPath = scenePaths.GetThumbnailScreenshotPath(oldHash) - newPath = scenePaths.GetThumbnailScreenshotPath(newHash) - migrateSceneFiles(oldPath, newPath) - - oldPath = scenePaths.GetScreenshotPath(oldHash) - newPath = scenePaths.GetScreenshotPath(newHash) - migrateSceneFiles(oldPath, newPath) - oldPath = scenePaths.GetVideoPreviewPath(oldHash) newPath = scenePaths.GetVideoPreviewPath(newHash) migrateSceneFiles(oldPath, newPath) diff --git a/pkg/scene/migrate_screenshots.go b/pkg/scene/migrate_screenshots.go new file mode 100644 index 000000000..94d73643f --- /dev/null +++ b/pkg/scene/migrate_screenshots.go @@ -0,0 +1,143 @@ +package scene + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/models" + "github.com/stashapp/stash/pkg/txn" +) + +type MigrateSceneScreenshotsInput struct { + DeleteFiles bool `json:"deleteFiles"` + OverwriteExisting bool `json:"overwriteExisting"` +} + +type HashFinderCoverUpdater interface { + FindByChecksum(ctx context.Context, checksum string) ([]*models.Scene, error) + FindByOSHash(ctx context.Context, oshash string) ([]*models.Scene, error) + CoverUpdater +} + +type ScreenshotMigrator struct { + Options MigrateSceneScreenshotsInput + SceneUpdater HashFinderCoverUpdater + TxnManager txn.Manager +} + +func (m *ScreenshotMigrator) MigrateScreenshots(ctx context.Context, screenshotPath string) error { + // find the scene based on the screenshot path + s, err := m.findScenes(ctx, screenshotPath) + if err != nil { + return fmt.Errorf("finding scenes for screenshot: %w", err) + } + + for _, scene := range s { + // migrate each scene in its own transaction + if err := txn.WithTxn(ctx, m.TxnManager, func(ctx context.Context) error { + return m.migrateSceneScreenshot(ctx, scene, screenshotPath) + }); err != nil { + return fmt.Errorf("migrating screenshot for scene %s: %w", scene.DisplayName(), err) + } + } + + // if deleteFiles is true, delete the file + if m.Options.DeleteFiles { + if err := os.Remove(screenshotPath); err != nil { + // log and continue + logger.Errorf("Error deleting screenshot file %s: %v", screenshotPath, err) + } else { + logger.Debugf("Deleted screenshot file %s", screenshotPath) + } + + // also delete the thumb file + thumbPath := strings.TrimSuffix(screenshotPath, ".jpg") + ".thumb.jpg" + // ignore errors for thumb files + if err := os.Remove(thumbPath); err == nil { + logger.Debugf("Deleted thumb file %s", thumbPath) + } + } + + return nil +} + +func (m *ScreenshotMigrator) findScenes(ctx context.Context, screenshotPath string) ([]*models.Scene, error) { + basename := filepath.Base(screenshotPath) + ext := filepath.Ext(basename) + basename = basename[:len(basename)-len(ext)] + + // use the basename to determine the hash type + algo := m.getHashType(basename) + + if algo == "" { + // log and return + return nil, fmt.Errorf("could not determine hash type") + } + + // use the hash type to get the scene + var ret []*models.Scene + err := txn.WithReadTxn(ctx, m.TxnManager, func(ctx context.Context) error { + var err error + + if algo == models.HashAlgorithmOshash { + // use oshash + ret, err = m.SceneUpdater.FindByOSHash(ctx, basename) + } else { + // use md5 + ret, err = m.SceneUpdater.FindByChecksum(ctx, basename) + } + + return err + }) + + return ret, err +} + +func (m *ScreenshotMigrator) getHashType(basename string) models.HashAlgorithm { + // if the basename is 16 characters long, must be oshash + if len(basename) == 16 { + return models.HashAlgorithmOshash + } + + // if its 32 characters long, must be md5 + if len(basename) == 32 { + return models.HashAlgorithmMd5 + } + + // otherwise, it's undefined + return "" +} + +func (m *ScreenshotMigrator) migrateSceneScreenshot(ctx context.Context, scene *models.Scene, screenshotPath string) error { + if !m.Options.OverwriteExisting { + // check if the scene has a cover already + hasCover, err := m.SceneUpdater.HasCover(ctx, scene.ID) + if err != nil { + return fmt.Errorf("checking for existing cover: %w", err) + } + + if hasCover { + // already has cover, just silently return + logger.Debugf("Scene %s already has a screenshot, skipping", scene.DisplayName()) + return nil + } + } + + // get the data from the file + data, err := os.ReadFile(screenshotPath) + if err != nil { + return fmt.Errorf("reading screenshot file: %w", err) + } + + if err := m.SceneUpdater.UpdateCover(ctx, scene.ID, data); err != nil { + return fmt.Errorf("updating scene screenshot: %w", err) + } + + logger.Infof("Updated screenshot for scene %s from %s", scene.DisplayName(), filepath.Base(screenshotPath)) + + return nil +} diff --git a/pkg/scene/scan.go b/pkg/scene/scan.go index b0f9ef3d4..5ccdee256 100644 --- a/pkg/scene/scan.go +++ b/pkg/scene/scan.go @@ -7,6 +7,7 @@ import ( "time" "github.com/stashapp/stash/pkg/file" + "github.com/stashapp/stash/pkg/file/video" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/paths" @@ -34,8 +35,8 @@ type ScanGenerator interface { type ScanHandler struct { CreatorUpdater CreatorUpdater - CoverGenerator CoverGenerator ScanGenerator ScanGenerator + CaptionUpdater video.CaptionUpdater PluginCache *plugin.Cache FileNamingAlgorithm models.HashAlgorithm @@ -46,12 +47,12 @@ func (h *ScanHandler) validate() error { if h.CreatorUpdater == nil { return errors.New("CreatorUpdater is required") } - if h.CoverGenerator == nil { - return errors.New("CoverGenerator is required") - } if h.ScanGenerator == nil { return errors.New("ScanGenerator is required") } + if h.CaptionUpdater == nil { + return errors.New("CaptionUpdater is required") + } if !h.FileNamingAlgorithm.IsValid() { return errors.New("FileNamingAlgorithm is required") } @@ -72,6 +73,12 @@ func (h *ScanHandler) Handle(ctx context.Context, f file.File, oldFile file.File return ErrNotVideoFile } + if oldFile != nil { + if err := video.CleanCaptions(ctx, videoFile, nil, h.CaptionUpdater); err != nil { + return fmt.Errorf("cleaning captions: %w", err) + } + } + // try to match the file to a scene existing, err := h.CreatorUpdater.FindByFileID(ctx, f.Base().ID) if err != nil { @@ -121,20 +128,13 @@ func (h *ScanHandler) Handle(ctx context.Context, f file.File, oldFile file.File } // do this after the commit so that cover generation doesn't hold up the transaction - txn.AddPostCommitHook(ctx, func(ctx context.Context) error { + txn.AddPostCommitHook(ctx, func(ctx context.Context) { for _, s := range existing { - if err := h.CoverGenerator.GenerateCover(ctx, s, videoFile); err != nil { - // just log if cover generation fails. We can try again on rescan - logger.Errorf("Error generating cover for %s: %v", videoFile.Path, err) - } - if err := h.ScanGenerator.Generate(ctx, s, videoFile); err != nil { // just log if cover generation fails. We can try again on rescan logger.Errorf("Error generating content for %s: %v", videoFile.Path, err) } } - - return nil }) return nil diff --git a/pkg/scene/screenshot.go b/pkg/scene/screenshot.go deleted file mode 100644 index 8335c53d6..000000000 --- a/pkg/scene/screenshot.go +++ /dev/null @@ -1,88 +0,0 @@ -package scene - -import ( - "bytes" - "context" - "image" - "image/jpeg" - "os" - - "github.com/stashapp/stash/pkg/file" - "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/models/paths" - - "github.com/disintegration/imaging" - - // needed to decode other image formats - _ "image/gif" - _ "image/png" -) - -type CoverGenerator interface { - GenerateCover(ctx context.Context, scene *models.Scene, f *file.VideoFile) error -} - -type ScreenshotSetter interface { - SetScreenshot(scene *models.Scene, imageData []byte) error -} - -type PathsCoverSetter struct { - Paths *paths.Paths - FileNamingAlgorithm models.HashAlgorithm -} - -func (ss *PathsCoverSetter) SetScreenshot(scene *models.Scene, imageData []byte) error { - // don't set where scene has no file - if scene.Path == "" { - return nil - } - checksum := scene.GetHash(ss.FileNamingAlgorithm) - return SetScreenshot(ss.Paths, checksum, imageData) -} - -func writeImage(path string, imageData []byte) error { - f, err := os.Create(path) - if err != nil { - return err - } - defer f.Close() - - _, err = f.Write(imageData) - return err -} - -func writeThumbnail(path string, thumbnail image.Image) error { - f, err := os.Create(path) - if err != nil { - return err - } - defer f.Close() - - return jpeg.Encode(f, thumbnail, nil) -} - -func SetScreenshot(paths *paths.Paths, checksum string, imageData []byte) error { - thumbPath := paths.Scene.GetThumbnailScreenshotPath(checksum) - normalPath := paths.Scene.GetScreenshotPath(checksum) - - img, _, err := image.Decode(bytes.NewReader(imageData)) - if err != nil { - return err - } - - // resize to 320 width maintaining aspect ratio, for the thumbnail - const width = 320 - origWidth := img.Bounds().Max.X - origHeight := img.Bounds().Max.Y - height := width / origWidth * origHeight - - thumbnail := imaging.Resize(img, width, height, imaging.Lanczos) - err = writeThumbnail(thumbPath, thumbnail) - if err != nil { - return err - } - - err = writeImage(normalPath, imageData) - - return err -} diff --git a/pkg/scene/service.go b/pkg/scene/service.go index c162858af..a3d01dd3d 100644 --- a/pkg/scene/service.go +++ b/pkg/scene/service.go @@ -22,6 +22,7 @@ type Creator interface { } type CoverUpdater interface { + HasCover(ctx context.Context, sceneID int) (bool, error) UpdateCover(ctx context.Context, sceneID int, cover []byte) error } diff --git a/pkg/scene/update.go b/pkg/scene/update.go index c38597da7..e3f3e252b 100644 --- a/pkg/scene/update.go +++ b/pkg/scene/update.go @@ -46,7 +46,7 @@ func (u *UpdateSet) IsEmpty() bool { // Update updates a scene by updating the fields in the Partial field, then // updates non-nil relationships. Returns an error if there is no work to // be done. -func (u *UpdateSet) Update(ctx context.Context, qb Updater, screenshotSetter ScreenshotSetter) (*models.Scene, error) { +func (u *UpdateSet) Update(ctx context.Context, qb Updater) (*models.Scene, error) { if u.IsEmpty() { return nil, ErrEmptyUpdater } @@ -64,10 +64,6 @@ func (u *UpdateSet) Update(ctx context.Context, qb Updater, screenshotSetter Scr if err := qb.UpdateCover(ctx, u.ID, u.CoverImage); err != nil { return nil, fmt.Errorf("error updating scene cover: %w", err) } - - if err := screenshotSetter.SetScreenshot(ret, u.CoverImage); err != nil { - return nil, fmt.Errorf("error setting scene screenshot: %w", err) - } } return ret, nil diff --git a/pkg/scene/update_test.go b/pkg/scene/update_test.go index ffd84f00c..c89be66f3 100644 --- a/pkg/scene/update_test.go +++ b/pkg/scene/update_test.go @@ -93,12 +93,6 @@ func TestUpdater_IsEmpty(t *testing.T) { } } -type mockScreenshotSetter struct{} - -func (s *mockScreenshotSetter) SetScreenshot(scene *models.Scene, imageData []byte) error { - return nil -} - func TestUpdater_Update(t *testing.T) { const ( sceneID = iota + 1 @@ -210,7 +204,7 @@ func TestUpdater_Update(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.u.Update(ctx, &qb, &mockScreenshotSetter{}) + got, err := tt.u.Update(ctx, &qb) if (err != nil) != tt.wantErr { t.Errorf("Updater.Update() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/pkg/scraper/cache.go b/pkg/scraper/cache.go index 894286c3c..3b5391994 100644 --- a/pkg/scraper/cache.go +++ b/pkg/scraper/cache.go @@ -41,6 +41,7 @@ type GlobalConfig interface { GetScraperCDPPath() string GetScraperCertCheck() bool GetPythonPath() string + GetProxy() string } func isCDPPathHTTP(c GlobalConfig) bool { @@ -96,6 +97,7 @@ func newClient(gc GlobalConfig) *http.Client { Transport: &http.Transport{ // ignore insecure certificates TLSClientConfig: &tls.Config{InsecureSkipVerify: !gc.GetScraperCertCheck()}, MaxIdleConnsPerHost: maxIdleConnsPerHost, + Proxy: http.ProxyFromEnvironment, }, Timeout: scrapeGetTimeout, // defaultCheckRedirect code with max changed from 10 to maxRedirects diff --git a/pkg/scraper/cookies.go b/pkg/scraper/cookies.go index 72855441f..b5822b62c 100644 --- a/pkg/scraper/cookies.go +++ b/pkg/scraper/cookies.go @@ -69,7 +69,7 @@ var characters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123 func randomSequence(n int) string { b := make([]rune, n) - rand.Seed(time.Now().UnixNano()) + rand := rand.New(rand.NewSource(time.Now().UnixNano())) for i := range b { b[i] = characters[rand.Intn(len(characters))] } diff --git a/pkg/scraper/mapped.go b/pkg/scraper/mapped.go index 5fd0d86bc..77c4911ae 100644 --- a/pkg/scraper/mapped.go +++ b/pkg/scraper/mapped.go @@ -822,7 +822,7 @@ func (s mappedScraper) scrapePerformers(ctx context.Context, q mappedQuery) ([]* return ret, nil } -func (s mappedScraper) processScene(ctx context.Context, q mappedQuery, r mappedResult) *ScrapedScene { +func (s mappedScraper) processScene(ctx context.Context, q mappedQuery, r mappedResult, resultIndex int) *ScrapedScene { var ret ScrapedScene sceneScraperConfig := s.Scene @@ -876,9 +876,10 @@ func (s mappedScraper) processScene(ctx context.Context, q mappedQuery, r mapped logger.Debug(`Processing scene studio:`) studioResults := sceneStudioMap.process(ctx, q, s.Common) - if len(studioResults) > 0 { + if len(studioResults) > 0 && resultIndex < len(studioResults) { studio := &models.ScrapedStudio{} - studioResults[0].apply(studio) + // when doing a `search` scrape get the related studio + studioResults[resultIndex].apply(studio) ret.Studio = studio } } @@ -908,9 +909,9 @@ func (s mappedScraper) scrapeScenes(ctx context.Context, q mappedQuery) ([]*Scra logger.Debug(`Processing scenes:`) results := sceneMap.process(ctx, q, s.Common) - for _, r := range results { + for i, r := range results { logger.Debug(`Processing scene:`) - ret = append(ret, s.processScene(ctx, q, r)) + ret = append(ret, s.processScene(ctx, q, r, i)) } return ret, nil @@ -928,7 +929,7 @@ func (s mappedScraper) scrapeScene(ctx context.Context, q mappedQuery) (*Scraped logger.Debug(`Processing scene:`) results := sceneMap.process(ctx, q, s.Common) if len(results) > 0 { - ret = s.processScene(ctx, q, results[0]) + ret = s.processScene(ctx, q, results[0], 0) } return ret, nil diff --git a/pkg/scraper/performer.go b/pkg/scraper/performer.go index f97250736..48f6ce318 100644 --- a/pkg/scraper/performer.go +++ b/pkg/scraper/performer.go @@ -2,26 +2,27 @@ package scraper type ScrapedPerformerInput struct { // Set if performer matched - StoredID *string `json:"stored_id"` - Name *string `json:"name"` - Gender *string `json:"gender"` - URL *string `json:"url"` - Twitter *string `json:"twitter"` - Instagram *string `json:"instagram"` - Birthdate *string `json:"birthdate"` - Ethnicity *string `json:"ethnicity"` - Country *string `json:"country"` - EyeColor *string `json:"eye_color"` - Height *string `json:"height"` - Measurements *string `json:"measurements"` - FakeTits *string `json:"fake_tits"` - CareerLength *string `json:"career_length"` - Tattoos *string `json:"tattoos"` - Piercings *string `json:"piercings"` - Aliases *string `json:"aliases"` - Details *string `json:"details"` - DeathDate *string `json:"death_date"` - HairColor *string `json:"hair_color"` - Weight *string `json:"weight"` - RemoteSiteID *string `json:"remote_site_id"` + StoredID *string `json:"stored_id"` + Name *string `json:"name"` + Disambiguation *string `json:"disambiguation"` + Gender *string `json:"gender"` + URL *string `json:"url"` + Twitter *string `json:"twitter"` + Instagram *string `json:"instagram"` + Birthdate *string `json:"birthdate"` + Ethnicity *string `json:"ethnicity"` + Country *string `json:"country"` + EyeColor *string `json:"eye_color"` + Height *string `json:"height"` + Measurements *string `json:"measurements"` + FakeTits *string `json:"fake_tits"` + CareerLength *string `json:"career_length"` + Tattoos *string `json:"tattoos"` + Piercings *string `json:"piercings"` + Aliases *string `json:"aliases"` + Details *string `json:"details"` + DeathDate *string `json:"death_date"` + HairColor *string `json:"hair_color"` + Weight *string `json:"weight"` + RemoteSiteID *string `json:"remote_site_id"` } diff --git a/pkg/scraper/stashbox/graphql/generated_client.go b/pkg/scraper/stashbox/graphql/generated_client.go index cc30ab136..9861769a8 100644 --- a/pkg/scraper/stashbox/graphql/generated_client.go +++ b/pkg/scraper/stashbox/graphql/generated_client.go @@ -239,48 +239,11 @@ const FindSceneByFingerprintDocument = `query FindSceneByFingerprint ($fingerpri ... SceneFragment } } -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment SceneFragment on Scene { +fragment ImageFragment on Image { id - title - code - details - director - duration - date - urls { - ... URLFragment - } - images { - ... ImageFragment - } - studio { - ... StudioFragment - } - tags { - ... TagFragment - } - performers { - ... PerformerAppearanceFragment - } - fingerprints { - ... FingerprintFragment - } -} -fragment StudioFragment on Studio { - name - id - urls { - ... URLFragment - } - images { - ... ImageFragment - } + url + width + height } fragment PerformerFragment on Performer { id @@ -316,10 +279,53 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } +fragment SceneFragment on Scene { + id + title + code + details + director + duration + date + urls { + ... URLFragment + } + images { + ... ImageFragment + } + studio { + ... StudioFragment + } + tags { + ... TagFragment + } + performers { + ... PerformerAppearanceFragment + } + fingerprints { + ... FingerprintFragment + } +} +fragment URLFragment on URL { + url + type +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} fragment FuzzyDateFragment on FuzzyDate { date accuracy } +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} fragment BodyModificationFragment on BodyModification { location description @@ -329,26 +335,20 @@ fragment FingerprintFragment on Fingerprint { hash duration } -fragment URLFragment on URL { - url - type -} -fragment ImageFragment on Image { +fragment StudioFragment on Studio { + name id - url - width - height + urls { + ... URLFragment + } + images { + ... ImageFragment + } } fragment TagFragment on Tag { name id } -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} ` func (c *Client) FindSceneByFingerprint(ctx context.Context, fingerprint FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByFingerprint, error) { @@ -369,26 +369,6 @@ const FindScenesByFullFingerprintsDocument = `query FindScenesByFullFingerprints ... SceneFragment } } -fragment URLFragment on URL { - url - type -} -fragment ImageFragment on Image { - id - url - width - height -} -fragment StudioFragment on Studio { - name - id - urls { - ... URLFragment - } - images { - ... ImageFragment - } -} fragment PerformerFragment on Performer { id name @@ -423,9 +403,15 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } -fragment BodyModificationFragment on BodyModification { - location - description +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip } fragment SceneFragment on Scene { id @@ -454,31 +440,45 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } +fragment URLFragment on URL { + url + type } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy +fragment ImageFragment on Image { + id + url + width + height } -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip +fragment BodyModificationFragment on BodyModification { + location + description } fragment FingerprintFragment on Fingerprint { algorithm hash duration } +fragment StudioFragment on Studio { + name + id + urls { + ... URLFragment + } + images { + ... ImageFragment + } +} fragment TagFragment on Tag { name id } +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} ` func (c *Client) FindScenesByFullFingerprints(ctx context.Context, fingerprints []*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesByFullFingerprints, error) { @@ -499,12 +499,6 @@ const FindScenesBySceneFingerprintsDocument = `query FindScenesBySceneFingerprin ... SceneFragment } } -fragment ImageFragment on Image { - id - url - width - height -} fragment StudioFragment on Studio { name id @@ -515,12 +509,6 @@ fragment StudioFragment on Studio { ... ImageFragment } } -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} fragment FuzzyDateFragment on FuzzyDate { date accuracy @@ -534,9 +522,17 @@ fragment URLFragment on URL { url type } -fragment TagFragment on Tag { - name +fragment ImageFragment on Image { id + url + width + height +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } } fragment PerformerFragment on Performer { id @@ -609,6 +605,10 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } +fragment TagFragment on Tag { + name + id +} ` func (c *Client) FindScenesBySceneFingerprints(ctx context.Context, fingerprints [][]*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesBySceneFingerprints, error) { @@ -629,21 +629,6 @@ const SearchSceneDocument = `query SearchScene ($term: String!) { ... SceneFragment } } -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} fragment SceneFragment on Scene { id title @@ -675,12 +660,42 @@ fragment URLFragment on URL { url type } +fragment TagFragment on Tag { + name + id +} +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} fragment ImageFragment on Image { id url width height } +fragment StudioFragment on Studio { + name + id + urls { + ... URLFragment + } + images { + ... ImageFragment + } +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} fragment PerformerFragment on Performer { id name @@ -715,29 +730,14 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy +fragment BodyModificationFragment on BodyModification { + location + description } -fragment StudioFragment on Studio { - name - id - urls { - ... URLFragment - } - images { - ... ImageFragment - } -} -fragment TagFragment on Tag { - name - id -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration } ` @@ -759,12 +759,6 @@ const SearchPerformerDocument = `query SearchPerformer ($term: String!) { ... PerformerFragment } } -fragment ImageFragment on Image { - id - url - width - height -} fragment FuzzyDateFragment on FuzzyDate { date accuracy @@ -817,6 +811,12 @@ fragment URLFragment on URL { url type } +fragment ImageFragment on Image { + id + url + width + height +} ` func (c *Client) SearchPerformer(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchPerformer, error) { @@ -915,6 +915,69 @@ const FindSceneByIDDocument = `query FindSceneByID ($id: ID!) { ... SceneFragment } } +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration +} +fragment URLFragment on URL { + url + type +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} +fragment TagFragment on Tag { + name + id +} +fragment PerformerFragment on Performer { + id + name + disambiguation + aliases + gender + merged_ids + urls { + ... URLFragment + } + images { + ... ImageFragment + } + birthdate { + ... FuzzyDateFragment + } + ethnicity + country + eye_color + hair_color + height + measurements { + ... MeasurementsFragment + } + breast_type + career_start_year + career_end_year + tattoos { + ... BodyModificationFragment + } + piercings { + ... BodyModificationFragment + } +} +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} fragment BodyModificationFragment on BodyModification { location description @@ -962,69 +1025,6 @@ fragment StudioFragment on Studio { ... ImageFragment } } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment URLFragment on URL { - url - type -} -fragment TagFragment on Tag { - name - id -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} -fragment PerformerFragment on Performer { - id - name - disambiguation - aliases - gender - merged_ids - urls { - ... URLFragment - } - images { - ... ImageFragment - } - birthdate { - ... FuzzyDateFragment - } - ethnicity - country - eye_color - hair_color - height - measurements { - ... MeasurementsFragment - } - breast_type - career_start_year - career_end_year - tattoos { - ... BodyModificationFragment - } - piercings { - ... BodyModificationFragment - } -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} ` func (c *Client) FindSceneByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByID, error) { diff --git a/pkg/scraper/stashbox/graphql/generated_models.go b/pkg/scraper/stashbox/graphql/generated_models.go index 6bb572edb..6b3e09565 100644 --- a/pkg/scraper/stashbox/graphql/generated_models.go +++ b/pkg/scraper/stashbox/graphql/generated_models.go @@ -88,9 +88,9 @@ type DraftEntity struct { ID *string `json:"id,omitempty"` } -func (DraftEntity) IsSceneDraftTag() {} -func (DraftEntity) IsSceneDraftStudio() {} func (DraftEntity) IsSceneDraftPerformer() {} +func (DraftEntity) IsSceneDraftStudio() {} +func (DraftEntity) IsSceneDraftTag() {} type DraftEntityInput struct { Name string `json:"name"` @@ -339,8 +339,8 @@ type Performer struct { Updated time.Time `json:"updated"` } -func (Performer) IsEditTarget() {} func (Performer) IsSceneDraftPerformer() {} +func (Performer) IsEditTarget() {} type PerformerAppearance struct { Performer *Performer `json:"performer,omitempty"` diff --git a/pkg/scraper/stashbox/graphql/override.go b/pkg/scraper/stashbox/graphql/override.go index d80b74307..f9b0a2146 100644 --- a/pkg/scraper/stashbox/graphql/override.go +++ b/pkg/scraper/stashbox/graphql/override.go @@ -7,7 +7,9 @@ import "github.com/99designs/gqlgen/graphql" type SceneDraftInput struct { ID *string `json:"id,omitempty"` Title *string `json:"title,omitempty"` + Code *string `json:"code,omitempty"` Details *string `json:"details,omitempty"` + Director *string `json:"director,omitempty"` URL *string `json:"url,omitempty"` Date *string `json:"date,omitempty"` Studio *DraftEntityInput `json:"studio,omitempty"` diff --git a/pkg/scraper/stashbox/stash_box.go b/pkg/scraper/stashbox/stash_box.go index a9e3cf54f..9d8e65d0c 100644 --- a/pkg/scraper/stashbox/stash_box.go +++ b/pkg/scraper/stashbox/stash_box.go @@ -10,7 +10,6 @@ import ( "io" "mime/multipart" "net/http" - "os" "strconv" "strings" @@ -20,7 +19,6 @@ import ( "github.com/Yamashou/gqlgenc/graphqljson" "github.com/stashapp/stash/pkg/file" - "github.com/stashapp/stash/pkg/fsutil" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/match" "github.com/stashapp/stash/pkg/models" @@ -43,6 +41,7 @@ type PerformerReader interface { match.PerformerFinder Find(ctx context.Context, id int) (*models.Performer, error) FindBySceneID(ctx context.Context, sceneID int) ([]*models.Performer, error) + models.AliasLoader models.StashIDLoader GetImage(ctx context.Context, performerID int) ([]byte, error) } @@ -234,7 +233,7 @@ func (c Client) findStashBoxScenesByFingerprints(ctx context.Context, scenes [][ } } - return results, nil + return ret, nil } func (c Client) SubmitStashBoxFingerprints(ctx context.Context, sceneIDs []string, endpoint string) (bool, error) { @@ -606,15 +605,16 @@ func performerFragmentToScrapedScenePerformer(p graphql.PerformerFragment) *mode images = append(images, image.URL) } sp := &models.ScrapedPerformer{ - Name: &p.Name, - Country: p.Country, - Measurements: formatMeasurements(p.Measurements), - CareerLength: formatCareerLength(p.CareerStartYear, p.CareerEndYear), - Tattoos: formatBodyModifications(p.Tattoos), - Piercings: formatBodyModifications(p.Piercings), - Twitter: findURL(p.Urls, "TWITTER"), - RemoteSiteID: &id, - Images: images, + Name: &p.Name, + Disambiguation: p.Disambiguation, + Country: p.Country, + Measurements: formatMeasurements(p.Measurements), + CareerLength: formatCareerLength(p.CareerStartYear, p.CareerEndYear), + Tattoos: formatBodyModifications(p.Tattoos), + Piercings: formatBodyModifications(p.Piercings), + Twitter: findURL(p.Urls, "TWITTER"), + RemoteSiteID: &id, + Images: images, // TODO - tags not currently supported // graphql schema change to accommodate this. Leave off for now. } @@ -705,6 +705,13 @@ func (c Client) sceneFragmentToScrapedScene(ctx context.Context, s *graphql.Scen ss.Image = getFirstImage(ctx, c.getHTTPClient(), s.Images) } + if ss.URL == nil && len(s.Urls) > 0 { + // The scene in Stash-box may not have a Studio URL but it does have another URL. + // For example it has a www.manyvids.com URL, which is auto set as type ManyVids. + // This should be re-visited once Stashapp can support more than one URL. + ss.URL = &s.Urls[0].URL + } + if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error { pqb := c.repository.Performer tqb := c.repository.Tag @@ -795,7 +802,7 @@ func appendFingerprintUnique(v []*graphql.FingerprintInput, toAdd *graphql.Finge return append(v, toAdd) } -func (c Client) SubmitSceneDraft(ctx context.Context, scene *models.Scene, endpoint string, imagePath string) (*string, error) { +func (c Client) SubmitSceneDraft(ctx context.Context, scene *models.Scene, endpoint string, cover []byte) (*string, error) { draft := graphql.SceneDraftInput{} var image io.Reader r := c.repository @@ -805,9 +812,15 @@ func (c Client) SubmitSceneDraft(ctx context.Context, scene *models.Scene, endpo if scene.Title != "" { draft.Title = &scene.Title } + if scene.Code != "" { + draft.Code = &scene.Code + } if scene.Details != "" { draft.Details = &scene.Details } + if scene.Director != "" { + draft.Director = &scene.Director + } if scene.URL != "" && len(strings.TrimSpace(scene.URL)) > 0 { url := strings.TrimSpace(scene.URL) draft.URL = &url @@ -919,14 +932,8 @@ func (c Client) SubmitSceneDraft(ctx context.Context, scene *models.Scene, endpo } draft.Tags = tags - if imagePath != "" { - exists, _ := fsutil.FileExists(imagePath) - if exists { - file, err := os.Open(imagePath) - if err == nil { - image = file - } - } + if len(cover) > 0 { + image = bytes.NewReader(cover) } if err := scene.LoadStashIDs(ctx, r.Scene); err != nil { @@ -964,6 +971,15 @@ func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Perf draft := graphql.PerformerDraftInput{} var image io.Reader pqb := c.repository.Performer + + if err := performer.LoadAliases(ctx, pqb); err != nil { + return nil, err + } + + if err := performer.LoadStashIDs(ctx, pqb); err != nil { + return nil, err + } + img, _ := pqb.GetImage(ctx, performer.ID) if img != nil { image = bytes.NewReader(img) @@ -972,6 +988,10 @@ func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Perf if performer.Name != "" { draft.Name = performer.Name } + // stash-box does not support Disambiguation currently + // if performer.Disambiguation != "" { + // draft.Disambiguation = performer.Disambiguation + // } if performer.Birthdate != nil { d := performer.Birthdate.String() draft.Birthdate = &d @@ -1008,8 +1028,20 @@ func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Perf if performer.Tattoos != "" { draft.Tattoos = &performer.Tattoos } - if performer.Aliases != "" { - draft.Aliases = &performer.Aliases + if len(performer.Aliases.List()) > 0 { + aliases := strings.Join(performer.Aliases.List(), ",") + draft.Aliases = &aliases + } + if performer.CareerLength != "" { + var career = strings.Split(performer.CareerLength, "-") + if i, err := strconv.Atoi(strings.TrimSpace(career[0])); err == nil { + draft.CareerStartYear = &i + } + if len(career) == 2 { + if y, err := strconv.Atoi(strings.TrimSpace(career[1])); err == nil { + draft.CareerEndYear = &y + } + } } var urls []string diff --git a/pkg/scraper/url.go b/pkg/scraper/url.go index ddc63a8fb..b53d7b27f 100644 --- a/pkg/scraper/url.go +++ b/pkg/scraper/url.go @@ -9,10 +9,12 @@ import ( "net/http" "net/url" "os" + "regexp" "strings" "time" "github.com/chromedp/cdproto/cdp" + "github.com/chromedp/cdproto/fetch" "github.com/chromedp/cdproto/network" "github.com/chromedp/chromedp" jsoniter "github.com/json-iterator/go" @@ -157,6 +159,11 @@ func urlFromCDP(ctx context.Context, urlCDP string, driverOptions scraperDriverO chromedp.UserDataDir(dir), chromedp.ExecPath(cdpPath), ) + if globalConfig.GetProxy() != "" { + url, _, _ := splitProxyAuth(globalConfig.GetProxy()) + opts = append(opts, chromedp.ProxyServer(url)) + } + ctx, cancelAct = chromedp.NewExecAllocator(ctx, opts...) } @@ -173,6 +180,39 @@ func urlFromCDP(ctx context.Context, urlCDP string, driverOptions scraperDriverO var res string headers := cdpHeaders(driverOptions) + if proxyUsesAuth(globalConfig.GetProxy()) { + _, user, pass := splitProxyAuth(globalConfig.GetProxy()) + + // Based on https://github.com/chromedp/examples/blob/master/proxy/main.go + lctx, lcancel := context.WithCancel(ctx) + chromedp.ListenTarget(lctx, func(ev interface{}) { + switch ev := ev.(type) { + case *fetch.EventRequestPaused: + go func() { + _ = chromedp.Run(ctx, fetch.ContinueRequest(ev.RequestID)) + }() + case *fetch.EventAuthRequired: + if ev.AuthChallenge.Source == fetch.AuthChallengeSourceProxy { + go func() { + _ = chromedp.Run(ctx, + fetch.ContinueWithAuth(ev.RequestID, &fetch.AuthChallengeResponse{ + Response: fetch.AuthChallengeResponseResponseProvideCredentials, + Username: user, + Password: pass, + }), + // Chrome will remember the credential for the current instance, + // so we can disable the fetch domain once credential is provided. + // Please file an issue if Chrome does not work in this way. + fetch.Disable(), + ) + // and cancel the event handler too. + lcancel() + }() + } + } + }) + } + err := chromedp.Run(ctx, network.Enable(), setCDPCookies(driverOptions), @@ -260,3 +300,32 @@ func cdpHeaders(driverOptions scraperDriverOptions) map[string]interface{} { } return headers } + +func proxyUsesAuth(proxyUrl string) bool { + if proxyUrl == "" { + return false + } + reg := regexp.MustCompile(`^(https?:\/\/)(([\P{Cc}]+):([\P{Cc}]+)@)?(([a-zA-Z0-9][a-zA-Z0-9.-]*)(:[0-9]{1,5})?)`) + matches := reg.FindAllStringSubmatch(proxyUrl, -1) + if matches != nil { + split := matches[0] + return len(split) == 0 || (len(split) > 5 && split[3] != "") + } + + return false +} + +func splitProxyAuth(proxyUrl string) (string, string, string) { + if proxyUrl == "" { + return "", "", "" + } + reg := regexp.MustCompile(`^(https?:\/\/)(([\P{Cc}]+):([\P{Cc}]+)@)?(([a-zA-Z0-9][a-zA-Z0-9.-]*)(:[0-9]{1,5})?)`) + matches := reg.FindAllStringSubmatch(proxyUrl, -1) + + if matches != nil && len(matches[0]) > 5 { + split := matches[0] + return split[1] + split[5], split[3], split[4] + } + + return proxyUrl, "", "" +} diff --git a/pkg/scraper/xpath_test.go b/pkg/scraper/xpath_test.go index 7120f8574..06b6ad5b6 100644 --- a/pkg/scraper/xpath_test.go +++ b/pkg/scraper/xpath_test.go @@ -834,6 +834,10 @@ func (mockGlobalConfig) GetPythonPath() string { return "" } +func (mockGlobalConfig) GetProxy() string { + return "" +} + func TestSubScrape(t *testing.T) { retHTML := `
diff --git a/pkg/session/authentication.go b/pkg/session/authentication.go index 3d756d7a2..d9a02314b 100644 --- a/pkg/session/authentication.go +++ b/pkg/session/authentication.go @@ -81,6 +81,6 @@ func LogExternalAccessError(err ExternalAccessError) { "You probably forwarded a port from your router. At the very least, add a password to stash in the settings. \n"+ "Stash will not serve requests until you edit config.yml, remove the security_tripwire_accessed_from_public_internet key and restart stash. \n"+ "This behaviour can be overridden (but not recommended) by setting dangerous_allow_public_without_auth to true in config.yml. \n"+ - "More information is available at https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet \n"+ + "More information is available at https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet \n"+ "Stash is not answering any other requests to protect your privacy.", net.IP(err).String()) } diff --git a/pkg/sliceutil/stringslice/string_collections.go b/pkg/sliceutil/stringslice/string_collections.go index f466d911b..3c66c5c48 100644 --- a/pkg/sliceutil/stringslice/string_collections.go +++ b/pkg/sliceutil/stringslice/string_collections.go @@ -1,6 +1,9 @@ package stringslice -import "strconv" +import ( + "strconv" + "strings" +) // https://gobyexample.com/collection-functions @@ -56,6 +59,19 @@ func StrAppendUniques(vs []string, toAdd []string) []string { return vs } +// StrExclude removes all instances of any value in toExclude from the vs string +// slice. It returns the new or unchanged string slice. +func StrExclude(vs []string, toExclude []string) []string { + var ret []string + for _, v := range vs { + if !StrInclude(toExclude, v) { + ret = append(ret, v) + } + } + + return ret +} + // StrUnique returns the vs string slice with non-unique values removed. func StrUnique(vs []string) []string { distinctValues := make(map[string]struct{}) @@ -94,3 +110,13 @@ func StringSliceToIntSlice(ss []string) ([]int, error) { return ret, nil } + +// FromString converts a string to a slice of strings, splitting on the sep character. +// Unlike strings.Split, this function will also trim whitespace from the resulting strings. +func FromString(s string, sep string) []string { + v := strings.Split(s, ",") + for i, vv := range v { + v[i] = strings.TrimSpace(vv) + } + return v +} diff --git a/pkg/sqlite/anonymise.go b/pkg/sqlite/anonymise.go new file mode 100644 index 000000000..c16d1160d --- /dev/null +++ b/pkg/sqlite/anonymise.go @@ -0,0 +1,897 @@ +package sqlite + +import ( + "context" + "crypto/rand" + "database/sql" + "fmt" + "math/big" + "path/filepath" + "strings" + "unicode" + + "github.com/doug-martin/goqu/v9" + "github.com/doug-martin/goqu/v9/exp" + "github.com/jmoiron/sqlx" + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/txn" + "github.com/stashapp/stash/pkg/utils" +) + +const ( + letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + hex = "0123456789abcdef" +) + +type Anonymiser struct { + *Database +} + +func NewAnonymiser(db *Database, outPath string) (*Anonymiser, error) { + if _, err := db.db.Exec(fmt.Sprintf(`VACUUM INTO "%s"`, outPath)); err != nil { + return nil, fmt.Errorf("vacuuming into %s: %w", outPath, err) + } + + newDB := NewDatabase() + if err := newDB.Open(outPath); err != nil { + return nil, fmt.Errorf("opening %s: %w", outPath, err) + } + + return &Anonymiser{Database: newDB}, nil +} + +func (db *Anonymiser) Anonymise(ctx context.Context) error { + if err := func() error { + defer db.Close() + + return utils.Do([]func() error{ + func() error { return db.deleteBlobs() }, + func() error { return db.deleteStashIDs() }, + func() error { return db.anonymiseFolders(ctx) }, + func() error { return db.anonymiseFiles(ctx) }, + func() error { return db.anonymiseFingerprints(ctx) }, + func() error { return db.anonymiseScenes(ctx) }, + func() error { return db.anonymiseMarkers(ctx) }, + func() error { return db.anonymiseImages(ctx) }, + func() error { return db.anonymiseGalleries(ctx) }, + func() error { return db.anonymisePerformers(ctx) }, + func() error { return db.anonymiseStudios(ctx) }, + func() error { return db.anonymiseTags(ctx) }, + func() error { return db.anonymiseMovies(ctx) }, + func() error { db.optimise(); return nil }, + }) + }(); err != nil { + // delete the database + _ = db.Remove() + + return err + } + + return nil +} + +func (db *Anonymiser) truncateColumn(tableName string, column string) error { + _, err := db.db.Exec("UPDATE " + tableName + " SET " + column + " = NULL") + return err +} + +func (db *Anonymiser) truncateTable(tableName string) error { + _, err := db.db.Exec("DELETE FROM " + tableName) + return err +} + +func (db *Anonymiser) deleteBlobs() error { + return utils.Do([]func() error{ + func() error { return db.truncateColumn("tags", "image_blob") }, + func() error { return db.truncateColumn("studios", "image_blob") }, + func() error { return db.truncateColumn("performers", "image_blob") }, + func() error { return db.truncateColumn("scenes", "cover_blob") }, + func() error { return db.truncateColumn("movies", "front_image_blob") }, + func() error { return db.truncateColumn("movies", "back_image_blob") }, + + func() error { return db.truncateTable("blobs") }, + }) +} + +func (db *Anonymiser) deleteStashIDs() error { + return utils.Do([]func() 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 (db *Anonymiser) anonymiseFolders(ctx context.Context) error { + logger.Infof("Anonymising folders") + return txn.WithTxn(ctx, db, func(ctx context.Context) error { + return db.anonymiseFoldersRecurse(ctx, 0, "") + }) +} + +func (db *Anonymiser) anonymiseFoldersRecurse(ctx context.Context, parentFolderID int, parentPath string) error { + table := folderTableMgr.table + + stmt := dialect.Update(table) + + if parentFolderID == 0 { + stmt = stmt.Set(goqu.Record{"path": goqu.Cast(table.Col(idColumn), "VARCHAR")}).Where(table.Col("parent_folder_id").IsNull()) + } else { + stmt = stmt.Prepared(true).Set(goqu.Record{ + "path": goqu.L("? || ? || id", parentPath, string(filepath.Separator)), + }).Where(table.Col("parent_folder_id").Eq(parentFolderID)) + } + + if _, err := exec(ctx, stmt); err != nil { + return fmt.Errorf("anonymising %s: %w", table.GetTable(), err) + } + + // now recurse to sub-folders + query := dialect.From(table).Select(table.Col(idColumn), table.Col("path")) + if parentFolderID == 0 { + query = query.Where(table.Col("parent_folder_id").IsNull()) + } else { + query = query.Where(table.Col("parent_folder_id").Eq(parentFolderID)) + } + + const single = false + return queryFunc(ctx, query, single, func(rows *sqlx.Rows) error { + var id int + var path string + if err := rows.Scan(&id, &path); err != nil { + return err + } + + return db.anonymiseFoldersRecurse(ctx, id, path) + }) +} + +func (db *Anonymiser) anonymiseFiles(ctx context.Context) error { + logger.Infof("Anonymising files") + return txn.WithTxn(ctx, db, func(ctx context.Context) error { + table := fileTableMgr.table + stmt := dialect.Update(table).Set(goqu.Record{"basename": goqu.Cast(table.Col(idColumn), "VARCHAR")}) + + if _, err := exec(ctx, stmt); err != nil { + return fmt.Errorf("anonymising %s: %w", table.GetTable(), err) + } + + return nil + }) +} + +func (db *Anonymiser) anonymiseFingerprints(ctx context.Context) error { + logger.Infof("Anonymising fingerprints") + table := fingerprintTableMgr.table + lastID := 0 + lastType := "" + total := 0 + const logEvery = 10000 + + for gotSome := true; gotSome; { + if err := txn.WithTxn(ctx, db, func(ctx context.Context) error { + query := dialect.From(table).Select( + table.Col(fileIDColumn), + table.Col("type"), + table.Col("fingerprint"), + ).Where(goqu.L("(file_id, type)").Gt(goqu.L("(?, ?)", lastID, lastType))).Limit(1000) + + gotSome = false + + const single = false + return queryFunc(ctx, query, single, func(rows *sqlx.Rows) error { + var ( + id int + typ string + fingerprint string + ) + + if err := rows.Scan( + &id, + &typ, + &fingerprint, + ); err != nil { + return err + } + + if err := db.anonymiseFingerprint(ctx, table, "fingerprint", fingerprint); err != nil { + return err + } + + lastID = id + lastType = typ + + gotSome = true + total++ + + if total%logEvery == 0 { + logger.Infof("Anonymised %d fingerprints", total) + } + + return nil + }) + }); err != nil { + return err + } + } + + return nil +} + +func (db *Anonymiser) anonymiseScenes(ctx context.Context) error { + logger.Infof("Anonymising scenes") + table := sceneTableMgr.table + lastID := 0 + total := 0 + const logEvery = 10000 + + for gotSome := true; gotSome; { + if err := txn.WithTxn(ctx, db, func(ctx context.Context) error { + query := dialect.From(table).Select( + table.Col(idColumn), + table.Col("title"), + table.Col("details"), + table.Col("url"), + table.Col("code"), + table.Col("director"), + ).Where(table.Col(idColumn).Gt(lastID)).Limit(1000) + + gotSome = false + + const single = false + return queryFunc(ctx, query, single, func(rows *sqlx.Rows) error { + var ( + id int + title sql.NullString + details sql.NullString + url sql.NullString + code sql.NullString + director sql.NullString + ) + + if err := rows.Scan( + &id, + &title, + &details, + &url, + &code, + &director, + ); err != nil { + return err + } + + set := goqu.Record{} + + // if title set set new title + db.obfuscateNullString(set, "title", title) + db.obfuscateNullString(set, "details", details) + db.obfuscateNullString(set, "url", url) + + if len(set) > 0 { + stmt := dialect.Update(table).Set(set).Where(table.Col(idColumn).Eq(id)) + + if _, err := exec(ctx, stmt); err != nil { + return fmt.Errorf("anonymising %s: %w", table.GetTable(), err) + } + } + + if code.Valid { + if err := db.anonymiseText(ctx, table, "code", code.String); err != nil { + return err + } + } + + if director.Valid { + if err := db.anonymiseText(ctx, table, "director", director.String); err != nil { + return err + } + } + + lastID = id + gotSome = true + total++ + + if total%logEvery == 0 { + logger.Infof("Anonymised %d scenes", total) + } + + return nil + }) + }); err != nil { + return err + } + } + + return nil +} + +func (db *Anonymiser) anonymiseMarkers(ctx context.Context) error { + logger.Infof("Anonymising scene markers") + table := sceneMarkerTableMgr.table + lastID := 0 + total := 0 + const logEvery = 10000 + + for gotSome := true; gotSome; { + if err := txn.WithTxn(ctx, db, func(ctx context.Context) error { + query := dialect.From(table).Select( + table.Col(idColumn), + table.Col("title"), + ).Where(table.Col(idColumn).Gt(lastID)).Limit(1000) + + gotSome = false + + const single = false + return queryFunc(ctx, query, single, func(rows *sqlx.Rows) error { + var ( + id int + title string + ) + + if err := rows.Scan( + &id, + &title, + ); err != nil { + return err + } + + if err := db.anonymiseText(ctx, table, "title", title); err != nil { + return err + } + + lastID = id + gotSome = true + total++ + + if total%logEvery == 0 { + logger.Infof("Anonymised %d scene markers", total) + } + + return nil + }) + }); err != nil { + return err + } + } + + return nil +} + +func (db *Anonymiser) anonymiseImages(ctx context.Context) error { + logger.Infof("Anonymising images") + table := imageTableMgr.table + lastID := 0 + total := 0 + const logEvery = 10000 + + for gotSome := true; gotSome; { + if err := txn.WithTxn(ctx, db, func(ctx context.Context) error { + query := dialect.From(table).Select( + table.Col(idColumn), + table.Col("title"), + table.Col("url"), + ).Where(table.Col(idColumn).Gt(lastID)).Limit(1000) + + gotSome = false + + const single = false + return queryFunc(ctx, query, single, func(rows *sqlx.Rows) error { + var ( + id int + title sql.NullString + url sql.NullString + ) + + if err := rows.Scan( + &id, + &title, + &url, + ); err != nil { + return err + } + + set := goqu.Record{} + db.obfuscateNullString(set, "title", title) + db.obfuscateNullString(set, "url", url) + + if len(set) > 0 { + stmt := dialect.Update(table).Set(set).Where(table.Col(idColumn).Eq(id)) + + if _, err := exec(ctx, stmt); err != nil { + return fmt.Errorf("anonymising %s: %w", table.GetTable(), err) + } + } + + lastID = id + gotSome = true + total++ + + if total%logEvery == 0 { + logger.Infof("Anonymised %d images", total) + } + + return nil + }) + }); err != nil { + return err + } + } + + return nil +} + +func (db *Anonymiser) anonymiseGalleries(ctx context.Context) error { + logger.Infof("Anonymising galleries") + table := galleryTableMgr.table + lastID := 0 + total := 0 + const logEvery = 10000 + + for gotSome := true; gotSome; { + if err := txn.WithTxn(ctx, db, func(ctx context.Context) error { + query := dialect.From(table).Select( + table.Col(idColumn), + table.Col("title"), + table.Col("details"), + ).Where(table.Col(idColumn).Gt(lastID)).Limit(1000) + + gotSome = false + + const single = false + return queryFunc(ctx, query, single, func(rows *sqlx.Rows) error { + var ( + id int + title sql.NullString + details sql.NullString + ) + + if err := rows.Scan( + &id, + &title, + &details, + ); err != nil { + return err + } + + set := goqu.Record{} + db.obfuscateNullString(set, "title", title) + db.obfuscateNullString(set, "details", details) + + if len(set) > 0 { + stmt := dialect.Update(table).Set(set).Where(table.Col(idColumn).Eq(id)) + + if _, err := exec(ctx, stmt); err != nil { + return fmt.Errorf("anonymising %s: %w", table.GetTable(), err) + } + } + + lastID = id + gotSome = true + total++ + + if total%logEvery == 0 { + logger.Infof("Anonymised %d galleries", total) + } + + return nil + }) + }); err != nil { + return err + } + } + + return nil +} + +func (db *Anonymiser) anonymisePerformers(ctx context.Context) error { + logger.Infof("Anonymising performers") + table := performerTableMgr.table + lastID := 0 + total := 0 + const logEvery = 10000 + + for gotSome := true; gotSome; { + if err := txn.WithTxn(ctx, db, func(ctx context.Context) error { + query := dialect.From(table).Select( + table.Col(idColumn), + table.Col("name"), + table.Col("details"), + table.Col("url"), + table.Col("twitter"), + table.Col("instagram"), + table.Col("tattoos"), + table.Col("piercings"), + ).Where(table.Col(idColumn).Gt(lastID)).Limit(1000) + + gotSome = false + + const single = false + return queryFunc(ctx, query, single, func(rows *sqlx.Rows) error { + var ( + id int + name sql.NullString + details sql.NullString + url sql.NullString + twitter sql.NullString + instagram sql.NullString + tattoos sql.NullString + piercings sql.NullString + ) + + if err := rows.Scan( + &id, + &name, + &details, + &url, + &twitter, + &instagram, + &tattoos, + &piercings, + ); err != nil { + return err + } + + set := goqu.Record{} + db.obfuscateNullString(set, "name", name) + db.obfuscateNullString(set, "details", details) + db.obfuscateNullString(set, "url", url) + db.obfuscateNullString(set, "twitter", twitter) + db.obfuscateNullString(set, "instagram", instagram) + db.obfuscateNullString(set, "tattoos", tattoos) + db.obfuscateNullString(set, "piercings", piercings) + + if len(set) > 0 { + stmt := dialect.Update(table).Set(set).Where(table.Col(idColumn).Eq(id)) + + if _, err := exec(ctx, stmt); err != nil { + return fmt.Errorf("anonymising %s: %w", table.GetTable(), err) + } + } + + lastID = id + gotSome = true + total++ + + if total%logEvery == 0 { + logger.Infof("Anonymised %d performers", total) + } + + return nil + }) + }); err != nil { + return err + } + } + + if err := db.anonymiseAliases(ctx, goqu.T(performersAliasesTable), "performer_id"); err != nil { + return err + } + + return nil +} + +func (db *Anonymiser) anonymiseStudios(ctx context.Context) error { + logger.Infof("Anonymising studios") + table := studioTableMgr.table + lastID := 0 + total := 0 + const logEvery = 10000 + + for gotSome := true; gotSome; { + if err := txn.WithTxn(ctx, db, func(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) + + gotSome = false + + const single = false + return queryFunc(ctx, query, single, func(rows *sqlx.Rows) 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 + } + + set := goqu.Record{} + db.obfuscateNullString(set, "name", name) + db.obfuscateNullString(set, "url", url) + db.obfuscateNullString(set, "details", details) + + if len(set) > 0 { + stmt := dialect.Update(table).Set(set).Where(table.Col(idColumn).Eq(id)) + + if _, err := exec(ctx, stmt); err != nil { + return fmt.Errorf("anonymising %s: %w", table.GetTable(), err) + } + } + + lastID = id + gotSome = true + total++ + + // TODO - anonymise studio aliases + + if total%logEvery == 0 { + logger.Infof("Anonymised %d studios", total) + } + + return nil + }) + }); err != nil { + return err + } + } + + if err := db.anonymiseAliases(ctx, goqu.T(studioAliasesTable), "studio_id"); err != nil { + return err + } + + return nil +} + +func (db *Anonymiser) anonymiseAliases(ctx context.Context, table exp.IdentifierExpression, idColumn string) error { + lastID := 0 + lastAlias := "" + total := 0 + const logEvery = 10000 + + for gotSome := true; gotSome; { + if err := txn.WithTxn(ctx, db, func(ctx context.Context) error { + query := dialect.From(table).Select( + table.Col(idColumn), + table.Col("alias"), + ).Where(goqu.L("(" + idColumn + ", alias)").Gt(goqu.L("(?, ?)", lastID, lastAlias))).Limit(1000) + + gotSome = false + + const single = false + return queryFunc(ctx, query, single, func(rows *sqlx.Rows) error { + var ( + id int + alias sql.NullString + ) + + if err := rows.Scan( + &id, + &alias, + ); err != nil { + return err + } + + set := goqu.Record{} + db.obfuscateNullString(set, "alias", alias) + + if len(set) > 0 { + stmt := dialect.Update(table).Set(set).Where( + table.Col(idColumn).Eq(id), + table.Col("alias").Eq(alias), + ) + + if _, err := exec(ctx, stmt); err != nil { + return fmt.Errorf("anonymising %s: %w", table.GetTable(), err) + } + } + + lastID = id + lastAlias = alias.String + gotSome = true + total++ + + if total%logEvery == 0 { + logger.Infof("Anonymised %d %s aliases", total, table.GetTable()) + } + + return nil + }) + }); err != nil { + return err + } + } + + return nil +} + +func (db *Anonymiser) anonymiseTags(ctx context.Context) error { + logger.Infof("Anonymising tags") + table := tagTableMgr.table + lastID := 0 + total := 0 + const logEvery = 10000 + + for gotSome := true; gotSome; { + if err := txn.WithTxn(ctx, db, func(ctx context.Context) error { + query := dialect.From(table).Select( + table.Col(idColumn), + table.Col("name"), + table.Col("description"), + ).Where(table.Col(idColumn).Gt(lastID)).Limit(1000) + + gotSome = false + + const single = false + return queryFunc(ctx, query, single, func(rows *sqlx.Rows) error { + var ( + id int + name sql.NullString + description sql.NullString + ) + + if err := rows.Scan( + &id, + &name, + &description, + ); err != nil { + return err + } + + set := goqu.Record{} + db.obfuscateNullString(set, "name", name) + db.obfuscateNullString(set, "description", description) + + if len(set) > 0 { + stmt := dialect.Update(table).Set(set).Where(table.Col(idColumn).Eq(id)) + + if _, err := exec(ctx, stmt); err != nil { + return fmt.Errorf("anonymising %s: %w", table.GetTable(), err) + } + } + + lastID = id + gotSome = true + total++ + + if total%logEvery == 0 { + logger.Infof("Anonymised %d tags", total) + } + + return nil + }) + }); err != nil { + return err + } + } + + if err := db.anonymiseAliases(ctx, goqu.T(tagAliasesTable), "tag_id"); err != nil { + return err + } + + return nil +} + +func (db *Anonymiser) anonymiseMovies(ctx context.Context) error { + logger.Infof("Anonymising movies") + table := movieTableMgr.table + lastID := 0 + total := 0 + const logEvery = 10000 + + for gotSome := true; gotSome; { + if err := txn.WithTxn(ctx, db, func(ctx context.Context) error { + query := dialect.From(table).Select( + table.Col(idColumn), + table.Col("name"), + table.Col("aliases"), + table.Col("synopsis"), + table.Col("url"), + table.Col("director"), + ).Where(table.Col(idColumn).Gt(lastID)).Limit(1000) + + gotSome = false + + const single = false + return queryFunc(ctx, query, single, func(rows *sqlx.Rows) error { + var ( + id int + name sql.NullString + aliases sql.NullString + synopsis sql.NullString + url sql.NullString + director sql.NullString + ) + + if err := rows.Scan( + &id, + &name, + &aliases, + &synopsis, + &url, + &director, + ); err != nil { + return err + } + + set := goqu.Record{} + db.obfuscateNullString(set, "name", name) + db.obfuscateNullString(set, "aliases", aliases) + db.obfuscateNullString(set, "synopsis", synopsis) + db.obfuscateNullString(set, "url", url) + db.obfuscateNullString(set, "director", director) + + if len(set) > 0 { + stmt := dialect.Update(table).Set(set).Where(table.Col(idColumn).Eq(id)) + + if _, err := exec(ctx, stmt); err != nil { + return fmt.Errorf("anonymising %s: %w", table.GetTable(), err) + } + } + + lastID = id + gotSome = true + total++ + + if total%logEvery == 0 { + logger.Infof("Anonymised %d movies", total) + } + + return nil + }) + }); err != nil { + return err + } + } + + return nil +} + +func (db *Anonymiser) anonymiseText(ctx context.Context, table exp.IdentifierExpression, column string, value string) error { + set := goqu.Record{} + set[column] = db.obfuscateString(value, letters) + + stmt := dialect.Update(table).Set(set).Where(table.Col(column).Eq(value)) + + if _, err := exec(ctx, stmt); err != nil { + return fmt.Errorf("anonymising %s: %w", column, err) + } + + return nil +} + +func (db *Anonymiser) anonymiseFingerprint(ctx context.Context, table exp.IdentifierExpression, column string, value string) error { + set := goqu.Record{} + set[column] = db.obfuscateString(value, hex) + + stmt := dialect.Update(table).Set(set).Where(table.Col(column).Eq(value)) + + if _, err := exec(ctx, stmt); err != nil { + return fmt.Errorf("anonymising %s: %w", column, err) + } + + return nil +} + +func (db *Anonymiser) obfuscateNullString(out goqu.Record, column string, in sql.NullString) { + if in.Valid { + out[column] = db.obfuscateString(in.String, letters) + } +} + +func (db *Anonymiser) obfuscateString(in string, dict string) string { + out := strings.Builder{} + for _, c := range in { + if unicode.IsSpace(c) { + out.WriteRune(c) + } else { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(dict)))) + if err != nil { + panic("error generating random number") + } + + out.WriteByte(dict[num.Int64()]) + } + } + + return out.String() +} diff --git a/pkg/sqlite/anonymise_test.go b/pkg/sqlite/anonymise_test.go new file mode 100644 index 000000000..868224eef --- /dev/null +++ b/pkg/sqlite/anonymise_test.go @@ -0,0 +1,39 @@ +//go:build integration +// +build integration + +package sqlite_test + +import ( + "context" + "os" + "testing" + + "github.com/stashapp/stash/pkg/sqlite" +) + +func TestAnonymiser_Anonymise(t *testing.T) { + f, err := os.CreateTemp("", "*.sqlite") + if err != nil { + t.Errorf("Could not create temporary file: %v", err) + return + } + + f.Close() + defer os.Remove(f.Name()) + + // use existing database + anonymiser, err := sqlite.NewAnonymiser(db, f.Name()) + if err != nil { + t.Errorf("Could not create anonymiser: %v", err) + return + } + + if err := anonymiser.Anonymise(context.Background()); err != nil { + t.Errorf("Could not anonymise: %v", err) + return + } + + t.Logf("Anonymised database written to %s", f.Name()) + + // TODO - ensure anonymous +} diff --git a/pkg/sqlite/batch.go b/pkg/sqlite/batch.go new file mode 100644 index 000000000..71ad5d354 --- /dev/null +++ b/pkg/sqlite/batch.go @@ -0,0 +1,20 @@ +package sqlite + +const defaultBatchSize = 1000 + +// batchExec executes the provided function in batches of the provided size. +func batchExec(ids []int, batchSize int, fn func(batch []int) error) error { + for i := 0; i < len(ids); i += batchSize { + end := i + batchSize + if end > len(ids) { + end = len(ids) + } + + batch := ids[i:end] + if err := fn(batch); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/sqlite/blob.go b/pkg/sqlite/blob.go new file mode 100644 index 000000000..27ecd94ad --- /dev/null +++ b/pkg/sqlite/blob.go @@ -0,0 +1,414 @@ +package sqlite + +import ( + "context" + "database/sql" + "errors" + "fmt" + "io/fs" + + "github.com/doug-martin/goqu/v9" + "github.com/doug-martin/goqu/v9/exp" + "github.com/jmoiron/sqlx" + "github.com/mattn/go-sqlite3" + "github.com/stashapp/stash/pkg/file" + "github.com/stashapp/stash/pkg/hash/md5" + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/sqlite/blob" + "github.com/stashapp/stash/pkg/utils" + "gopkg.in/guregu/null.v4" +) + +const ( + blobTable = "blobs" + blobChecksumColumn = "checksum" +) + +type BlobStoreOptions struct { + // UseFilesystem should be true if blob data should be stored in the filesystem + UseFilesystem bool + // UseDatabase should be true if blob data should be stored in the database + UseDatabase bool + // Path is the filesystem path to use for storing blobs + Path string +} + +type BlobStore struct { + repository + + tableMgr *table + + fsStore *blob.FilesystemStore + options BlobStoreOptions +} + +func NewBlobStore(options BlobStoreOptions) *BlobStore { + return &BlobStore{ + repository: repository{ + tableName: blobTable, + idColumn: blobChecksumColumn, + }, + + tableMgr: blobTableMgr, + + fsStore: blob.NewFilesystemStore(options.Path, &file.OsFS{}), + options: options, + } +} + +type blobRow struct { + Checksum string `db:"checksum"` + Blob []byte `db:"blob"` +} + +func (qb *BlobStore) table() exp.IdentifierExpression { + return qb.tableMgr.table +} + +func (qb *BlobStore) Count(ctx context.Context) (int, error) { + table := qb.table() + q := dialect.From(table).Select(goqu.COUNT(table.Col(blobChecksumColumn))) + + var ret int + if err := querySimple(ctx, q, &ret); err != nil { + return 0, err + } + + return ret, nil +} + +// Write stores the data and its checksum in enabled stores. +// Always writes at least the checksum to the database. +func (qb *BlobStore) Write(ctx context.Context, data []byte) (string, error) { + if !qb.options.UseDatabase && !qb.options.UseFilesystem { + panic("no blob store configured") + } + + if len(data) == 0 { + return "", fmt.Errorf("cannot write empty data") + } + + checksum := md5.FromBytes(data) + + // only write blob to the database if UseDatabase is true + // always at least write the checksum + var storedData []byte + if qb.options.UseDatabase { + storedData = data + } + + if err := qb.write(ctx, checksum, storedData); err != nil { + return "", fmt.Errorf("writing to database: %w", err) + } + + if qb.options.UseFilesystem { + if err := qb.fsStore.Write(ctx, checksum, data); err != nil { + return "", fmt.Errorf("writing to filesystem: %w", err) + } + } + + return checksum, nil +} + +func (qb *BlobStore) write(ctx context.Context, checksum string, data []byte) error { + table := qb.table() + q := dialect.Insert(table).Prepared(true).Rows(blobRow{ + Checksum: checksum, + Blob: data, + }).OnConflict(goqu.DoNothing()) + + _, err := exec(ctx, q) + if err != nil { + return fmt.Errorf("inserting into %s: %w", table, err) + } + + return nil +} + +func (qb *BlobStore) update(ctx context.Context, checksum string, data []byte) error { + table := qb.table() + q := dialect.Update(table).Prepared(true).Set(goqu.Record{ + "blob": data, + }).Where(goqu.C(blobChecksumColumn).Eq(checksum)) + + _, err := exec(ctx, q) + if err != nil { + return fmt.Errorf("updating %s: %w", table, err) + } + + return nil +} + +type ChecksumNotFoundError struct { + Checksum string +} + +func (e *ChecksumNotFoundError) Error() string { + return fmt.Sprintf("checksum %s does not exist", e.Checksum) +} + +type ChecksumBlobNotExistError struct { + Checksum string +} + +func (e *ChecksumBlobNotExistError) Error() string { + return fmt.Sprintf("blob for checksum %s does not exist", e.Checksum) +} + +func (qb *BlobStore) readSQL(ctx context.Context, querySQL string, args ...interface{}) ([]byte, string, error) { + if !qb.options.UseDatabase && !qb.options.UseFilesystem { + panic("no blob store configured") + } + + // always try to get from the database first, even if set to use filesystem + var row blobRow + found := false + const single = true + if err := qb.queryFunc(ctx, querySQL, args, single, func(r *sqlx.Rows) error { + found = true + if err := r.StructScan(&row); err != nil { + return err + } + + return nil + }); err != nil { + return nil, "", fmt.Errorf("reading from database: %w", err) + } + + if !found { + // not found in the database - does not exist + return nil, "", nil + } + + checksum := row.Checksum + + if row.Blob != nil { + return row.Blob, checksum, nil + } + + // don't use the filesystem if not configured to do so + if qb.options.UseFilesystem { + ret, err := qb.fsStore.Read(ctx, checksum) + if err == nil { + return ret, checksum, nil + } + + if !errors.Is(err, fs.ErrNotExist) { + return nil, checksum, fmt.Errorf("reading from filesystem: %w", err) + } + } + + return nil, checksum, &ChecksumBlobNotExistError{ + Checksum: checksum, + } +} + +// Read reads the data from the database or filesystem, depending on which is enabled. +func (qb *BlobStore) Read(ctx context.Context, checksum string) ([]byte, error) { + if !qb.options.UseDatabase && !qb.options.UseFilesystem { + panic("no blob store configured") + } + + // always try to get from the database first, even if set to use filesystem + ret, err := qb.readFromDatabase(ctx, checksum) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("reading from database: %w", err) + } + + // not found in the database - does not exist + return nil, &ChecksumNotFoundError{ + Checksum: checksum, + } + } + + if ret != nil { + return ret, nil + } + + // don't use the filesystem if not configured to do so + if qb.options.UseFilesystem { + ret, err := qb.fsStore.Read(ctx, checksum) + if err == nil { + return ret, nil + } + + if !errors.Is(err, fs.ErrNotExist) { + return nil, fmt.Errorf("reading from filesystem: %w", err) + } + } + + // blob not found - should not happen + return nil, &ChecksumBlobNotExistError{ + Checksum: checksum, + } +} + +func (qb *BlobStore) readFromDatabase(ctx context.Context, checksum string) ([]byte, error) { + q := dialect.From(qb.table()).Select(qb.table().All()).Where(qb.tableMgr.byID(checksum)) + + var row blobRow + const single = true + if err := queryFunc(ctx, q, single, func(r *sqlx.Rows) error { + if err := r.StructScan(&row); err != nil { + return err + } + + return nil + }); err != nil { + return nil, fmt.Errorf("querying %s: %w", qb.table(), err) + } + + return row.Blob, nil +} + +// Delete marks a checksum as no longer in use by a single reference. +// If no references remain, the blob is deleted from the database and filesystem. +func (qb *BlobStore) Delete(ctx context.Context, checksum string) error { + // try to delete the blob from the database + if err := qb.delete(ctx, checksum); err != nil { + if qb.isConstraintError(err) { + // blob is still referenced - do not delete + logger.Debugf("Blob %s is still referenced - not deleting", checksum) + return nil + } + + // unexpected error + return fmt.Errorf("deleting from database: %w", err) + } + + // blob was deleted from the database - delete from filesystem if enabled + if qb.options.UseFilesystem { + logger.Debugf("Deleting blob %s from filesystem", checksum) + if err := qb.fsStore.Delete(ctx, checksum); err != nil { + return fmt.Errorf("deleting from filesystem: %w", err) + } + } + + return nil +} + +func (qb *BlobStore) isConstraintError(err error) bool { + var sqliteError sqlite3.Error + if errors.As(err, &sqliteError) { + return sqliteError.Code == sqlite3.ErrConstraint + } + return false +} + +func (qb *BlobStore) delete(ctx context.Context, checksum string) error { + table := qb.table() + + q := dialect.Delete(table).Where(goqu.C(blobChecksumColumn).Eq(checksum)) + + _, err := exec(ctx, q) + if err != nil { + return fmt.Errorf("deleting from %s: %w", table, err) + } + + return nil +} + +type blobJoinQueryBuilder struct { + repository + blobStore *BlobStore + + joinTable string +} + +func (qb *blobJoinQueryBuilder) GetImage(ctx context.Context, id int, blobCol string) ([]byte, error) { + sqlQuery := utils.StrFormat(` +SELECT blobs.checksum, blobs.blob FROM {joinTable} INNER JOIN blobs ON {joinTable}.{joinCol} = blobs.checksum +WHERE {joinTable}.id = ? +`, utils.StrFormatMap{ + "joinTable": qb.joinTable, + "joinCol": blobCol, + }) + + ret, _, err := qb.blobStore.readSQL(ctx, sqlQuery, id) + return ret, err +} + +func (qb *blobJoinQueryBuilder) UpdateImage(ctx context.Context, id int, blobCol string, image []byte) error { + if len(image) == 0 { + return qb.DestroyImage(ctx, id, blobCol) + } + + oldChecksum, err := qb.getChecksum(ctx, id, blobCol) + if err != nil { + return err + } + + checksum, err := qb.blobStore.Write(ctx, image) + if err != nil { + return err + } + + sqlQuery := fmt.Sprintf("UPDATE %s SET %s = ? WHERE id = ?", qb.joinTable, blobCol) + if _, err := qb.tx.Exec(ctx, sqlQuery, checksum, id); err != nil { + return err + } + + // #3595 - delete the old blob if the checksum is different + if oldChecksum != nil && *oldChecksum != checksum { + if err := qb.blobStore.Delete(ctx, *oldChecksum); err != nil { + return err + } + } + + return nil +} + +func (qb *blobJoinQueryBuilder) getChecksum(ctx context.Context, id int, blobCol string) (*string, error) { + sqlQuery := utils.StrFormat(` +SELECT {joinTable}.{joinCol} FROM {joinTable} WHERE {joinTable}.id = ? +`, utils.StrFormatMap{ + "joinTable": qb.joinTable, + "joinCol": blobCol, + }) + + var checksum null.String + err := qb.repository.querySimple(ctx, sqlQuery, []interface{}{id}, &checksum) + if err != nil { + return nil, err + } + + if !checksum.Valid { + return nil, nil + } + + return &checksum.String, nil +} + +func (qb *blobJoinQueryBuilder) DestroyImage(ctx context.Context, id int, blobCol string) error { + checksum, err := qb.getChecksum(ctx, id, blobCol) + if err != nil { + return err + } + + if checksum == nil { + // no image to delete + return nil + } + + updateQuery := fmt.Sprintf("UPDATE %s SET %s = NULL WHERE id = ?", qb.joinTable, blobCol) + if _, err = qb.tx.Exec(ctx, updateQuery, id); err != nil { + return err + } + + return qb.blobStore.Delete(ctx, *checksum) +} + +func (qb *blobJoinQueryBuilder) HasImage(ctx context.Context, id int, blobCol string) (bool, error) { + stmt := utils.StrFormat("SELECT COUNT(*) as count FROM (SELECT {joinCol} FROM {joinTable} WHERE id = ? AND {joinCol} IS NOT NULL LIMIT 1)", utils.StrFormatMap{ + "joinTable": qb.joinTable, + "joinCol": blobCol, + }) + + c, err := qb.runCountQuery(ctx, stmt, []interface{}{id}) + if err != nil { + return false, err + } + + return c == 1, nil +} diff --git a/pkg/sqlite/blob/fs.go b/pkg/sqlite/blob/fs.go new file mode 100644 index 000000000..9c85f926a --- /dev/null +++ b/pkg/sqlite/blob/fs.go @@ -0,0 +1,110 @@ +package blob + +import ( + "bytes" + "context" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + + "github.com/stashapp/stash/pkg/file" + "github.com/stashapp/stash/pkg/fsutil" + "github.com/stashapp/stash/pkg/logger" +) + +const ( + blobsDirDepth int = 2 + blobsDirLength int = 2 // thumbDirDepth * thumbDirLength must be smaller than the length of checksum +) + +type FS interface { + Create(name string) (*os.File, error) + MkdirAll(path string, perm fs.FileMode) error + Open(name string) (fs.ReadDirFile, error) + Remove(name string) error + + file.RenamerRemover +} + +type FilesystemStore struct { + deleter *file.Deleter + path string + fs FS +} + +func NewFilesystemStore(path string, fs FS) *FilesystemStore { + deleter := &file.Deleter{ + RenamerRemover: fs, + } + + return &FilesystemStore{ + deleter: deleter, + path: path, + fs: fs, + } +} + +func (s *FilesystemStore) checksumToPath(checksum string) string { + return filepath.Join(s.path, fsutil.GetIntraDir(checksum, blobsDirDepth, blobsDirLength), checksum) +} + +func (s *FilesystemStore) Write(ctx context.Context, checksum string, data []byte) error { + if s.path == "" { + return fmt.Errorf("no path set") + } + + fn := s.checksumToPath(checksum) + + // create the directory if it doesn't exist + if err := s.fs.MkdirAll(filepath.Dir(fn), 0755); err != nil { + return fmt.Errorf("creating directory %q: %w", filepath.Dir(fn), err) + } + + logger.Debugf("Writing blob file %s", fn) + out, err := s.fs.Create(fn) + if err != nil { + return fmt.Errorf("creating file %q: %w", fn, err) + } + + r := bytes.NewReader(data) + + if _, err = io.Copy(out, r); err != nil { + return fmt.Errorf("writing file %q: %w", fn, err) + } + + return nil +} + +func (s *FilesystemStore) Read(ctx context.Context, checksum string) ([]byte, error) { + if s.path == "" { + return nil, fmt.Errorf("no path set") + } + + fn := s.checksumToPath(checksum) + f, err := s.fs.Open(fn) + if err != nil { + return nil, fmt.Errorf("opening file %q: %w", fn, err) + } + + defer f.Close() + + return io.ReadAll(f) +} + +func (s *FilesystemStore) Delete(ctx context.Context, checksum string) error { + if s.path == "" { + return fmt.Errorf("no path set") + } + + s.deleter.RegisterHooks(ctx) + + fn := s.checksumToPath(checksum) + + if err := s.deleter.Files([]string{fn}); err != nil { + return fmt.Errorf("deleting file %q: %w", fn, err) + } + + return nil +} diff --git a/pkg/sqlite/blob_migrate.go b/pkg/sqlite/blob_migrate.go new file mode 100644 index 000000000..e121d0792 --- /dev/null +++ b/pkg/sqlite/blob_migrate.go @@ -0,0 +1,116 @@ +package sqlite + +import ( + "context" + "fmt" + + "github.com/jmoiron/sqlx" +) + +func (qb *BlobStore) FindBlobs(ctx context.Context, n uint, lastChecksum string) ([]string, error) { + table := qb.table() + q := dialect.From(table).Select(table.Col(blobChecksumColumn)).Order(table.Col(blobChecksumColumn).Asc()).Limit(n) + + if lastChecksum != "" { + q = q.Where(table.Col(blobChecksumColumn).Gt(lastChecksum)) + } + + const single = false + var checksums []string + if err := queryFunc(ctx, q, single, func(rows *sqlx.Rows) error { + var checksum string + if err := rows.Scan(&checksum); err != nil { + return err + } + checksums = append(checksums, checksum) + return nil + }); err != nil { + return nil, err + } + + return checksums, nil +} + +// MigrateBlob migrates a blob from the filesystem to the database, or vice versa. +// The target is determined by the UseDatabase and UseFilesystem options. +// If deleteOld is true, the blob is deleted from the source after migration. +func (qb *BlobStore) MigrateBlob(ctx context.Context, checksum string, deleteOld bool) error { + if !qb.options.UseDatabase && !qb.options.UseFilesystem { + panic("no blob store configured") + } + + if qb.options.UseDatabase && qb.options.UseFilesystem { + panic("both filesystem and database configured") + } + + if qb.options.Path == "" { + panic("no blob path configured") + } + + if qb.options.UseDatabase { + return qb.migrateBlobDatabase(ctx, checksum, deleteOld) + } + + return qb.migrateBlobFilesystem(ctx, checksum, deleteOld) +} + +// migrateBlobDatabase migrates a blob from the filesystem to the database +func (qb *BlobStore) migrateBlobDatabase(ctx context.Context, checksum string, deleteOld bool) error { + // ignore if the blob is already present in the database + // (still delete the old data if requested) + existing, err := qb.readFromDatabase(ctx, checksum) + if err != nil { + return fmt.Errorf("reading from database: %w", err) + } + + if len(existing) == 0 { + // find the blob in the filesystem + blob, err := qb.fsStore.Read(ctx, checksum) + if err != nil { + return fmt.Errorf("reading from filesystem: %w", err) + } + + // write the blob to the database + if err := qb.update(ctx, checksum, blob); err != nil { + return fmt.Errorf("writing to database: %w", err) + } + } + + if deleteOld { + // delete the blob from the filesystem after commit + if err := qb.fsStore.Delete(ctx, checksum); err != nil { + return fmt.Errorf("deleting from filesystem: %w", err) + } + } + + return nil +} + +// migrateBlobFilesystem migrates a blob from the database to the filesystem +func (qb *BlobStore) migrateBlobFilesystem(ctx context.Context, checksum string, deleteOld bool) error { + // find the blob in the database + blob, err := qb.readFromDatabase(ctx, checksum) + if err != nil { + return fmt.Errorf("reading from database: %w", err) + } + + if len(blob) == 0 { + // it's possible that the blob is already present in the filesystem + // just ignore + return nil + } + + // write the blob to the filesystem + if err := qb.fsStore.Write(ctx, checksum, blob); err != nil { + return fmt.Errorf("writing to filesystem: %w", err) + } + + if deleteOld { + // delete the blob from the database row + if err := qb.update(ctx, checksum, nil); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/sqlite/blob_test.go b/pkg/sqlite/blob_test.go new file mode 100644 index 000000000..4c6e0ccc2 --- /dev/null +++ b/pkg/sqlite/blob_test.go @@ -0,0 +1,45 @@ +//go:build integration +// +build integration + +package sqlite_test + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +type updateImageFunc func(ctx context.Context, id int, image []byte) error +type getImageFunc func(ctx context.Context, movieID int) ([]byte, error) + +func testUpdateImage(t *testing.T, ctx context.Context, id int, updateFn updateImageFunc, getFn getImageFunc) error { + image := []byte("image") + err := updateFn(ctx, id, image) + if err != nil { + return fmt.Errorf("Error updating performer image: %s", err.Error()) + } + + // ensure image set + storedImage, err := getFn(ctx, id) + if err != nil { + return fmt.Errorf("Error getting image: %s", err.Error()) + } + assert.Equal(t, storedImage, image) + + // set nil image + err = updateFn(ctx, id, nil) + if err != nil { + return fmt.Errorf("error setting nil image: %w", err) + } + + // ensure image null + storedImage, err = getFn(ctx, id) + if err != nil { + return fmt.Errorf("Error getting image: %s", err.Error()) + } + assert.Nil(t, storedImage) + + return nil +} diff --git a/pkg/sqlite/database.go b/pkg/sqlite/database.go index b2c333024..d8e8b5e0d 100644 --- a/pkg/sqlite/database.go +++ b/pkg/sqlite/database.go @@ -2,7 +2,6 @@ package sqlite import ( "context" - "database/sql" "embed" "errors" "fmt" @@ -10,18 +9,30 @@ import ( "path/filepath" "time" - "github.com/fvbommel/sortorder" "github.com/golang-migrate/migrate/v4" sqlite3mig "github.com/golang-migrate/migrate/v4/database/sqlite3" "github.com/golang-migrate/migrate/v4/source/iofs" "github.com/jmoiron/sqlx" - sqlite3 "github.com/mattn/go-sqlite3" "github.com/stashapp/stash/pkg/fsutil" "github.com/stashapp/stash/pkg/logger" ) -var appSchemaVersion uint = 41 +const ( + // Number of database connections to use + // The same value is used for both the maximum and idle limit, + // to prevent opening connections on the fly which has a notieable performance penalty. + // Fewer connections use less memory, more connections increase performance, + // but have diminishing returns. + // 10 was found to be a good tradeoff. + dbConns = 10 + // Idle connection timeout, in seconds + // Closes a connection after a period of inactivity, which saves on memory and + // causes the sqlite -wal and -shm files to be automatically deleted. + dbConnTimeout = 30 +) + +var appSchemaVersion uint = 45 //go:embed migrations/*.sql var migrationsBox embed.FS @@ -52,20 +63,17 @@ func (e *MismatchedSchemaVersionError) Error() string { return fmt.Sprintf("schema version %d is incompatible with required schema version %d", e.CurrentSchemaVersion, e.RequiredSchemaVersion) } -const sqlite3Driver = "sqlite3ex" - -func init() { - // register custom driver with regexp function - registerCustomDriver() -} - type Database struct { + Blobs *BlobStore File *FileStore Folder *FolderStore Image *ImageStore Gallery *GalleryStore Scene *SceneStore Performer *PerformerStore + Studio *studioQueryBuilder + Tag *tagQueryBuilder + Movie *movieQueryBuilder db *sqlx.DB dbPath string @@ -78,20 +86,29 @@ type Database struct { func NewDatabase() *Database { fileStore := NewFileStore() folderStore := NewFolderStore() + blobStore := NewBlobStore(BlobStoreOptions{}) ret := &Database{ + Blobs: blobStore, File: fileStore, Folder: folderStore, - Scene: NewSceneStore(fileStore), + Scene: NewSceneStore(fileStore, blobStore), Image: NewImageStore(fileStore), Gallery: NewGalleryStore(fileStore, folderStore), - Performer: NewPerformerStore(), + Performer: NewPerformerStore(blobStore), + Studio: NewStudioReaderWriter(blobStore), + Tag: NewTagReaderWriter(blobStore), + Movie: NewMovieReaderWriter(blobStore), lockChan: make(chan struct{}, 1), } return ret } +func (db *Database) SetBlobStoreOptions(options BlobStoreOptions) { + *db.Blobs = *NewBlobStore(options) +} + // Ready returns an error if the database is not ready to begin transactions. func (db *Database) Ready() error { if db.db == nil { @@ -196,15 +213,15 @@ func (db *Database) Close() error { func (db *Database) open(disableForeignKeys bool) (*sqlx.DB, error) { // https://github.com/mattn/go-sqlite3 - url := "file:" + db.dbPath + "?_journal=WAL&_sync=NORMAL" + url := "file:" + db.dbPath + "?_journal=WAL&_sync=NORMAL&_busy_timeout=50" if !disableForeignKeys { url += "&_fk=true" } conn, err := sqlx.Open(sqlite3Driver, url) - conn.SetMaxOpenConns(25) - conn.SetMaxIdleConns(4) - conn.SetConnMaxLifetime(30 * time.Second) + conn.SetMaxOpenConns(dbConns) + conn.SetMaxIdleConns(dbConns) + conn.SetConnMaxIdleTime(dbConnTimeout * time.Second) if err != nil { return nil, fmt.Errorf("db.Open(): %w", err) } @@ -212,7 +229,7 @@ func (db *Database) open(disableForeignKeys bool) (*sqlx.DB, error) { return conn, nil } -func (db *Database) Reset() error { +func (db *Database) Remove() error { databasePath := db.dbPath err := db.Close() @@ -236,6 +253,15 @@ func (db *Database) Reset() error { } } + return nil +} + +func (db *Database) Reset() error { + databasePath := db.dbPath + if err := db.Remove(); err != nil { + return err + } + if err := db.Open(databasePath); err != nil { return fmt.Errorf("[reset DB] unable to initialize: %w", err) } @@ -265,6 +291,16 @@ func (db *Database) Backup(backupPath string) error { return nil } +func (db *Database) Anonymise(outPath string) error { + anon, err := NewAnonymiser(db, outPath) + + if err != nil { + return err + } + + return anon.Anonymise(context.Background()) +} + func (db *Database) RestoreFromBackup(backupPath string) error { logger.Infof("Restoring backup database %s into %s", backupPath, db.dbPath) return os.Rename(backupPath, db.dbPath) @@ -293,6 +329,16 @@ func (db *Database) DatabaseBackupPath(backupDirectoryPath string) string { return fn } +func (db *Database) AnonymousDatabasePath(backupDirectoryPath string) string { + fn := fmt.Sprintf("%s.anonymous.%d.%s", filepath.Base(db.dbPath), db.schemaVersion, time.Now().Format("20060102_150405")) + + if backupDirectoryPath != "" { + return filepath.Join(backupDirectoryPath, fn) + } + + return fn +} + func (db *Database) Version() uint { return db.schemaVersion } @@ -383,8 +429,14 @@ func (db *Database) RunMigrations() error { } // optimize database after migration + db.optimise() + + return nil +} + +func (db *Database) optimise() { logger.Info("Optimizing database") - _, err = db.db.Exec("ANALYZE") + _, err := db.db.Exec("ANALYZE") if err != nil { logger.Warnf("error while performing post-migration optimization: %v", err) } @@ -392,8 +444,12 @@ func (db *Database) RunMigrations() error { if err != nil { logger.Warnf("error while performing post-migration vacuum: %v", err) } +} - return nil +// Vacuum runs a VACUUM on the database, rebuilding the database file into a minimal amount of disk space. +func (db *Database) Vacuum(ctx context.Context) error { + _, err := db.db.ExecContext(ctx, "VACUUM") + return err } func (db *Database) runCustomMigrations(ctx context.Context, fns []customMigrationFunc) error { @@ -420,37 +476,3 @@ func (db *Database) runCustomMigration(ctx context.Context, fn customMigrationFu return nil } - -func registerCustomDriver() { - sql.Register(sqlite3Driver, - &sqlite3.SQLiteDriver{ - ConnectHook: func(conn *sqlite3.SQLiteConn) error { - funcs := map[string]interface{}{ - "regexp": regexFn, - "durationToTinyInt": durationToTinyIntFn, - } - - for name, fn := range funcs { - if err := conn.RegisterFunc(name, fn, true); err != nil { - return fmt.Errorf("error registering function %s: %s", name, err.Error()) - } - } - - // COLLATE NATURAL_CS - Case sensitive natural sort - err := conn.RegisterCollation("NATURAL_CS", func(s string, s2 string) int { - if sortorder.NaturalLess(s, s2) { - return -1 - } else { - return 1 - } - }) - - if err != nil { - return fmt.Errorf("error registering natural sort collation: %v", err) - } - - return nil - }, - }, - ) -} diff --git a/pkg/sqlite/driver.go b/pkg/sqlite/driver.go new file mode 100644 index 000000000..5712c77c7 --- /dev/null +++ b/pkg/sqlite/driver.go @@ -0,0 +1,71 @@ +package sqlite + +import ( + "database/sql" + "database/sql/driver" + "fmt" + + "github.com/fvbommel/sortorder" + sqlite3 "github.com/mattn/go-sqlite3" +) + +const sqlite3Driver = "sqlite3ex" + +func init() { + // register custom driver + sql.Register(sqlite3Driver, &CustomSQLiteDriver{}) +} + +type CustomSQLiteDriver struct{} + +type CustomSQLiteConn struct { + *sqlite3.SQLiteConn +} + +func (d *CustomSQLiteDriver) Open(dsn string) (driver.Conn, error) { + sqlite3Driver := &sqlite3.SQLiteDriver{ + ConnectHook: func(conn *sqlite3.SQLiteConn) error { + funcs := map[string]interface{}{ + "regexp": regexFn, + "durationToTinyInt": durationToTinyIntFn, + "basename": basenameFn, + } + + for name, fn := range funcs { + if err := conn.RegisterFunc(name, fn, true); err != nil { + return fmt.Errorf("error registering function %s: %v", name, err) + } + } + + // COLLATE NATURAL_CS - Case sensitive natural sort + err := conn.RegisterCollation("NATURAL_CS", func(s string, s2 string) int { + if sortorder.NaturalLess(s, s2) { + return -1 + } else { + return 1 + } + }) + + if err != nil { + return fmt.Errorf("error registering natural sort collation: %v", err) + } + + return nil + }, + } + + conn, err := sqlite3Driver.Open(dsn) + if err != nil { + return nil, err + } + + return &CustomSQLiteConn{conn.(*sqlite3.SQLiteConn)}, nil +} + +func (c *CustomSQLiteConn) Close() error { + conn := c.SQLiteConn + + _, _ = conn.Exec("PRAGMA analysis_limit=1000; PRAGMA optimize;", []driver.Value{}) + + return conn.Close() +} diff --git a/pkg/sqlite/file.go b/pkg/sqlite/file.go index 0cd21ee4b..06c83b8d6 100644 --- a/pkg/sqlite/file.go +++ b/pkg/sqlite/file.go @@ -575,6 +575,23 @@ func (qb *FileStore) find(ctx context.Context, id file.ID) (file.File, error) { // FindByPath returns the first file that matches the given path. Wildcard characters are supported. func (qb *FileStore) FindByPath(ctx context.Context, p string) (file.File, error) { + + ret, err := qb.FindAllByPath(ctx, p) + + if err != nil { + return nil, err + } + + if len(ret) == 0 { + return nil, nil + } + + return ret[0], nil +} + +// FindAllByPath returns all the files that match the given path. +// Wildcard characters are supported. +func (qb *FileStore) FindAllByPath(ctx context.Context, p string) ([]file.File, error) { // separate basename from path basename := filepath.Base(p) dirName := filepath.Dir(p) @@ -601,7 +618,7 @@ func (qb *FileStore) FindByPath(ctx context.Context, p string) (file.File, error ) } - ret, err := qb.get(ctx, q) + ret, err := qb.getMany(ctx, q) if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("getting file by path %s: %w", p, err) } @@ -806,7 +823,9 @@ func (qb *FileStore) Query(ctx context.Context, options models.FileQueryOptions) } filter := qb.makeFilter(ctx, fileFilter) - query.addFilter(filter) + if err := query.addFilter(filter); err != nil { + return nil, err + } qb.setQuerySort(&query, findFilter) query.sortAndPagination += getPagination(findFilter) diff --git a/pkg/sqlite/functions.go b/pkg/sqlite/functions.go index 29e93aa22..f639d0b5b 100644 --- a/pkg/sqlite/functions.go +++ b/pkg/sqlite/functions.go @@ -1,6 +1,7 @@ package sqlite import ( + "path/filepath" "strconv" "strings" ) @@ -30,3 +31,7 @@ func durationToTinyIntFn(str string) (int64, error) { return int64(seconds), nil } + +func basenameFn(str string) (string, error) { + return filepath.Base(str), nil +} diff --git a/pkg/sqlite/gallery.go b/pkg/sqlite/gallery.go index e45d7cb9f..590586b94 100644 --- a/pkg/sqlite/gallery.go +++ b/pkg/sqlite/gallery.go @@ -26,6 +26,7 @@ const ( galleriesTagsTable = "galleries_tags" galleriesImagesTable = "galleries_images" galleriesScenesTable = "scenes_galleries" + galleriesChaptersTable = "galleries_chapters" galleryIDColumn = "gallery_id" ) @@ -363,17 +364,23 @@ func (qb *GalleryStore) Find(ctx context.Context, id int) (*models.Gallery, erro } func (qb *GalleryStore) FindMany(ctx context.Context, ids []int) ([]*models.Gallery, error) { - q := qb.selectDataset().Prepared(true).Where(qb.table().Col(idColumn).In(ids)) - unsorted, err := qb.getMany(ctx, q) - if err != nil { - return nil, err - } - galleries := make([]*models.Gallery, len(ids)) - for _, s := range unsorted { - i := intslice.IntIndex(ids, s.ID) - galleries[i] = s + if err := batchExec(ids, defaultBatchSize, func(batch []int) error { + q := qb.selectDataset().Prepared(true).Where(qb.table().Col(idColumn).In(batch)) + unsorted, err := qb.getMany(ctx, q) + if err != nil { + return err + } + + for _, s := range unsorted { + i := intslice.IntIndex(ids, s.ID) + galleries[i] = s + } + + return nil + }); err != nil { + return nil, err } for i := range galleries { @@ -662,6 +669,7 @@ func (qb *GalleryStore) makeFilter(ctx context.Context, galleryFilter *models.Ga query.handleCriterion(ctx, galleryTagCountCriterionHandler(qb, galleryFilter.TagCount)) query.handleCriterion(ctx, galleryPerformersCriterionHandler(qb, galleryFilter.Performers)) query.handleCriterion(ctx, galleryPerformerCountCriterionHandler(qb, galleryFilter.PerformerCount)) + query.handleCriterion(ctx, hasChaptersCriterionHandler(galleryFilter.HasChapters)) query.handleCriterion(ctx, galleryStudioCriterionHandler(qb, galleryFilter.Studios)) query.handleCriterion(ctx, galleryPerformerTagsCriterionHandler(qb, galleryFilter.PerformerTags)) query.handleCriterion(ctx, galleryAverageResolutionCriterionHandler(qb, galleryFilter.AverageResolution)) @@ -723,11 +731,15 @@ func (qb *GalleryStore) makeQuery(ctx context.Context, galleryFilter *models.Gal as: "gallery_folder", onClause: "galleries.folder_id = gallery_folder.id", }, + join{ + table: galleriesChaptersTable, + onClause: "galleries_chapters.gallery_id = galleries.id", + }, ) // add joins for files and checksum filepathColumn := "folders.path || '" + string(filepath.Separator) + "' || files.basename" - searchColumns := []string{"galleries.title", "gallery_folder.path", filepathColumn, "files_fingerprints.fingerprint"} + searchColumns := []string{"galleries.title", "gallery_folder.path", filepathColumn, "files_fingerprints.fingerprint", "galleries_chapters.title"} query.parseQueryString(searchColumns, *q) } @@ -736,7 +748,9 @@ func (qb *GalleryStore) makeQuery(ctx context.Context, galleryFilter *models.Gal } filter := qb.makeFilter(ctx, galleryFilter) - query.addFilter(filter) + if err := query.addFilter(filter); err != nil { + return nil, err + } qb.setGallerySort(&query, findFilter) query.sortAndPagination += getPagination(findFilter) @@ -941,6 +955,19 @@ func galleryImageCountCriterionHandler(qb *GalleryStore, imageCount *models.IntC return h.handler(imageCount) } +func hasChaptersCriterionHandler(hasChapters *string) criterionHandlerFunc { + return func(ctx context.Context, f *filterBuilder) { + if hasChapters != nil { + f.addLeftJoin("galleries_chapters", "", "galleries_chapters.gallery_id = galleries.id") + if *hasChapters == "true" { + f.addHaving("count(galleries_chapters.gallery_id) > 0") + } else { + f.addWhere("galleries_chapters.id IS NULL") + } + } + } +} + func galleryStudioCriterionHandler(qb *GalleryStore, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { h := hierarchicalMultiCriterionHandlerBuilder{ tx: qb.tx, @@ -1109,7 +1136,7 @@ func (qb *GalleryStore) setGallerySort(query *queryBuilder, findFilter *models.F case "title": addFileTable() addFolderTable() - query.sortAndPagination += " ORDER BY galleries.title COLLATE NATURAL_CS " + direction + ", folders.path " + direction + ", file_folder.path " + direction + ", files.basename COLLATE NATURAL_CS " + direction + query.sortAndPagination += " ORDER BY COALESCE(galleries.title, files.basename, basename(COALESCE(folders.path, ''))) COLLATE NATURAL_CS " + direction + ", file_folder.path " + direction default: query.sortAndPagination += getSort(sort, direction, "galleries") } diff --git a/pkg/sqlite/gallery_chapter.go b/pkg/sqlite/gallery_chapter.go new file mode 100644 index 000000000..694a70655 --- /dev/null +++ b/pkg/sqlite/gallery_chapter.go @@ -0,0 +1,94 @@ +package sqlite + +import ( + "context" + "fmt" + + "github.com/stashapp/stash/pkg/models" +) + +type galleryChapterQueryBuilder struct { + repository +} + +var GalleryChapterReaderWriter = &galleryChapterQueryBuilder{ + repository{ + tableName: galleriesChaptersTable, + idColumn: idColumn, + }, +} + +func (qb *galleryChapterQueryBuilder) Create(ctx context.Context, newObject models.GalleryChapter) (*models.GalleryChapter, error) { + var ret models.GalleryChapter + if err := qb.insertObject(ctx, newObject, &ret); err != nil { + return nil, err + } + + return &ret, nil +} + +func (qb *galleryChapterQueryBuilder) Update(ctx context.Context, updatedObject models.GalleryChapter) (*models.GalleryChapter, error) { + const partial = false + if err := qb.update(ctx, updatedObject.ID, updatedObject, partial); err != nil { + return nil, err + } + + var ret models.GalleryChapter + if err := qb.getByID(ctx, updatedObject.ID, &ret); err != nil { + return nil, err + } + + return &ret, nil +} + +func (qb *galleryChapterQueryBuilder) Destroy(ctx context.Context, id int) error { + return qb.destroyExisting(ctx, []int{id}) +} + +func (qb *galleryChapterQueryBuilder) Find(ctx context.Context, id int) (*models.GalleryChapter, error) { + query := "SELECT * FROM galleries_chapters WHERE id = ? LIMIT 1" + args := []interface{}{id} + results, err := qb.queryGalleryChapters(ctx, query, args) + if err != nil || len(results) < 1 { + return nil, err + } + return results[0], nil +} + +func (qb *galleryChapterQueryBuilder) FindMany(ctx context.Context, ids []int) ([]*models.GalleryChapter, error) { + var markers []*models.GalleryChapter + for _, id := range ids { + marker, err := qb.Find(ctx, id) + if err != nil { + return nil, err + } + + if marker == nil { + return nil, fmt.Errorf("gallery chapter with id %d not found", id) + } + + markers = append(markers, marker) + } + + return markers, nil +} + +func (qb *galleryChapterQueryBuilder) FindByGalleryID(ctx context.Context, galleryID int) ([]*models.GalleryChapter, error) { + query := ` + SELECT galleries_chapters.* FROM galleries_chapters + WHERE galleries_chapters.gallery_id = ? + GROUP BY galleries_chapters.id + ORDER BY galleries_chapters.image_index ASC + ` + args := []interface{}{galleryID} + return qb.queryGalleryChapters(ctx, query, args) +} + +func (qb *galleryChapterQueryBuilder) queryGalleryChapters(ctx context.Context, query string, args []interface{}) ([]*models.GalleryChapter, error) { + var ret models.GalleryChapters + if err := qb.query(ctx, query, args, &ret); err != nil { + return nil, err + } + + return []*models.GalleryChapter(ret), nil +} diff --git a/pkg/sqlite/gallery_chapter_test.go b/pkg/sqlite/gallery_chapter_test.go new file mode 100644 index 000000000..3464b462a --- /dev/null +++ b/pkg/sqlite/gallery_chapter_test.go @@ -0,0 +1,44 @@ +//go:build integration +// +build integration + +package sqlite_test + +import ( + "context" + "testing" + + "github.com/stashapp/stash/pkg/sqlite" + "github.com/stretchr/testify/assert" +) + +func TestChapterFindByGalleryID(t *testing.T) { + withTxn(func(ctx context.Context) error { + mqb := sqlite.GalleryChapterReaderWriter + + galleryID := galleryIDs[galleryIdxWithChapters] + chapters, err := mqb.FindByGalleryID(ctx, galleryID) + + if err != nil { + t.Errorf("Error finding chapters: %s", err.Error()) + } + + assert.Greater(t, len(chapters), 0) + for _, chapter := range chapters { + assert.Equal(t, galleryIDs[galleryIdxWithChapters], int(chapter.GalleryID.Int64)) + } + + chapters, err = mqb.FindByGalleryID(ctx, 0) + + if err != nil { + t.Errorf("Error finding chapter: %s", err.Error()) + } + + assert.Len(t, chapters, 0) + + return nil + }) +} + +// TODO Update +// TODO Destroy +// TODO Find diff --git a/pkg/sqlite/gallery_test.go b/pkg/sqlite/gallery_test.go index 80e25b6d1..6d145cb1b 100644 --- a/pkg/sqlite/gallery_test.go +++ b/pkg/sqlite/gallery_test.go @@ -2414,6 +2414,13 @@ func TestGalleryQuerySorting(t *testing.T) { -1, -1, }, + { + "title", + "title", + models.SortDirectionEnumDesc, + -1, + -1, + }, } qb := db.Gallery @@ -2609,6 +2616,37 @@ func TestGalleryStore_RemoveImages(t *testing.T) { } } +func TestGalleryQueryHasChapters(t *testing.T) { + withTxn(func(ctx context.Context) error { + sqb := db.Gallery + hasChapters := "true" + galleryFilter := models.GalleryFilterType{ + HasChapters: &hasChapters, + } + + q := getGalleryStringValue(galleryIdxWithChapters, titleField) + findFilter := models.FindFilterType{ + Q: &q, + } + + galleries := queryGallery(ctx, t, sqb, &galleryFilter, &findFilter) + + assert.Len(t, galleries, 1) + assert.Equal(t, galleryIDs[galleryIdxWithChapters], galleries[0].ID) + + hasChapters = "false" + galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter) + assert.Len(t, galleries, 0) + + findFilter.Q = nil + galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter) + + assert.NotEqual(t, 0, len(galleries)) + + return nil + }) +} + // TODO Count // TODO All // TODO Query diff --git a/pkg/sqlite/image.go b/pkg/sqlite/image.go index 9cc0e957a..d5bb4e852 100644 --- a/pkg/sqlite/image.go +++ b/pkg/sqlite/image.go @@ -31,6 +31,8 @@ type imageRow struct { Title zero.String `db:"title"` // expressed as 1-100 Rating null.Int `db:"rating"` + URL zero.String `db:"url"` + Date models.SQLiteDate `db:"date"` Organized bool `db:"organized"` OCounter int `db:"o_counter"` StudioID null.Int `db:"studio_id,omitempty"` @@ -42,6 +44,10 @@ func (r *imageRow) fromImage(i models.Image) { r.ID = i.ID r.Title = zero.StringFrom(i.Title) r.Rating = intFromPtr(i.Rating) + r.URL = zero.StringFrom(i.URL) + if i.Date != nil { + _ = r.Date.Scan(i.Date.Time) + } r.Organized = i.Organized r.OCounter = i.OCounter r.StudioID = intFromPtr(i.StudioID) @@ -62,6 +68,8 @@ func (r *imageQueryRow) resolve() *models.Image { ID: r.ID, Title: r.Title.String, Rating: nullIntPtr(r.Rating), + URL: r.URL.String, + Date: r.Date.DatePtr(), Organized: r.Organized, OCounter: r.OCounter, StudioID: nullIntPtr(r.StudioID), @@ -87,6 +95,8 @@ type imageRowRecord struct { func (r *imageRowRecord) fromPartial(i models.ImagePartial) { r.setNullString("title", i.Title) r.setNullInt("rating", i.Rating) + r.setNullString("url", i.URL) + r.setSQLiteDate("date", i.Date) r.setBool("organized", i.Organized) r.setInt("o_counter", i.OCounter) r.setNullInt("studio_id", i.StudioID) @@ -250,17 +260,23 @@ func (qb *ImageStore) Find(ctx context.Context, id int) (*models.Image, error) { } func (qb *ImageStore) FindMany(ctx context.Context, ids []int) ([]*models.Image, error) { - q := qb.selectDataset().Prepared(true).Where(qb.table().Col(idColumn).In(ids)) - unsorted, err := qb.getMany(ctx, q) - if err != nil { - return nil, err - } - images := make([]*models.Image, len(ids)) - for _, s := range unsorted { - i := intslice.IntIndex(ids, s.ID) - images[i] = s + if err := batchExec(ids, defaultBatchSize, func(batch []int) error { + q := qb.selectDataset().Prepared(true).Where(qb.table().Col(idColumn).In(batch)) + unsorted, err := qb.getMany(ctx, q) + if err != nil { + return err + } + + for _, s := range unsorted { + i := intslice.IntIndex(ids, s.ID) + images[i] = s + } + + return nil + }); err != nil { + return nil, err } for i := range images { @@ -638,6 +654,8 @@ func (qb *ImageStore) makeFilter(ctx context.Context, imageFilter *models.ImageF query.handleCriterion(ctx, rating5CriterionHandler(imageFilter.Rating, "images.rating", nil)) query.handleCriterion(ctx, intCriterionHandler(imageFilter.OCounter, "images.o_counter", nil)) query.handleCriterion(ctx, boolCriterionHandler(imageFilter.Organized, "images.organized", nil)) + query.handleCriterion(ctx, dateCriterionHandler(imageFilter.Date, "images.date")) + query.handleCriterion(ctx, stringCriterionHandler(imageFilter.URL, "images.url")) query.handleCriterion(ctx, resolutionCriterionHandler(imageFilter.Resolution, "image_files.height", "image_files.width", qb.addImageFilesTable)) query.handleCriterion(ctx, imageIsMissingCriterionHandler(qb, imageFilter.IsMissing)) @@ -716,7 +734,9 @@ func (qb *ImageStore) makeQuery(ctx context.Context, imageFilter *models.ImageFi } filter := qb.makeFilter(ctx, imageFilter) - query.addFilter(filter) + if err := query.addFilter(filter); err != nil { + return nil, err + } qb.setImageSortAndPagination(&query, findFilter) @@ -1019,7 +1039,7 @@ func (qb *ImageStore) setImageSortAndPagination(q *queryBuilder, findFilter *mod case "title": addFilesJoin() addFolderJoin() - sortClause = " ORDER BY images.title COLLATE NATURAL_CS " + direction + ", folders.path " + direction + ", files.basename COLLATE NATURAL_CS " + direction + sortClause = " ORDER BY COALESCE(images.title, files.basename) COLLATE NATURAL_CS " + direction + ", folders.path " + direction default: sortClause = getSort(sort, direction, "images") } diff --git a/pkg/sqlite/image_test.go b/pkg/sqlite/image_test.go index d40859de9..31f6d4876 100644 --- a/pkg/sqlite/image_test.go +++ b/pkg/sqlite/image_test.go @@ -56,6 +56,8 @@ func Test_imageQueryBuilder_Create(t *testing.T) { title = "title" rating = 60 ocounter = 5 + url = "url" + date = models.NewDate("2003-02-01") createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) @@ -72,6 +74,8 @@ func Test_imageQueryBuilder_Create(t *testing.T) { models.Image{ Title: title, Rating: &rating, + Date: &date, + URL: url, Organized: true, OCounter: ocounter, StudioID: &studioIDs[studioIdxWithImage], @@ -88,6 +92,8 @@ func Test_imageQueryBuilder_Create(t *testing.T) { models.Image{ Title: title, Rating: &rating, + Date: &date, + URL: url, Organized: true, OCounter: ocounter, StudioID: &studioIDs[studioIdxWithImage], @@ -209,6 +215,8 @@ func Test_imageQueryBuilder_Update(t *testing.T) { var ( title = "title" rating = 60 + url = "url" + date = models.NewDate("2003-02-01") ocounter = 5 createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) @@ -225,6 +233,8 @@ func Test_imageQueryBuilder_Update(t *testing.T) { ID: imageIDs[imageIdxWithGallery], Title: title, Rating: &rating, + URL: url, + Date: &date, Organized: true, OCounter: ocounter, StudioID: &studioIDs[studioIdxWithImage], @@ -372,6 +382,8 @@ func clearImagePartial() models.ImagePartial { return models.ImagePartial{ Title: models.OptionalString{Set: true, Null: true}, Rating: models.OptionalInt{Set: true, Null: true}, + URL: models.OptionalString{Set: true, Null: true}, + Date: models.OptionalDate{Set: true, Null: true}, StudioID: models.OptionalInt{Set: true, Null: true}, GalleryIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, TagIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, @@ -383,6 +395,8 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) { var ( title = "title" rating = 60 + url = "url" + date = models.NewDate("2003-02-01") ocounter = 5 createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) @@ -401,6 +415,8 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) { models.ImagePartial{ Title: models.NewOptionalString(title), Rating: models.NewOptionalInt(rating), + URL: models.NewOptionalString(url), + Date: models.NewOptionalDate(date), Organized: models.NewOptionalBool(true), OCounter: models.NewOptionalInt(ocounter), StudioID: models.NewOptionalInt(studioIDs[studioIdxWithImage]), @@ -423,6 +439,8 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) { ID: imageIDs[imageIdx1WithGallery], Title: title, Rating: &rating, + URL: url, + Date: &date, Organized: true, OCounter: ocounter, StudioID: &studioIDs[studioIdxWithImage], @@ -943,7 +961,8 @@ func Test_imageQueryBuilder_Destroy(t *testing.T) { } func makeImageWithID(index int) *models.Image { - ret := makeImage(index) + const fromDB = true + ret := makeImage(index, true) ret.ID = imageIDs[index] ret.Files = models.NewRelatedImageFiles([]*file.ImageFile{makeImageFile(index)}) @@ -2560,6 +2579,13 @@ func TestImageQuerySorting(t *testing.T) { -1, -1, }, + { + "date", + "date", + models.SortDirectionEnumDesc, + imageIdxWithTwoGalleries, + imageIdxWithGrandChildStudio, + }, } qb := db.Image diff --git a/pkg/sqlite/migrations/42_performer_disambig_aliases.up.sql b/pkg/sqlite/migrations/42_performer_disambig_aliases.up.sql new file mode 100644 index 000000000..34197d8ae --- /dev/null +++ b/pkg/sqlite/migrations/42_performer_disambig_aliases.up.sql @@ -0,0 +1,121 @@ +PRAGMA foreign_keys=OFF; + +CREATE TABLE `performer_aliases` ( + `performer_id` integer NOT NULL, + `alias` varchar(255) NOT NULL, + foreign key(`performer_id`) references `performers`(`id`) on delete CASCADE, + PRIMARY KEY(`performer_id`, `alias`) +); + +CREATE INDEX `performer_aliases_alias` on `performer_aliases` (`alias`); + +DROP INDEX `performers_checksum_unique`; + +-- drop aliases and checksum +-- add disambiguation + +CREATE TABLE `performers_new` ( + `id` integer not null primary key autoincrement, + `name` varchar(255), + `disambiguation` varchar(255), + `gender` varchar(20), + `url` varchar(255), + `twitter` varchar(255), + `instagram` varchar(255), + `birthdate` date, + `ethnicity` varchar(255), + `country` varchar(255), + `eye_color` varchar(255), + `height` int, + `measurements` varchar(255), + `fake_tits` varchar(255), + `career_length` varchar(255), + `tattoos` varchar(255), + `piercings` varchar(255), + `favorite` boolean not null default '0', + `created_at` datetime not null, + `updated_at` datetime not null, + `details` text, + `death_date` date, + `hair_color` varchar(255), + `weight` integer, + `rating` tinyint, + `ignore_auto_tag` boolean not null default '0' +); + +INSERT INTO `performers_new` + ( + `id`, + `name`, + `gender`, + `url`, + `twitter`, + `instagram`, + `birthdate`, + `ethnicity`, + `country`, + `eye_color`, + `height`, + `measurements`, + `fake_tits`, + `career_length`, + `tattoos`, + `piercings`, + `favorite`, + `created_at`, + `updated_at`, + `details`, + `death_date`, + `hair_color`, + `weight`, + `rating`, + `ignore_auto_tag` + ) + SELECT + `id`, + `name`, + `gender`, + `url`, + `twitter`, + `instagram`, + `birthdate`, + `ethnicity`, + `country`, + `eye_color`, + `height`, + `measurements`, + `fake_tits`, + `career_length`, + `tattoos`, + `piercings`, + `favorite`, + `created_at`, + `updated_at`, + `details`, + `death_date`, + `hair_color`, + `weight`, + `rating`, + `ignore_auto_tag` + FROM `performers`; + +INSERT INTO `performer_aliases` + ( + `performer_id`, + `alias` + ) + SELECT + `id`, + `aliases` + FROM `performers` + WHERE `performers`.`aliases` IS NOT NULL AND `performers`.`aliases` != ''; + +DROP TABLE `performers`; +ALTER TABLE `performers_new` rename to `performers`; + + +-- these will be executed in the post-migration +-- CREATE UNIQUE INDEX `performers_name_disambiguation_unique` on `performers` (`name`, `disambiguation`) WHERE `disambiguation` IS NOT NULL; +-- CREATE UNIQUE INDEX `performers_name_unique` on `performers` (`name`) WHERE `disambiguation` IS NULL; + +PRAGMA foreign_keys=ON; diff --git a/pkg/sqlite/migrations/42_postmigrate.go b/pkg/sqlite/migrations/42_postmigrate.go new file mode 100644 index 000000000..235687f92 --- /dev/null +++ b/pkg/sqlite/migrations/42_postmigrate.go @@ -0,0 +1,353 @@ +package migrations + +import ( + "context" + "database/sql" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/jmoiron/sqlx" + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/sliceutil/stringslice" + "github.com/stashapp/stash/pkg/sqlite" +) + +type schema42Migrator struct { + migrator +} + +func post42(ctx context.Context, db *sqlx.DB) error { + logger.Info("Running post-migration for schema version 42") + + m := schema42Migrator{ + migrator: migrator{ + db: db, + }, + } + + if err := m.migrate(ctx); err != nil { + return fmt.Errorf("migrating performer aliases: %w", err) + } + + if err := m.migrateDuplicatePerformers(ctx); err != nil { + return fmt.Errorf("migrating duplicate performers: %w", err) + } + + // do this after duplicate performer detection, since setting disambiguation + // breaks the duplicate disambiguation setting code + if err := m.migratePerformersDisam(ctx); err != nil { + return fmt.Errorf("migrating performer names: %w", err) + } + + if err := m.executeSchemaChanges(); err != nil { + return fmt.Errorf("executing schema changes: %w", err) + } + + return nil +} + +func (m *schema42Migrator) migrate(ctx context.Context) error { + logger.Info("Migrating performer aliases") + + const ( + limit = 1000 + logEvery = 10000 + ) + + lastID := 0 + count := 0 + + for { + gotSome := false + + if err := m.withTxn(ctx, func(tx *sqlx.Tx) error { + query := "SELECT `performer_id`, `alias` FROM `performer_aliases`" + + if lastID != 0 { + query += fmt.Sprintf(" WHERE `performer_id` > %d ", lastID) + } + + query += fmt.Sprintf(" ORDER BY `performer_id` LIMIT %d", limit) + + rows, err := m.db.Query(query) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var ( + id int + aliases string + ) + + err := rows.Scan(&id, &aliases) + if err != nil { + return err + } + + lastID = id + gotSome = true + count++ + + if err := m.migratePerformerAliases(id, aliases); err != nil { + return err + } + } + + return rows.Err() + }); err != nil { + return err + } + + if !gotSome { + break + } + + if count%logEvery == 0 { + logger.Infof("Migrated %d rows", count) + } + } + + return nil +} + +func (m *schema42Migrator) migratePerformerAliases(id int, aliases string) error { + // split aliases by , or / + aliasList := strings.FieldsFunc(aliases, func(r rune) bool { + return strings.ContainsRune(",/", r) + }) + + if len(aliasList) < 2 { + // existing value is fine + return nil + } + + // delete the existing row + if _, err := m.db.Exec("DELETE FROM `performer_aliases` WHERE `performer_id` = ?", id); err != nil { + return err + } + + // trim whitespace from each alias + for i, alias := range aliasList { + aliasList[i] = strings.TrimSpace(alias) + } + + // remove duplicates + aliasList = stringslice.StrAppendUniques(nil, aliasList) + + // insert aliases into table + for _, alias := range aliasList { + _, err := m.db.Exec("INSERT INTO `performer_aliases` (`performer_id`, `alias`) VALUES (?, ?)", id, alias) + if err != nil { + return err + } + } + + return nil +} + +func (m *schema42Migrator) migratePerformersDisam(ctx context.Context) error { + logger.Info("Migrating performer disambiguation") + + const ( + limit = 1 + logEvery = 10000 + ) + + count := 0 + lastID := 0 + + for { + gotSome := false + + if err := m.withTxn(ctx, func(tx *sqlx.Tx) error { + query := ` +SELECT id, name FROM performers WHERE performers.name like '% (%)'` + + if lastID != 0 { + query += fmt.Sprintf(" AND `id` > %d ", lastID) + } + + query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit) + + rows, err := m.db.Query(query) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var ( + id int + name string + ) + + err := rows.Scan(&id, &name) + if err != nil { + return err + } + + gotSome = true + lastID = id + count++ + + if err := m.massagePerformerName(id, name); err != nil { + return err + } + } + + return rows.Err() + }); err != nil { + return err + } + + if !gotSome { + break + } + + if count%logEvery == 0 { + logger.Infof("Migrated %d performers", count) + } + } + + return nil +} + +// extracts the performer name and disambiguation from the name field based on +// the format "name (disambiguation)". +var performerDisRE = regexp.MustCompile(`^((?:[^(\s]+\s)+)\(([^)]+)\)$`) + +func (m *schema42Migrator) massagePerformerName(performerID int, name string) error { + + r := performerDisRE.FindStringSubmatch(name) + if len(r) != 3 { + // ignore corner case invalid names + return nil + } + + // get the performer name and disambiguation from the capturing groups + // trim the trailing whitespace (single only) from the name + newName := strings.TrimSuffix(r[1], " ") + newDis := r[2] + + logger.Infof("Separating %q into %q and disambiguation %q", name, newName, newDis) + + _, err := m.db.Exec("UPDATE performers SET name = ?, disambiguation = ? WHERE id = ?", newName, newDis, performerID) + if err != nil { + return err + } + + return nil +} + +func (m *schema42Migrator) migrateDuplicatePerformers(ctx context.Context) error { + logger.Info("Migrating duplicate performers") + + const ( + limit = 1000 + logEvery = 10000 + ) + + count := 0 + + for { + gotSome := false + + if err := m.withTxn(ctx, func(tx *sqlx.Tx) error { + query := ` +SELECT id, name FROM performers WHERE performers.disambiguation IS NULL AND EXISTS ( + SELECT 1 FROM performers p2 WHERE + performers.name = p2.name AND + performers.rowid > p2.rowid +)` + + query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit) + + rows, err := m.db.Query(query) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var ( + id int + name string + ) + + err := rows.Scan(&id, &name) + if err != nil { + return err + } + + gotSome = true + count++ + + if err := m.migrateDuplicatePerformer(id, name); err != nil { + return err + } + } + + return rows.Err() + }); err != nil { + return err + } + + if !gotSome { + break + } + + if count%logEvery == 0 { + logger.Infof("Migrated %d performers", count) + } + } + + return nil +} + +func (m *schema42Migrator) migrateDuplicatePerformer(performerID int, name string) error { + // get the highest value of disambiguation for this performer name + query := ` +SELECT disambiguation FROM performers WHERE name = ? ORDER BY disambiguation DESC LIMIT 1` + + var disambiguation sql.NullString + if err := m.db.Get(&disambiguation, query, name); err != nil { + return err + } + + newDisambiguation := 1 + + // if there is no disambiguation, set it to 1 + if disambiguation.Valid { + numericDis, err := strconv.Atoi(disambiguation.String) + if err != nil { + // shouldn't happen + return err + } + + newDisambiguation = numericDis + 1 + } + + logger.Infof("Adding disambiguation '%d' for performer %q", newDisambiguation, name) + + _, err := m.db.Exec("UPDATE performers SET disambiguation = ? WHERE id = ?", strconv.Itoa(newDisambiguation), performerID) + if err != nil { + return err + } + + return nil +} + +func (m *schema42Migrator) executeSchemaChanges() error { + return m.execAll([]string{ + "CREATE UNIQUE INDEX `performers_name_disambiguation_unique` on `performers` (`name`, `disambiguation`) WHERE `disambiguation` IS NOT NULL", + "CREATE UNIQUE INDEX `performers_name_unique` on `performers` (`name`) WHERE `disambiguation` IS NULL", + }) +} + +func init() { + sqlite.RegisterPostMigration(42, post42) +} diff --git a/pkg/sqlite/migrations/43_image_date_url.up.sql b/pkg/sqlite/migrations/43_image_date_url.up.sql new file mode 100644 index 000000000..b66591acb --- /dev/null +++ b/pkg/sqlite/migrations/43_image_date_url.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE `images` ADD COLUMN `url` varchar(255); +ALTER TABLE `images` ADD COLUMN `date` date; \ No newline at end of file diff --git a/pkg/sqlite/migrations/44_gallery_chapters.up.sql b/pkg/sqlite/migrations/44_gallery_chapters.up.sql new file mode 100644 index 000000000..de38e23e9 --- /dev/null +++ b/pkg/sqlite/migrations/44_gallery_chapters.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `galleries_chapters` ( + `id` integer not null primary key autoincrement, + `title` varchar(255) not null, + `image_index` integer not null, + `gallery_id` integer not null, + `created_at` datetime not null, + `updated_at` datetime not null, + foreign key(`gallery_id`) references `galleries`(`id`) on delete CASCADE +); +CREATE INDEX `index_galleries_chapters_on_gallery_id` on `galleries_chapters` (`gallery_id`); diff --git a/pkg/sqlite/migrations/45_blobs.up.sql b/pkg/sqlite/migrations/45_blobs.up.sql new file mode 100644 index 000000000..ea62fe5bc --- /dev/null +++ b/pkg/sqlite/migrations/45_blobs.up.sql @@ -0,0 +1,19 @@ +CREATE TABLE `blobs` ( + `checksum` varchar(255) NOT NULL PRIMARY KEY, + `blob` blob +); + +ALTER TABLE `tags` ADD COLUMN `image_blob` varchar(255) REFERENCES `blobs`(`checksum`); +ALTER TABLE `studios` ADD COLUMN `image_blob` varchar(255) REFERENCES `blobs`(`checksum`); +ALTER TABLE `performers` ADD COLUMN `image_blob` varchar(255) REFERENCES `blobs`(`checksum`); +ALTER TABLE `scenes` ADD COLUMN `cover_blob` varchar(255) REFERENCES `blobs`(`checksum`); + +ALTER TABLE `movies` ADD COLUMN `front_image_blob` varchar(255) REFERENCES `blobs`(`checksum`); +ALTER TABLE `movies` ADD COLUMN `back_image_blob` varchar(255) REFERENCES `blobs`(`checksum`); + +-- performed in the post-migration +-- DROP TABLE `tags_image`; +-- DROP TABLE `studios_image`; +-- DROP TABLE `performers_image`; +-- DROP TABLE `scenes_cover`; +-- DROP TABLE `movies_images`; diff --git a/pkg/sqlite/migrations/45_postmigrate.go b/pkg/sqlite/migrations/45_postmigrate.go new file mode 100644 index 000000000..ee205788b --- /dev/null +++ b/pkg/sqlite/migrations/45_postmigrate.go @@ -0,0 +1,296 @@ +package migrations + +import ( + "context" + "fmt" + "strings" + + "github.com/jmoiron/sqlx" + "github.com/stashapp/stash/internal/manager/config" + "github.com/stashapp/stash/pkg/hash/md5" + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/sqlite" + "github.com/stashapp/stash/pkg/utils" +) + +type schema45Migrator struct { + migrator + hasBlobs bool +} + +func post45(ctx context.Context, db *sqlx.DB) error { + logger.Info("Running post-migration for schema version 45") + + m := schema45Migrator{ + migrator: migrator{ + db: db, + }, + } + + if err := m.migrateImagesTable(ctx, migrateImagesTableOptions{ + joinTable: "tags_image", + joinIDCol: "tag_id", + destTable: "tags", + cols: []migrateImageToBlobOptions{ + { + joinImageCol: "image", + destCol: "image_blob", + }, + }, + }); err != nil { + return err + } + + if err := m.migrateImagesTable(ctx, migrateImagesTableOptions{ + joinTable: "studios_image", + joinIDCol: "studio_id", + destTable: "studios", + cols: []migrateImageToBlobOptions{ + { + joinImageCol: "image", + destCol: "image_blob", + }, + }, + }); err != nil { + return err + } + + if err := m.migrateImagesTable(ctx, migrateImagesTableOptions{ + joinTable: "performers_image", + joinIDCol: "performer_id", + destTable: "performers", + cols: []migrateImageToBlobOptions{ + { + joinImageCol: "image", + destCol: "image_blob", + }, + }, + }); err != nil { + return err + } + + if err := m.migrateImagesTable(ctx, migrateImagesTableOptions{ + joinTable: "scenes_cover", + joinIDCol: "scene_id", + destTable: "scenes", + cols: []migrateImageToBlobOptions{ + { + joinImageCol: "cover", + destCol: "cover_blob", + }, + }, + }); err != nil { + return err + } + + if err := m.migrateImagesTable(ctx, migrateImagesTableOptions{ + joinTable: "movies_images", + joinIDCol: "movie_id", + destTable: "movies", + cols: []migrateImageToBlobOptions{ + { + joinImageCol: "front_image", + destCol: "front_image_blob", + }, + { + joinImageCol: "back_image", + destCol: "back_image_blob", + }, + }, + }); err != nil { + return err + } + + tablesToDrop := []string{ + "tags_image", + "studios_image", + "performers_image", + "scenes_cover", + "movies_images", + } + + for _, table := range tablesToDrop { + if err := m.dropTable(ctx, table); err != nil { + return err + } + } + + if err := m.migrateConfig(ctx); err != nil { + return err + } + + return nil +} + +type migrateImageToBlobOptions struct { + joinImageCol string + destCol string +} + +type migrateImagesTableOptions struct { + joinTable string + joinIDCol string + destTable string + cols []migrateImageToBlobOptions +} + +func (o migrateImagesTableOptions) selectColumns() string { + var cols []string + for _, c := range o.cols { + cols = append(cols, "`"+c.joinImageCol+"`") + } + + return strings.Join(cols, ", ") +} + +func (m *schema45Migrator) migrateImagesTable(ctx context.Context, options migrateImagesTableOptions) error { + logger.Infof("Moving %s to blobs table", options.joinTable) + + const ( + limit = 1000 + logEvery = 10000 + ) + + count := 0 + + for { + gotSome := false + + if err := m.withTxn(ctx, func(tx *sqlx.Tx) error { + query := fmt.Sprintf("SELECT %s, %s FROM `%s`", options.joinIDCol, options.selectColumns(), options.joinTable) + + query += fmt.Sprintf(" LIMIT %d", limit) + + rows, err := m.db.Query(query) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + m.hasBlobs = true + + var id int + + result := make([]interface{}, len(options.cols)+1) + result[0] = &id + for i := range options.cols { + v := []byte{} + result[i+1] = &v + } + + err := rows.Scan(result...) + if err != nil { + return err + } + + gotSome = true + count++ + + for i, col := range options.cols { + image := result[i+1].(*[]byte) + + if len(*image) > 0 { + if err := m.insertImage(*image, id, options.destTable, col.destCol); err != nil { + return err + } + } + } + + // delete the row from the join table so we don't process it again + deleteSQL := utils.StrFormat("DELETE FROM `{joinTable}` WHERE `{joinIDCol}` = ?", utils.StrFormatMap{ + "joinTable": options.joinTable, + "joinIDCol": options.joinIDCol, + }) + if _, err := m.db.Exec(deleteSQL, id); err != nil { + return err + } + } + + return rows.Err() + }); err != nil { + return err + } + + if !gotSome { + break + } + + if count%logEvery == 0 { + logger.Infof("Migrated %d images", count) + } + } + + return nil +} + +func (m *schema45Migrator) insertImage(data []byte, id int, destTable string, destCol string) error { + // calculate checksum and insert into blobs table + checksum := md5.FromBytes(data) + + if _, err := m.db.Exec("INSERT INTO `blobs` (`checksum`, `blob`) VALUES (?, ?) ON CONFLICT DO NOTHING", checksum, data); err != nil { + return err + } + + // set the tag image checksum + updateSQL := utils.StrFormat("UPDATE `{destTable}` SET `{destCol}` = ? WHERE `id` = ?", utils.StrFormatMap{ + "destTable": destTable, + "destCol": destCol, + }) + if _, err := m.db.Exec(updateSQL, checksum, id); err != nil { + return err + } + + return nil +} + +func (m *schema45Migrator) dropTable(ctx context.Context, table string) error { + if err := m.withTxn(ctx, func(tx *sqlx.Tx) error { + logger.Debugf("Dropping %s", table) + _, err := m.db.Exec(fmt.Sprintf("DROP TABLE `%s`", table)) + return err + }); err != nil { + return err + } + + return nil +} + +func (m *schema45Migrator) migrateConfig(ctx context.Context) error { + c := config.GetInstance() + + // if we don't have blobs, and storage is already set, then don't overwrite + if !m.hasBlobs && c.GetBlobsStorage().IsValid() { + logger.Infof("Blobs storage already set, not overwriting") + return nil + } + + // if we have blobs in the database, then default to database storage + // otherwise default to filesystem storage + defaultStorage := config.BlobStorageTypeFilesystem + if m.hasBlobs || c.GetBlobsPath() == "" { + defaultStorage = config.BlobStorageTypeDatabase + } + + logger.Infof("Setting blobs storage to %s", defaultStorage.String()) + c.Set(config.BlobsStorage, defaultStorage) + if err := c.Write(); err != nil { + logger.Errorf("Error while writing configuration file: %s", err.Error()) + } + + // if default scan settings are set, then set to generate scene covers by default + scanDefaults := c.GetDefaultScanSettings() + if scanDefaults != nil { + scanDefaults.ScanGenerateCovers = true + c.Set(config.DefaultScanSettings, scanDefaults) + if err := c.Write(); err != nil { + logger.Errorf("Error while writing configuration file: %s", err.Error()) + } + } + + return nil +} + +func init() { + sqlite.RegisterPostMigration(45, post45) +} diff --git a/pkg/sqlite/migrations/custom_migration.go b/pkg/sqlite/migrations/custom_migration.go index baebc7094..f243e20e3 100644 --- a/pkg/sqlite/migrations/custom_migration.go +++ b/pkg/sqlite/migrations/custom_migration.go @@ -36,3 +36,13 @@ func (m *migrator) withTxn(ctx context.Context, fn func(tx *sqlx.Tx) error) erro err = fn(tx) return err } + +func (m *migrator) execAll(stmts []string) error { + for _, stmt := range stmts { + if _, err := m.db.Exec(stmt); err != nil { + return fmt.Errorf("executing statement %s: %w", stmt, err) + } + } + + return nil +} diff --git a/pkg/sqlite/movies.go b/pkg/sqlite/movies.go index a3b0e2f2b..1c591614d 100644 --- a/pkg/sqlite/movies.go +++ b/pkg/sqlite/movies.go @@ -12,18 +12,30 @@ import ( "github.com/stashapp/stash/pkg/sliceutil/intslice" ) -const movieTable = "movies" -const movieIDColumn = "movie_id" +const ( + movieTable = "movies" + movieIDColumn = "movie_id" + + movieFrontImageBlobColumn = "front_image_blob" + movieBackImageBlobColumn = "back_image_blob" +) type movieQueryBuilder struct { repository + blobJoinQueryBuilder } -var MovieReaderWriter = &movieQueryBuilder{ - repository{ - tableName: movieTable, - idColumn: idColumn, - }, +func NewMovieReaderWriter(blobStore *BlobStore) *movieQueryBuilder { + return &movieQueryBuilder{ + repository{ + tableName: movieTable, + idColumn: idColumn, + }, + blobJoinQueryBuilder{ + blobStore: blobStore, + joinTable: movieTable, + }, + } } func (qb *movieQueryBuilder) Create(ctx context.Context, newObject models.Movie) (*models.Movie, error) { @@ -54,6 +66,11 @@ func (qb *movieQueryBuilder) UpdateFull(ctx context.Context, updatedObject model } func (qb *movieQueryBuilder) Destroy(ctx context.Context, id int) error { + // must handle image checksums manually + if err := qb.destroyImages(ctx, id); err != nil { + return err + } + return qb.destroyExisting(ctx, []int{id}) } @@ -70,17 +87,23 @@ func (qb *movieQueryBuilder) Find(ctx context.Context, id int) (*models.Movie, e func (qb *movieQueryBuilder) FindMany(ctx context.Context, ids []int) ([]*models.Movie, error) { tableMgr := movieTableMgr - q := goqu.Select("*").From(tableMgr.table).Where(tableMgr.byIDInts(ids...)) - unsorted, err := qb.getMany(ctx, q) - if err != nil { - return nil, err - } - ret := make([]*models.Movie, len(ids)) - for _, s := range unsorted { - i := intslice.IntIndex(ids, s.ID) - ret[i] = s + if err := batchExec(ids, defaultBatchSize, func(batch []int) error { + q := goqu.Select("*").From(tableMgr.table).Where(tableMgr.byIDInts(batch...)) + unsorted, err := qb.getMany(ctx, q) + if err != nil { + return err + } + + for _, s := range unsorted { + i := intslice.IntIndex(ids, s.ID) + ret[i] = s + } + + return nil + }); err != nil { + return nil, err } for i := range ret { @@ -180,7 +203,9 @@ func (qb *movieQueryBuilder) Query(ctx context.Context, movieFilter *models.Movi filter := qb.makeFilter(ctx, movieFilter) - query.addFilter(filter) + if err := query.addFilter(filter); err != nil { + return nil, 0, err + } query.sortAndPagination = qb.getMovieSort(findFilter) + getPagination(findFilter) idsResult, countResult, err := query.executeFind(ctx) @@ -201,11 +226,9 @@ func movieIsMissingCriterionHandler(qb *movieQueryBuilder, isMissing *string) cr if isMissing != nil && *isMissing != "" { switch *isMissing { case "front_image": - f.addLeftJoin("movies_images", "", "movies_images.movie_id = movies.id") - f.addWhere("movies_images.front_image IS NULL") + f.addWhere("movies.front_image_blob IS NULL") case "back_image": - f.addLeftJoin("movies_images", "", "movies_images.movie_id = movies.id") - f.addWhere("movies_images.back_image IS NULL") + f.addWhere("movies.back_image_blob IS NULL") case "scenes": f.addLeftJoin("movies_scenes", "", "movies_scenes.movie_id = movies.id") f.addWhere("movies_scenes.scene_id IS NULL") @@ -314,39 +337,35 @@ func (qb *movieQueryBuilder) queryMovies(ctx context.Context, query string, args return []*models.Movie(ret), nil } -func (qb *movieQueryBuilder) UpdateImages(ctx context.Context, movieID int, frontImage []byte, backImage []byte) error { - // Delete the existing cover and then create new - if err := qb.DestroyImages(ctx, movieID); err != nil { - return err - } - - _, err := qb.tx.Exec(ctx, - `INSERT INTO movies_images (movie_id, front_image, back_image) VALUES (?, ?, ?)`, - movieID, - frontImage, - backImage, - ) - - return err +func (qb *movieQueryBuilder) UpdateFrontImage(ctx context.Context, movieID int, frontImage []byte) error { + return qb.UpdateImage(ctx, movieID, movieFrontImageBlobColumn, frontImage) } -func (qb *movieQueryBuilder) DestroyImages(ctx context.Context, movieID int) error { - // Delete the existing joins - _, err := qb.tx.Exec(ctx, "DELETE FROM movies_images WHERE movie_id = ?", movieID) - if err != nil { +func (qb *movieQueryBuilder) UpdateBackImage(ctx context.Context, movieID int, backImage []byte) error { + return qb.UpdateImage(ctx, movieID, movieBackImageBlobColumn, backImage) +} + +func (qb *movieQueryBuilder) destroyImages(ctx context.Context, movieID int) error { + if err := qb.DestroyImage(ctx, movieID, movieFrontImageBlobColumn); err != nil { return err } - return err + if err := qb.DestroyImage(ctx, movieID, movieBackImageBlobColumn); err != nil { + return err + } + + return nil } func (qb *movieQueryBuilder) GetFrontImage(ctx context.Context, movieID int) ([]byte, error) { - query := `SELECT front_image from movies_images WHERE movie_id = ?` - return getImage(ctx, qb.tx, query, movieID) + return qb.GetImage(ctx, movieID, movieFrontImageBlobColumn) } func (qb *movieQueryBuilder) GetBackImage(ctx context.Context, movieID int) ([]byte, error) { - query := `SELECT back_image from movies_images WHERE movie_id = ?` - return getImage(ctx, qb.tx, query, movieID) + return qb.GetImage(ctx, movieID, movieBackImageBlobColumn) +} + +func (qb *movieQueryBuilder) HasBackImage(ctx context.Context, movieID int) (bool, error) { + return qb.HasImage(ctx, movieID, movieBackImageBlobColumn) } func (qb *movieQueryBuilder) FindByPerformerID(ctx context.Context, performerID int) ([]*models.Movie, error) { diff --git a/pkg/sqlite/movies_test.go b/pkg/sqlite/movies_test.go index eff0cf50b..9180dde20 100644 --- a/pkg/sqlite/movies_test.go +++ b/pkg/sqlite/movies_test.go @@ -15,12 +15,11 @@ import ( "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sqlite" ) func TestMovieFindByName(t *testing.T) { withTxn(func(ctx context.Context) error { - mqb := sqlite.MovieReaderWriter + mqb := db.Movie name := movieNames[movieIdxWithScene] // find a movie by name @@ -53,7 +52,7 @@ func TestMovieFindByNames(t *testing.T) { withTxn(func(ctx context.Context) error { var names []string - mqb := sqlite.MovieReaderWriter + mqb := db.Movie names = append(names, movieNames[movieIdxWithScene]) // find movies by names @@ -76,9 +75,80 @@ func TestMovieFindByNames(t *testing.T) { }) } +func moviesToIDs(i []*models.Movie) []int { + ret := make([]int, len(i)) + for i, v := range i { + ret[i] = v.ID + } + + return ret +} + +func TestMovieQuery(t *testing.T) { + var ( + frontImage = "front_image" + backImage = "back_image" + ) + + tests := []struct { + name string + findFilter *models.FindFilterType + filter *models.MovieFilterType + includeIdxs []int + excludeIdxs []int + wantErr bool + }{ + { + "is missing front image", + nil, + &models.MovieFilterType{ + IsMissing: &frontImage, + }, + // just ensure that it doesn't error + nil, + nil, + false, + }, + { + "is missing back image", + nil, + &models.MovieFilterType{ + IsMissing: &backImage, + }, + // just ensure that it doesn't error + nil, + nil, + false, + }, + } + + for _, tt := range tests { + runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { + assert := assert.New(t) + + results, _, err := db.Movie.Query(ctx, tt.filter, tt.findFilter) + if (err != nil) != tt.wantErr { + t.Errorf("MovieQueryBuilder.Query() error = %v, wantErr %v", err, tt.wantErr) + return + } + + ids := moviesToIDs(results) + include := indexesToIDs(performerIDs, tt.includeIdxs) + exclude := indexesToIDs(performerIDs, tt.excludeIdxs) + + for _, i := range include { + assert.Contains(ids, i) + } + for _, e := range exclude { + assert.NotContains(ids, e) + } + }) + } +} + func TestMovieQueryStudio(t *testing.T) { withTxn(func(ctx context.Context) error { - mqb := sqlite.MovieReaderWriter + mqb := db.Movie studioCriterion := models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(studioIDs[studioIdxWithMovie]), @@ -163,7 +233,7 @@ func TestMovieQueryURL(t *testing.T) { func verifyMovieQuery(t *testing.T, filter models.MovieFilterType, verifyFn func(s *models.Movie)) { withTxn(func(ctx context.Context) error { t.Helper() - sqb := sqlite.MovieReaderWriter + sqb := db.Movie movies := queryMovie(ctx, t, sqb, &filter, nil) @@ -196,7 +266,7 @@ func TestMovieQuerySorting(t *testing.T) { } withTxn(func(ctx context.Context) error { - sqb := sqlite.MovieReaderWriter + sqb := db.Movie movies := queryMovie(ctx, t, sqb, nil, &findFilter) // scenes should be in same order as indexes @@ -216,122 +286,50 @@ func TestMovieQuerySorting(t *testing.T) { }) } -func TestMovieUpdateMovieImages(t *testing.T) { - if err := withTxn(func(ctx context.Context) error { - mqb := sqlite.MovieReaderWriter +func TestMovieUpdateFrontImage(t *testing.T) { + if err := withRollbackTxn(func(ctx context.Context) error { + qb := db.Movie // create movie to test against const name = "TestMovieUpdateMovieImages" - movie := models.Movie{ + toCreate := models.Movie{ Name: sql.NullString{String: name, Valid: true}, Checksum: md5.FromString(name), } - created, err := mqb.Create(ctx, movie) + movie, err := qb.Create(ctx, toCreate) if err != nil { return fmt.Errorf("Error creating movie: %s", err.Error()) } - frontImage := []byte("frontImage") - backImage := []byte("backImage") - err = mqb.UpdateImages(ctx, created.ID, frontImage, backImage) - if err != nil { - return fmt.Errorf("Error updating movie images: %s", err.Error()) - } - - // ensure images are set - storedFront, err := mqb.GetFrontImage(ctx, created.ID) - if err != nil { - return fmt.Errorf("Error getting front image: %s", err.Error()) - } - assert.Equal(t, storedFront, frontImage) - - storedBack, err := mqb.GetBackImage(ctx, created.ID) - if err != nil { - return fmt.Errorf("Error getting back image: %s", err.Error()) - } - assert.Equal(t, storedBack, backImage) - - // set front image only - newImage := []byte("newImage") - err = mqb.UpdateImages(ctx, created.ID, newImage, nil) - if err != nil { - return fmt.Errorf("Error updating movie images: %s", err.Error()) - } - - storedFront, err = mqb.GetFrontImage(ctx, created.ID) - if err != nil { - return fmt.Errorf("Error getting front image: %s", err.Error()) - } - assert.Equal(t, storedFront, newImage) - - // back image should be nil - storedBack, err = mqb.GetBackImage(ctx, created.ID) - if err != nil { - return fmt.Errorf("Error getting back image: %s", err.Error()) - } - assert.Nil(t, nil) - - // set back image only - err = mqb.UpdateImages(ctx, created.ID, nil, newImage) - if err == nil { - return fmt.Errorf("Expected error setting nil front image") - } - - return nil + return testUpdateImage(t, ctx, movie.ID, qb.UpdateFrontImage, qb.GetFrontImage) }); err != nil { t.Error(err.Error()) } } -func TestMovieDestroyMovieImages(t *testing.T) { - if err := withTxn(func(ctx context.Context) error { - mqb := sqlite.MovieReaderWriter +func TestMovieUpdateBackImage(t *testing.T) { + if err := withRollbackTxn(func(ctx context.Context) error { + qb := db.Movie // create movie to test against - const name = "TestMovieDestroyMovieImages" - movie := models.Movie{ + const name = "TestMovieUpdateMovieImages" + toCreate := models.Movie{ Name: sql.NullString{String: name, Valid: true}, Checksum: md5.FromString(name), } - created, err := mqb.Create(ctx, movie) + movie, err := qb.Create(ctx, toCreate) if err != nil { return fmt.Errorf("Error creating movie: %s", err.Error()) } - frontImage := []byte("frontImage") - backImage := []byte("backImage") - err = mqb.UpdateImages(ctx, created.ID, frontImage, backImage) - if err != nil { - return fmt.Errorf("Error updating movie images: %s", err.Error()) - } - - err = mqb.DestroyImages(ctx, created.ID) - if err != nil { - return fmt.Errorf("Error destroying movie images: %s", err.Error()) - } - - // front image should be nil - storedFront, err := mqb.GetFrontImage(ctx, created.ID) - if err != nil { - return fmt.Errorf("Error getting front image: %s", err.Error()) - } - assert.Nil(t, storedFront) - - // back image should be nil - storedBack, err := mqb.GetBackImage(ctx, created.ID) - if err != nil { - return fmt.Errorf("Error getting back image: %s", err.Error()) - } - assert.Nil(t, storedBack) - - return nil + return testUpdateImage(t, ctx, movie.ID, qb.UpdateBackImage, qb.GetBackImage) }); err != nil { t.Error(err.Error()) } } // TODO Update -// TODO Destroy +// TODO Destroy - ensure image is destroyed // TODO Find // TODO Count // TODO All diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index fae593ba6..f288401d3 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -17,33 +17,37 @@ import ( "gopkg.in/guregu/null.v4/zero" ) -const performerTable = "performers" -const performerIDColumn = "performer_id" -const performersTagsTable = "performers_tags" -const performersImageTable = "performers_image" // performer cover image +const ( + performerTable = "performers" + performerIDColumn = "performer_id" + performersAliasesTable = "performer_aliases" + performerAliasColumn = "alias" + performersTagsTable = "performers_tags" + + performerImageBlobColumn = "image_blob" +) type performerRow struct { - ID int `db:"id" goqu:"skipinsert"` - Checksum string `db:"checksum"` - Name zero.String `db:"name"` - Gender zero.String `db:"gender"` - URL zero.String `db:"url"` - Twitter zero.String `db:"twitter"` - Instagram zero.String `db:"instagram"` - Birthdate models.SQLiteDate `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"` - CareerLength zero.String `db:"career_length"` - Tattoos zero.String `db:"tattoos"` - Piercings zero.String `db:"piercings"` - Aliases zero.String `db:"aliases"` - Favorite sql.NullBool `db:"favorite"` - CreatedAt models.SQLiteTimestamp `db:"created_at"` - UpdatedAt models.SQLiteTimestamp `db:"updated_at"` + ID int `db:"id" goqu:"skipinsert"` + Name string `db:"name"` + Disambigation zero.String `db:"disambiguation"` + Gender zero.String `db:"gender"` + URL zero.String `db:"url"` + Twitter zero.String `db:"twitter"` + Instagram zero.String `db:"instagram"` + Birthdate models.SQLiteDate `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"` + CareerLength zero.String `db:"career_length"` + Tattoos zero.String `db:"tattoos"` + Piercings zero.String `db:"piercings"` + Favorite sql.NullBool `db:"favorite"` + CreatedAt models.SQLiteTimestamp `db:"created_at"` + UpdatedAt models.SQLiteTimestamp `db:"updated_at"` // expressed as 1-100 Rating null.Int `db:"rating"` Details zero.String `db:"details"` @@ -51,12 +55,15 @@ type performerRow struct { HairColor zero.String `db:"hair_color"` Weight null.Int `db:"weight"` IgnoreAutoTag bool `db:"ignore_auto_tag"` + + // not used for resolution + ImageBlob zero.String `db:"image_blob"` } func (r *performerRow) fromPerformer(o models.Performer) { r.ID = o.ID - r.Checksum = o.Checksum - r.Name = zero.StringFrom(o.Name) + r.Name = o.Name + r.Disambigation = zero.StringFrom(o.Disambiguation) if o.Gender.IsValid() { r.Gender = zero.StringFrom(o.Gender.String()) } @@ -75,7 +82,6 @@ func (r *performerRow) fromPerformer(o models.Performer) { r.CareerLength = zero.StringFrom(o.CareerLength) r.Tattoos = zero.StringFrom(o.Tattoos) r.Piercings = zero.StringFrom(o.Piercings) - r.Aliases = zero.StringFrom(o.Aliases) r.Favorite = sql.NullBool{Bool: o.Favorite, Valid: true} r.CreatedAt = models.SQLiteTimestamp{Timestamp: o.CreatedAt} r.UpdatedAt = models.SQLiteTimestamp{Timestamp: o.UpdatedAt} @@ -91,27 +97,26 @@ func (r *performerRow) fromPerformer(o models.Performer) { func (r *performerRow) resolve() *models.Performer { ret := &models.Performer{ - ID: r.ID, - Checksum: r.Checksum, - Name: r.Name.String, - Gender: models.GenderEnum(r.Gender.String), - URL: r.URL.String, - Twitter: r.Twitter.String, - Instagram: r.Instagram.String, - Birthdate: r.Birthdate.DatePtr(), - Ethnicity: r.Ethnicity.String, - Country: r.Country.String, - EyeColor: r.EyeColor.String, - Height: nullIntPtr(r.Height), - Measurements: r.Measurements.String, - FakeTits: r.FakeTits.String, - CareerLength: r.CareerLength.String, - Tattoos: r.Tattoos.String, - Piercings: r.Piercings.String, - Aliases: r.Aliases.String, - Favorite: r.Favorite.Bool, - CreatedAt: r.CreatedAt.Timestamp, - UpdatedAt: r.UpdatedAt.Timestamp, + ID: r.ID, + Name: r.Name, + Disambiguation: r.Disambigation.String, + Gender: models.GenderEnum(r.Gender.String), + URL: r.URL.String, + Twitter: r.Twitter.String, + Instagram: r.Instagram.String, + Birthdate: r.Birthdate.DatePtr(), + Ethnicity: r.Ethnicity.String, + Country: r.Country.String, + EyeColor: r.EyeColor.String, + Height: nullIntPtr(r.Height), + Measurements: r.Measurements.String, + FakeTits: r.FakeTits.String, + CareerLength: r.CareerLength.String, + Tattoos: r.Tattoos.String, + Piercings: r.Piercings.String, + Favorite: r.Favorite.Bool, + CreatedAt: r.CreatedAt.Timestamp, + UpdatedAt: r.UpdatedAt.Timestamp, // expressed as 1-100 Rating: nullIntPtr(r.Rating), Details: r.Details.String, @@ -129,8 +134,8 @@ type performerRowRecord struct { } func (r *performerRowRecord) fromPartial(o models.PerformerPartial) { - r.setNullString("checksum", o.Checksum) - r.setNullString("name", o.Name) + r.setString("name", o.Name) + r.setNullString("disambiguation", o.Disambiguation) r.setNullString("gender", o.Gender) r.setNullString("url", o.URL) r.setNullString("twitter", o.Twitter) @@ -145,7 +150,6 @@ func (r *performerRowRecord) fromPartial(o models.PerformerPartial) { r.setNullString("career_length", o.CareerLength) r.setNullString("tattoos", o.Tattoos) r.setNullString("piercings", o.Piercings) - r.setNullString("aliases", o.Aliases) r.setBool("favorite", o.Favorite) r.setSQLiteTimestamp("created_at", o.CreatedAt) r.setSQLiteTimestamp("updated_at", o.UpdatedAt) @@ -159,16 +163,21 @@ func (r *performerRowRecord) fromPartial(o models.PerformerPartial) { type PerformerStore struct { repository + blobJoinQueryBuilder tableMgr *table } -func NewPerformerStore() *PerformerStore { +func NewPerformerStore(blobStore *BlobStore) *PerformerStore { return &PerformerStore{ repository: repository{ tableName: performerTable, idColumn: idColumn, }, + blobJoinQueryBuilder: blobJoinQueryBuilder{ + blobStore: blobStore, + joinTable: performerTable, + }, tableMgr: performerTableMgr, } } @@ -182,6 +191,24 @@ func (qb *PerformerStore) Create(ctx context.Context, newObject *models.Performe return err } + if newObject.Aliases.Loaded() { + if err := performersAliasesTableMgr.insertJoins(ctx, id, newObject.Aliases.List()); err != nil { + return err + } + } + + if newObject.TagIDs.Loaded() { + if err := performersTagsTableMgr.insertJoins(ctx, id, newObject.TagIDs.List()); err != nil { + return err + } + } + + if newObject.StashIDs.Loaded() { + if err := performersStashIDsTableMgr.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) @@ -192,14 +219,14 @@ func (qb *PerformerStore) Create(ctx context.Context, newObject *models.Performe return nil } -func (qb *PerformerStore) UpdatePartial(ctx context.Context, id int, updatedObject models.PerformerPartial) (*models.Performer, error) { +func (qb *PerformerStore) UpdatePartial(ctx context.Context, id int, partial models.PerformerPartial) (*models.Performer, error) { r := performerRowRecord{ updateRecord{ Record: make(exp.Record), }, } - r.fromPartial(updatedObject) + r.fromPartial(partial) if len(r.Record) > 0 { if err := qb.tableMgr.updateByID(ctx, id, r.Record); err != nil { @@ -207,6 +234,23 @@ func (qb *PerformerStore) UpdatePartial(ctx context.Context, id int, updatedObje } } + if partial.Aliases != nil { + if err := performersAliasesTableMgr.modifyJoins(ctx, id, partial.Aliases.Values, partial.Aliases.Mode); err != nil { + return nil, err + } + } + + if partial.TagIDs != nil { + if err := performersTagsTableMgr.modifyJoins(ctx, id, partial.TagIDs.IDs, partial.TagIDs.Mode); err != nil { + return nil, err + } + } + if partial.StashIDs != nil { + if err := performersStashIDsTableMgr.modifyJoins(ctx, id, partial.StashIDs.StashIDs, partial.StashIDs.Mode); err != nil { + return nil, err + } + } + return qb.Find(ctx, id) } @@ -218,10 +262,33 @@ func (qb *PerformerStore) Update(ctx context.Context, updatedObject *models.Perf return err } + if updatedObject.Aliases.Loaded() { + if err := performersAliasesTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.Aliases.List()); err != nil { + return err + } + } + + if updatedObject.TagIDs.Loaded() { + if err := performersTagsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.TagIDs.List()); err != nil { + return err + } + } + + if updatedObject.StashIDs.Loaded() { + if err := performersStashIDsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.StashIDs.List()); err != nil { + return err + } + } + return nil } func (qb *PerformerStore) Destroy(ctx context.Context, id int) error { + // must handle image checksums manually + if err := qb.DestroyImage(ctx, id); err != nil { + return err + } + return qb.destroyExisting(ctx, []int{id}) } @@ -246,17 +313,23 @@ func (qb *PerformerStore) Find(ctx context.Context, id int) (*models.Performer, func (qb *PerformerStore) FindMany(ctx context.Context, ids []int) ([]*models.Performer, error) { tableMgr := performerTableMgr - q := goqu.Select("*").From(tableMgr.table).Where(tableMgr.byIDInts(ids...)) - unsorted, err := qb.getMany(ctx, q) - if err != nil { - return nil, err - } - ret := make([]*models.Performer, len(ids)) - for _, s := range unsorted { - i := intslice.IntIndex(ids, s.ID) - ret[i] = s + if err := batchExec(ids, defaultBatchSize, func(batch []int) error { + q := goqu.Select("*").From(tableMgr.table).Where(tableMgr.byIDInts(batch...)) + unsorted, err := qb.getMany(ctx, q) + if err != nil { + return err + } + + for _, s := range unsorted { + i := intslice.IntIndex(ids, s.ID) + ret[i] = s + } + + return nil + }); err != nil { + return nil, err } for i := range ret { @@ -397,14 +470,19 @@ func (qb *PerformerStore) QueryForAutoTag(ctx context.Context, words []string) ( // TODO - Query needs to be changed to support queries of this type, and // this method should be removed table := qb.table() - sq := dialect.From(table).Select(table.Col(idColumn)).Where() + sq := dialect.From(table).Select(table.Col(idColumn)) + // TODO - disabled alias matching until we get finer control over it + // .LeftJoin( + // performersAliasesJoinTable, + // goqu.On(performersAliasesJoinTable.Col(performerIDColumn).Eq(table.Col(idColumn))), + // ) var whereClauses []exp.Expression for _, w := range words { whereClauses = append(whereClauses, table.Col("name").Like(w+"%")) - // TODO - commented out until alias matching works both ways - // whereClauses = append(whereClauses, table.Col("aliases").Like(w+"%") + // TODO - see above + // whereClauses = append(whereClauses, performersAliasesJoinTable.Col("alias").Like(w+"%")) } sq = sq.Where( @@ -483,6 +561,7 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform const tableName = performerTable query.handleCriterion(ctx, stringCriterionHandler(filter.Name, tableName+".name")) + query.handleCriterion(ctx, stringCriterionHandler(filter.Disambiguation, tableName+".disambiguation")) query.handleCriterion(ctx, stringCriterionHandler(filter.Details, tableName+".details")) query.handleCriterion(ctx, boolCriterionHandler(filter.FilterFavorites, tableName+".favorite", nil)) @@ -540,8 +619,7 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform parentIDCol: "performers.id", }) - // TODO - need better handling of aliases - query.handleCriterion(ctx, stringCriterionHandler(filter.Aliases, tableName+".aliases")) + query.handleCriterion(ctx, performerAliasCriterionHandler(qb, filter.Aliases)) query.handleCriterion(ctx, performerTagsCriterionHandler(qb, filter.Tags)) @@ -559,7 +637,7 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform return query } -func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) { +func (qb *PerformerStore) makeQuery(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) { if performerFilter == nil { performerFilter = &models.PerformerFilterType{} } @@ -571,18 +649,31 @@ func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.Per distinctIDs(&query, performerTable) if q := findFilter.Q; q != nil && *q != "" { - searchColumns := []string{"performers.name", "performers.aliases"} + query.join(performersAliasesTable, "", "performer_aliases.performer_id = performers.id") + searchColumns := []string{"performers.name", "performer_aliases.alias"} query.parseQueryString(searchColumns, *q) } if err := qb.validateFilter(performerFilter); err != nil { - return nil, 0, err + return nil, err } filter := qb.makeFilter(ctx, performerFilter) - query.addFilter(filter) + if err := query.addFilter(filter); err != nil { + return nil, err + } query.sortAndPagination = qb.getPerformerSort(findFilter) + getPagination(findFilter) + + return &query, nil +} + +func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) { + query, err := qb.makeQuery(ctx, performerFilter, findFilter) + if err != nil { + return nil, 0, err + } + idsResult, countResult, err := query.executeFind(ctx) if err != nil { return nil, 0, err @@ -596,6 +687,15 @@ func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.Per return performers, countResult, nil } +func (qb *PerformerStore) QueryCount(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) (int, error) { + query, err := qb.makeQuery(ctx, performerFilter, findFilter) + if err != nil { + return 0, err + } + + return query.executeCount(ctx) +} + func performerIsMissingCriterionHandler(qb *PerformerStore, isMissing *string) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if isMissing != nil && *isMissing != "" { @@ -604,10 +704,9 @@ func performerIsMissingCriterionHandler(qb *PerformerStore, isMissing *string) c f.addLeftJoin(performersScenesTable, "scenes_join", "scenes_join.performer_id = performers.id") f.addWhere("scenes_join.scene_id IS NULL") case "image": - f.addLeftJoin(performersImageTable, "image_join", "image_join.performer_id = performers.id") - f.addWhere("image_join.performer_id IS NULL") + f.addWhere("performers.image_blob IS NULL") case "stash_id": - qb.stashIDRepository().join(f, "performer_stash_ids", "performers.id") + performersStashIDsTableMgr.join(f, "performer_stash_ids", "performers.id") f.addWhere("performer_stash_ids.performer_id IS NULL") default: f.addWhere("(performers." + *isMissing + " IS NULL OR TRIM(performers." + *isMissing + ") = '')") @@ -637,6 +736,18 @@ func performerAgeFilterCriterionHandler(age *models.IntCriterionInput) criterion } } +func performerAliasCriterionHandler(qb *PerformerStore, alias *models.StringCriterionInput) criterionHandlerFunc { + h := stringListCriterionHandlerBuilder{ + joinTable: performersAliasesTable, + stringColumn: performerAliasColumn, + addJoinTable: func(f *filterBuilder) { + performersAliasesTableMgr.join(f, "", "performers.id") + }, + } + + return h.handler(alias) +} + func performerTagsCriterionHandler(qb *PerformerStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { h := joinedHierarchicalMultiCriterionHandlerBuilder{ tx: qb.tx, @@ -813,32 +924,16 @@ func (qb *PerformerStore) GetTagIDs(ctx context.Context, id int) ([]int, error) return qb.tagsRepository().getIDs(ctx, id) } -func (qb *PerformerStore) UpdateTags(ctx context.Context, id int, tagIDs []int) error { - // Delete the existing joins and then create new ones - return qb.tagsRepository().replace(ctx, id, tagIDs) -} - -func (qb *PerformerStore) imageRepository() *imageRepository { - return &imageRepository{ - repository: repository{ - tx: qb.tx, - tableName: "performers_image", - idColumn: performerIDColumn, - }, - imageColumn: "image", - } -} - func (qb *PerformerStore) GetImage(ctx context.Context, performerID int) ([]byte, error) { - return qb.imageRepository().get(ctx, performerID) + return qb.blobJoinQueryBuilder.GetImage(ctx, performerID, performerImageBlobColumn) } func (qb *PerformerStore) UpdateImage(ctx context.Context, performerID int, image []byte) error { - return qb.imageRepository().replace(ctx, performerID, image) + return qb.blobJoinQueryBuilder.UpdateImage(ctx, performerID, performerImageBlobColumn, image) } func (qb *PerformerStore) DestroyImage(ctx context.Context, performerID int) error { - return qb.imageRepository().destroy(ctx, []int{performerID}) + return qb.blobJoinQueryBuilder.DestroyImage(ctx, performerID, performerImageBlobColumn) } func (qb *PerformerStore) stashIDRepository() *stashIDRepository { @@ -851,12 +946,12 @@ func (qb *PerformerStore) stashIDRepository() *stashIDRepository { } } -func (qb *PerformerStore) GetStashIDs(ctx context.Context, performerID int) ([]models.StashID, error) { - return qb.stashIDRepository().get(ctx, performerID) +func (qb *PerformerStore) GetAliases(ctx context.Context, performerID int) ([]string, error) { + return performersAliasesTableMgr.get(ctx, performerID) } -func (qb *PerformerStore) UpdateStashIDs(ctx context.Context, performerID int, stashIDs []models.StashID) error { - return qb.stashIDRepository().replace(ctx, performerID, stashIDs) +func (qb *PerformerStore) GetStashIDs(ctx context.Context, performerID int) ([]models.StashID, error) { + return performersStashIDsTableMgr.get(ctx, performerID) } func (qb *PerformerStore) FindByStashID(ctx context.Context, stashID models.StashID) ([]*models.Performer, error) { diff --git a/pkg/sqlite/performer_test.go b/pkg/sqlite/performer_test.go index 934287524..2b24d6455 100644 --- a/pkg/sqlite/performer_test.go +++ b/pkg/sqlite/performer_test.go @@ -12,37 +12,204 @@ import ( "testing" "time" - "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/models" "github.com/stretchr/testify/assert" ) +func loadPerformerRelationships(ctx context.Context, expected models.Performer, actual *models.Performer) error { + if expected.Aliases.Loaded() { + if err := actual.LoadAliases(ctx, db.Performer); err != nil { + return err + } + } + if expected.TagIDs.Loaded() { + if err := actual.LoadTagIDs(ctx, db.Performer); err != nil { + return err + } + } + if expected.StashIDs.Loaded() { + if err := actual.LoadStashIDs(ctx, db.Performer); err != nil { + return err + } + } + + return nil +} + +func Test_PerformerStore_Create(t *testing.T) { + var ( + name = "name" + disambiguation = "disambiguation" + gender = models.GenderEnumFemale + details = "details" + url = "url" + twitter = "twitter" + instagram = "instagram" + rating = 3 + ethnicity = "ethnicity" + country = "country" + eyeColor = "eyeColor" + height = 134 + measurements = "measurements" + fakeTits = "fakeTits" + careerLength = "careerLength" + tattoos = "tattoos" + piercings = "piercings" + aliases = []string{"alias1", "alias2"} + hairColor = "hairColor" + weight = 123 + 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) + + birthdate = models.NewDate("2003-02-01") + deathdate = models.NewDate("2023-02-01") + ) + + tests := []struct { + name string + newObject models.Performer + wantErr bool + }{ + { + "full", + models.Performer{ + Name: name, + Disambiguation: disambiguation, + Gender: gender, + URL: url, + Twitter: twitter, + Instagram: instagram, + Birthdate: &birthdate, + Ethnicity: ethnicity, + Country: country, + EyeColor: eyeColor, + Height: &height, + Measurements: measurements, + FakeTits: fakeTits, + CareerLength: careerLength, + Tattoos: tattoos, + Piercings: piercings, + Favorite: favorite, + Rating: &rating, + Details: details, + DeathDate: &deathdate, + HairColor: hairColor, + Weight: &weight, + IgnoreAutoTag: ignoreAutoTag, + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}), + Aliases: models.NewRelatedStrings(aliases), + StashIDs: models.NewRelatedStashIDs([]models.StashID{ + { + StashID: stashID1, + Endpoint: endpoint1, + }, + { + StashID: stashID2, + Endpoint: endpoint2, + }, + }), + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, + false, + }, + { + "invalid tag id", + models.Performer{ + Name: name, + TagIDs: models.NewRelatedIDs([]int{invalidID}), + }, + true, + }, + } + + qb := db.Performer + + 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("PerformerStore.Create() error = %v, wantErr = %v", err, tt.wantErr) + } + + if tt.wantErr { + assert.Zero(p.ID) + return + } + + assert.NotZero(p.ID) + + copy := tt.newObject + copy.ID = p.ID + + // load relationships + if err := loadPerformerRelationships(ctx, copy, &p); err != nil { + t.Errorf("loadPerformerRelationships() error = %v", err) + return + } + + assert.Equal(copy, p) + + // ensure can find the performer + found, err := qb.Find(ctx, p.ID) + if err != nil { + t.Errorf("PerformerStore.Find() error = %v", err) + } + + if !assert.NotNil(found) { + return + } + + // load relationships + if err := loadPerformerRelationships(ctx, copy, found); err != nil { + t.Errorf("loadPerformerRelationships() error = %v", err) + return + } + assert.Equal(copy, *found) + + return + }) + } +} + func Test_PerformerStore_Update(t *testing.T) { var ( - name = "name" - gender = models.GenderEnumFemale - checksum = "checksum" - details = "details" - url = "url" - twitter = "twitter" - instagram = "instagram" - rating = 3 - ethnicity = "ethnicity" - country = "country" - eyeColor = "eyeColor" - height = 134 - measurements = "measurements" - fakeTits = "fakeTits" - careerLength = "careerLength" - tattoos = "tattoos" - piercings = "piercings" - aliases = "aliases" - hairColor = "hairColor" - weight = 123 - ignoreAutoTag = true - favorite = true - createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) - updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + name = "name" + disambiguation = "disambiguation" + gender = models.GenderEnumFemale + details = "details" + url = "url" + twitter = "twitter" + instagram = "instagram" + rating = 3 + ethnicity = "ethnicity" + country = "country" + eyeColor = "eyeColor" + height = 134 + measurements = "measurements" + fakeTits = "fakeTits" + careerLength = "careerLength" + tattoos = "tattoos" + piercings = "piercings" + aliases = []string{"alias1", "alias2"} + hairColor = "hairColor" + weight = 123 + 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) birthdate = models.NewDate("2003-02-01") deathdate = models.NewDate("2023-02-01") @@ -56,43 +223,73 @@ func Test_PerformerStore_Update(t *testing.T) { { "full", &models.Performer{ - ID: performerIDs[performerIdxWithGallery], - Name: name, - Checksum: checksum, - Gender: gender, - URL: url, - Twitter: twitter, - Instagram: instagram, - Birthdate: &birthdate, - Ethnicity: ethnicity, - Country: country, - EyeColor: eyeColor, - Height: &height, - Measurements: measurements, - FakeTits: fakeTits, - CareerLength: careerLength, - Tattoos: tattoos, - Piercings: piercings, - Aliases: aliases, - Favorite: favorite, - Rating: &rating, - Details: details, - DeathDate: &deathdate, - HairColor: hairColor, - Weight: &weight, - IgnoreAutoTag: ignoreAutoTag, - CreatedAt: createdAt, - UpdatedAt: updatedAt, + ID: performerIDs[performerIdxWithGallery], + Name: name, + Disambiguation: disambiguation, + Gender: gender, + URL: url, + Twitter: twitter, + Instagram: instagram, + Birthdate: &birthdate, + Ethnicity: ethnicity, + Country: country, + EyeColor: eyeColor, + Height: &height, + Measurements: measurements, + FakeTits: fakeTits, + CareerLength: careerLength, + Tattoos: tattoos, + Piercings: piercings, + Favorite: favorite, + Rating: &rating, + Details: details, + DeathDate: &deathdate, + HairColor: hairColor, + Weight: &weight, + IgnoreAutoTag: ignoreAutoTag, + Aliases: models.NewRelatedStrings(aliases), + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{ + { + StashID: stashID1, + Endpoint: endpoint1, + }, + { + StashID: stashID2, + Endpoint: endpoint2, + }, + }), + CreatedAt: createdAt, + UpdatedAt: updatedAt, }, false, }, { - "clear all", + "clear nullables", &models.Performer{ - ID: performerIDs[performerIdxWithGallery], + ID: performerIDs[performerIdxWithGallery], + Aliases: models.NewRelatedStrings([]string{}), + TagIDs: models.NewRelatedIDs([]int{}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{}), }, false, }, + { + "clear tag ids", + &models.Performer{ + ID: performerIDs[sceneIdxWithTag], + TagIDs: models.NewRelatedIDs([]int{}), + }, + false, + }, + { + "invalid tag id", + &models.Performer{ + ID: performerIDs[sceneIdxWithGallery], + TagIDs: models.NewRelatedIDs([]int{invalidID}), + }, + true, + }, } qb := db.Performer @@ -115,37 +312,80 @@ func Test_PerformerStore_Update(t *testing.T) { t.Errorf("PerformerStore.Find() error = %v", err) } + // load relationships + if err := loadPerformerRelationships(ctx, copy, s); err != nil { + t.Errorf("loadPerformerRelationships() error = %v", err) + return + } + assert.Equal(copy, *s) }) } } +func clearPerformerPartial() models.PerformerPartial { + nullString := models.OptionalString{Set: true, Null: true} + nullDate := models.OptionalDate{Set: true, Null: true} + nullInt := models.OptionalInt{Set: true, Null: true} + + // leave mandatory fields + return models.PerformerPartial{ + Disambiguation: nullString, + Gender: nullString, + URL: nullString, + Twitter: nullString, + Instagram: nullString, + Birthdate: nullDate, + Ethnicity: nullString, + Country: nullString, + EyeColor: nullString, + Height: nullInt, + Measurements: nullString, + FakeTits: nullString, + CareerLength: nullString, + Tattoos: nullString, + Piercings: nullString, + Aliases: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet}, + Rating: nullInt, + Details: nullString, + DeathDate: nullDate, + HairColor: nullString, + Weight: nullInt, + TagIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, + StashIDs: &models.UpdateStashIDs{Mode: models.RelationshipUpdateModeSet}, + } +} + func Test_PerformerStore_UpdatePartial(t *testing.T) { var ( - name = "name" - gender = models.GenderEnumFemale - checksum = "checksum" - details = "details" - url = "url" - twitter = "twitter" - instagram = "instagram" - rating = 3 - ethnicity = "ethnicity" - country = "country" - eyeColor = "eyeColor" - height = 143 - measurements = "measurements" - fakeTits = "fakeTits" - careerLength = "careerLength" - tattoos = "tattoos" - piercings = "piercings" - aliases = "aliases" - hairColor = "hairColor" - weight = 123 - ignoreAutoTag = true - favorite = true - createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) - updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) + name = "name" + disambiguation = "disambiguation" + gender = models.GenderEnumFemale + details = "details" + url = "url" + twitter = "twitter" + instagram = "instagram" + rating = 3 + ethnicity = "ethnicity" + country = "country" + eyeColor = "eyeColor" + height = 143 + measurements = "measurements" + fakeTits = "fakeTits" + careerLength = "careerLength" + tattoos = "tattoos" + piercings = "piercings" + aliases = []string{"alias1", "alias2"} + hairColor = "hairColor" + weight = 123 + 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) birthdate = models.NewDate("2003-02-01") deathdate = models.NewDate("2023-02-01") @@ -162,23 +402,26 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { "full", performerIDs[performerIdxWithDupName], models.PerformerPartial{ - Name: models.NewOptionalString(name), - Checksum: models.NewOptionalString(checksum), - Gender: models.NewOptionalString(gender.String()), - URL: models.NewOptionalString(url), - Twitter: models.NewOptionalString(twitter), - Instagram: models.NewOptionalString(instagram), - Birthdate: models.NewOptionalDate(birthdate), - Ethnicity: models.NewOptionalString(ethnicity), - Country: models.NewOptionalString(country), - EyeColor: models.NewOptionalString(eyeColor), - Height: models.NewOptionalInt(height), - Measurements: models.NewOptionalString(measurements), - FakeTits: models.NewOptionalString(fakeTits), - CareerLength: models.NewOptionalString(careerLength), - Tattoos: models.NewOptionalString(tattoos), - Piercings: models.NewOptionalString(piercings), - Aliases: models.NewOptionalString(aliases), + Name: models.NewOptionalString(name), + Disambiguation: models.NewOptionalString(disambiguation), + Gender: models.NewOptionalString(gender.String()), + URL: models.NewOptionalString(url), + Twitter: models.NewOptionalString(twitter), + Instagram: models.NewOptionalString(instagram), + Birthdate: models.NewOptionalDate(birthdate), + Ethnicity: models.NewOptionalString(ethnicity), + Country: models.NewOptionalString(country), + EyeColor: models.NewOptionalString(eyeColor), + Height: models.NewOptionalInt(height), + Measurements: models.NewOptionalString(measurements), + FakeTits: models.NewOptionalString(fakeTits), + CareerLength: models.NewOptionalString(careerLength), + Tattoos: models.NewOptionalString(tattoos), + Piercings: models.NewOptionalString(piercings), + Aliases: &models.UpdateStrings{ + Values: aliases, + Mode: models.RelationshipUpdateModeSet, + }, Favorite: models.NewOptionalBool(favorite), Rating: models.NewOptionalInt(rating), Details: models.NewOptionalString(details), @@ -186,40 +429,89 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { HairColor: models.NewOptionalString(hairColor), Weight: models.NewOptionalInt(weight), IgnoreAutoTag: models.NewOptionalBool(ignoreAutoTag), - CreatedAt: models.NewOptionalTime(createdAt), - UpdatedAt: models.NewOptionalTime(updatedAt), + TagIDs: &models.UpdateIDs{ + IDs: []int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}, + Mode: models.RelationshipUpdateModeSet, + }, + StashIDs: &models.UpdateStashIDs{ + StashIDs: []models.StashID{ + { + StashID: stashID1, + Endpoint: endpoint1, + }, + { + StashID: stashID2, + Endpoint: endpoint2, + }, + }, + Mode: models.RelationshipUpdateModeSet, + }, + CreatedAt: models.NewOptionalTime(createdAt), + UpdatedAt: models.NewOptionalTime(updatedAt), }, models.Performer{ - ID: performerIDs[performerIdxWithDupName], - Name: name, - Checksum: checksum, - Gender: gender, - URL: url, - Twitter: twitter, - Instagram: instagram, - Birthdate: &birthdate, - Ethnicity: ethnicity, - Country: country, - EyeColor: eyeColor, - Height: &height, - Measurements: measurements, - FakeTits: fakeTits, - CareerLength: careerLength, - Tattoos: tattoos, - Piercings: piercings, - Aliases: aliases, - Favorite: favorite, - Rating: &rating, - Details: details, - DeathDate: &deathdate, - HairColor: hairColor, - Weight: &weight, - IgnoreAutoTag: ignoreAutoTag, - CreatedAt: createdAt, - UpdatedAt: updatedAt, + ID: performerIDs[performerIdxWithDupName], + Name: name, + Disambiguation: disambiguation, + Gender: gender, + URL: url, + Twitter: twitter, + Instagram: instagram, + Birthdate: &birthdate, + Ethnicity: ethnicity, + Country: country, + EyeColor: eyeColor, + Height: &height, + Measurements: measurements, + FakeTits: fakeTits, + CareerLength: careerLength, + Tattoos: tattoos, + Piercings: piercings, + Aliases: models.NewRelatedStrings(aliases), + Favorite: favorite, + Rating: &rating, + Details: details, + DeathDate: &deathdate, + HairColor: hairColor, + Weight: &weight, + IgnoreAutoTag: ignoreAutoTag, + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithPerformer], tagIDs[tagIdx1WithDupName]}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{ + { + StashID: stashID1, + Endpoint: endpoint1, + }, + { + StashID: stashID2, + Endpoint: endpoint2, + }, + }), + CreatedAt: createdAt, + UpdatedAt: updatedAt, }, false, }, + { + "clear all", + performerIDs[performerIdxWithTwoTags], + clearPerformerPartial(), + models.Performer{ + ID: performerIDs[performerIdxWithTwoTags], + Name: getPerformerStringValue(performerIdxWithTwoTags, "Name"), + Favorite: true, + Aliases: models.NewRelatedStrings([]string{}), + TagIDs: models.NewRelatedIDs([]int{}), + StashIDs: models.NewRelatedStashIDs([]models.StashID{}), + }, + false, + }, + { + "invalid id", + invalidID, + models.PerformerPartial{}, + models.Performer{}, + true, + }, } for _, tt := range tests { qb := db.Performer @@ -237,6 +529,11 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { return } + if err := loadPerformerRelationships(ctx, tt.want, got); err != nil { + t.Errorf("loadPerformerRelationships() error = %v", err) + return + } + assert.Equal(tt.want, *got) s, err := qb.Find(ctx, tt.id) @@ -244,6 +541,12 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) { t.Errorf("PerformerStore.Find() error = %v", err) } + // load relationships + if err := loadPerformerRelationships(ctx, tt.want, s); err != nil { + t.Errorf("loadPerformerRelationships() error = %v", err) + return + } + assert.Equal(tt.want, *s) }) } @@ -653,6 +956,19 @@ func TestPerformerQuery(t *testing.T) { nil, false, }, + { + "alias", + nil, + &models.PerformerFilterType{ + Aliases: &models.StringCriterionInput{ + Value: getPerformerStringValue(performerIdxWithGallery, "alias"), + Modifier: models.CriterionModifierEquals, + }, + }, + []int{performerIdxWithGallery}, + []int{performerIdxWithScene}, + false, + }, } for _, tt := range tests { @@ -706,34 +1022,14 @@ func TestPerformerUpdatePerformerImage(t *testing.T) { // create performer to test against const name = "TestPerformerUpdatePerformerImage" performer := models.Performer{ - Name: name, - Checksum: md5.FromString(name), + Name: name, } err := qb.Create(ctx, &performer) if err != nil { return fmt.Errorf("Error creating performer: %s", err.Error()) } - image := []byte("image") - err = qb.UpdateImage(ctx, performer.ID, image) - if err != nil { - return fmt.Errorf("Error updating performer image: %s", err.Error()) - } - - // ensure image set - storedImage, err := qb.GetImage(ctx, performer.ID) - if err != nil { - return fmt.Errorf("Error getting image: %s", err.Error()) - } - assert.Equal(t, storedImage, image) - - // set nil image - err = qb.UpdateImage(ctx, performer.ID, nil) - if err == nil { - return fmt.Errorf("Expected error setting nil image") - } - - return nil + return testUpdateImage(t, ctx, performer.ID, qb.UpdateImage, qb.GetImage) }); err != nil { t.Error(err.Error()) } @@ -746,8 +1042,7 @@ func TestPerformerDestroyPerformerImage(t *testing.T) { // create performer to test against const name = "TestPerformerDestroyPerformerImage" performer := models.Performer{ - Name: name, - Checksum: md5.FromString(name), + Name: name, } err := qb.Create(ctx, &performer) if err != nil { @@ -819,11 +1114,14 @@ func verifyPerformerAge(t *testing.T, ageCriterion models.IntCriterionInput) { d := performer.Birthdate.Time age := cd.Year() - d.Year() - if cd.YearDay() < d.YearDay() { + // using YearDay screws up on leap years + if cd.Month() < d.Month() || (cd.Month() == d.Month() && cd.Day() < d.Day()) { age = age - 1 } - verifyInt(t, age, ageCriterion) + if !verifyInt(t, age, ageCriterion) { + t.Errorf("Performer birthdate: %s, deathdate: %s", performer.Birthdate.String(), performer.DeathDate.String()) + } } return nil @@ -925,6 +1223,7 @@ func verifyPerformerQuery(t *testing.T, filter models.PerformerFilterType, verif } func queryPerformers(ctx context.Context, t *testing.T, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) []*models.Performer { + t.Helper() performers, _, err := db.Performer.Query(ctx, performerFilter, findFilter) if err != nil { t.Errorf("Error querying performers: %s", err.Error()) @@ -1253,23 +1552,78 @@ func TestPerformerStashIDs(t *testing.T) { if err := withRollbackTxn(func(ctx context.Context) error { qb := db.Performer - // create performer to test against - const name = "TestStashIDs" - performer := models.Performer{ - Name: name, - Checksum: md5.FromString(name), + // create scene to test against + const name = "TestPerformerStashIDs" + performer := &models.Performer{ + Name: name, } - err := qb.Create(ctx, &performer) - if err != nil { + if err := qb.Create(ctx, performer); err != nil { return fmt.Errorf("Error creating performer: %s", err.Error()) } - testStashIDReaderWriter(ctx, t, qb, performer.ID) + if err := performer.LoadStashIDs(ctx, qb); err != nil { + return err + } + + testPerformerStashIDs(ctx, t, performer) return nil }); err != nil { t.Error(err.Error()) } } + +func testPerformerStashIDs(ctx context.Context, t *testing.T, s *models.Performer) { + // ensure no stash IDs to begin with + assert.Len(t, s.StashIDs.List(), 0) + + // add stash ids + const stashIDStr = "stashID" + const endpoint = "endpoint" + stashID := models.StashID{ + StashID: stashIDStr, + Endpoint: endpoint, + } + + qb := db.Performer + + // update stash ids and ensure was updated + var err error + s, err = qb.UpdatePartial(ctx, s.ID, models.PerformerPartial{ + StashIDs: &models.UpdateStashIDs{ + StashIDs: []models.StashID{stashID}, + Mode: models.RelationshipUpdateModeSet, + }, + }) + if err != nil { + t.Error(err.Error()) + } + + if err := s.LoadStashIDs(ctx, qb); err != nil { + t.Error(err.Error()) + return + } + + assert.Equal(t, []models.StashID{stashID}, s.StashIDs.List()) + + // remove stash ids and ensure was updated + s, err = qb.UpdatePartial(ctx, s.ID, models.PerformerPartial{ + StashIDs: &models.UpdateStashIDs{ + StashIDs: []models.StashID{stashID}, + Mode: models.RelationshipUpdateModeRemove, + }, + }) + if err != nil { + t.Error(err.Error()) + } + + if err := s.LoadStashIDs(ctx, qb); err != nil { + t.Error(err.Error()) + return + } + + assert.Len(t, s.StashIDs.List(), 0) +} + func TestPerformerQueryLegacyRating(t *testing.T) { const rating = 3 ratingCriterion := models.IntCriterionInput{ diff --git a/pkg/sqlite/query.go b/pkg/sqlite/query.go index 00b790955..597ab66b9 100644 --- a/pkg/sqlite/query.go +++ b/pkg/sqlite/query.go @@ -22,8 +22,6 @@ type queryBuilder struct { recursiveWith bool sortAndPagination string - - err error } func (qb queryBuilder) body() string { @@ -61,20 +59,11 @@ func (qb queryBuilder) findIDs(ctx context.Context) ([]int, error) { } func (qb queryBuilder) executeFind(ctx context.Context) ([]int, int, error) { - if qb.err != nil { - return nil, 0, qb.err - } - body := qb.body() - 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) { - if qb.err != nil { - return 0, qb.err - } - body := qb.body() withClause := "" @@ -136,11 +125,10 @@ func (qb *queryBuilder) addJoins(joins ...join) { qb.joins.add(joins...) } -func (qb *queryBuilder) addFilter(f *filterBuilder) { +func (qb *queryBuilder) addFilter(f *filterBuilder) error { err := f.getError() if err != nil { - qb.err = err - return + return err } clause, args := f.generateWithClauses() @@ -172,6 +160,8 @@ func (qb *queryBuilder) addFilter(f *filterBuilder) { } qb.addJoins(f.getAllJoins()...) + + return nil } func (qb *queryBuilder) parseQueryString(columns []string, q string) { diff --git a/pkg/sqlite/record.go b/pkg/sqlite/record.go index 7e79dc721..fbee73e86 100644 --- a/pkg/sqlite/record.go +++ b/pkg/sqlite/record.go @@ -14,14 +14,14 @@ func (r *updateRecord) set(destField string, v interface{}) { r.Record[destField] = v } -// func (r *updateRecord) setString(destField string, v models.OptionalString) { -// if v.Set { -// if v.Null { -// panic("null value not allowed in optional string") -// } -// r.set(destField, v.Value) -// } -// } +func (r *updateRecord) setString(destField string, v models.OptionalString) { + if v.Set { + if v.Null { + panic("null value not allowed in optional string") + } + r.set(destField, v.Value) + } +} func (r *updateRecord) setNullString(destField string, v models.OptionalString) { if v.Set { @@ -32,7 +32,7 @@ func (r *updateRecord) setNullString(destField string, v models.OptionalString) func (r *updateRecord) setBool(destField string, v models.OptionalBool) { if v.Set { if v.Null { - panic("null value not allowed in optional int") + panic("null value not allowed in optional bool") } r.set(destField, v.Value) } @@ -102,11 +102,11 @@ func (r *updateRecord) setSQLiteDate(destField string, v models.OptionalDate) { if v.Set { if v.Null { r.set(destField, models.SQLiteDate{}) + } else { + r.set(destField, models.SQLiteDate{ + String: v.Value.String(), + Valid: true, + }) } - - r.set(destField, models.SQLiteDate{ - String: v.Value.String(), - Valid: true, - }) } } diff --git a/pkg/sqlite/repository.go b/pkg/sqlite/repository.go index fe7c6c7da..c3b1d74aa 100644 --- a/pkg/sqlite/repository.go +++ b/pkg/sqlite/repository.go @@ -387,29 +387,6 @@ func (r *joinRepository) replace(ctx context.Context, id int, foreignIDs []int) return nil } -type imageRepository struct { - repository - imageColumn string -} - -func (r *imageRepository) get(ctx context.Context, id int) ([]byte, error) { - query := fmt.Sprintf("SELECT %s from %s WHERE %s = ?", r.imageColumn, r.tableName, r.idColumn) - var ret []byte - err := r.querySimple(ctx, query, []interface{}{id}, &ret) - return ret, err -} - -func (r *imageRepository) replace(ctx context.Context, id int, image []byte) error { - if err := r.destroy(ctx, []int{id}); err != nil { - return err - } - - stmt := fmt.Sprintf("INSERT INTO %s (%s, %s) VALUES (?, ?)", r.tableName, r.idColumn, r.imageColumn) - _, err := r.tx.Exec(ctx, stmt, id, image) - - return err -} - type captionRepository struct { repository } diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index 341e9ee26..a5e903653 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "path/filepath" + "sort" "strconv" "strings" "time" @@ -30,6 +31,8 @@ const ( scenesTagsTable = "scenes_tags" scenesGalleriesTable = "scenes_galleries" moviesScenesTable = "movies_scenes" + + sceneCoverBlobColumn = "cover_blob" ) var findExactDuplicateQuery = ` @@ -71,6 +74,9 @@ type sceneRow struct { ResumeTime float64 `db:"resume_time"` PlayDuration float64 `db:"play_duration"` PlayCount int `db:"play_count"` + + // not used in resolutions or updates + CoverBlob zero.String `db:"cover_blob"` } func (r *sceneRow) fromScene(o models.Scene) { @@ -171,6 +177,7 @@ func (r *sceneRowRecord) fromPartial(o models.ScenePartial) { type SceneStore struct { repository + blobJoinQueryBuilder tableMgr *table oCounterManager @@ -178,12 +185,16 @@ type SceneStore struct { fileStore *FileStore } -func NewSceneStore(fileStore *FileStore) *SceneStore { +func NewSceneStore(fileStore *FileStore, blobStore *BlobStore) *SceneStore { return &SceneStore{ repository: repository{ tableName: sceneTable, idColumn: idColumn, }, + blobJoinQueryBuilder: blobJoinQueryBuilder{ + blobStore: blobStore, + joinTable: sceneTable, + }, tableMgr: sceneTableMgr, oCounterManager: oCounterManager{sceneTableMgr}, @@ -352,6 +363,11 @@ func (qb *SceneStore) Update(ctx context.Context, updatedObject *models.Scene) e } func (qb *SceneStore) Destroy(ctx context.Context, id int) error { + // must handle image checksums manually + if err := qb.destroyCover(ctx, id); err != nil { + return err + } + // scene markers should be handled prior to calling destroy // galleries should be handled prior to calling destroy @@ -363,18 +379,24 @@ func (qb *SceneStore) Find(ctx context.Context, id int) (*models.Scene, error) { } func (qb *SceneStore) FindMany(ctx context.Context, ids []int) ([]*models.Scene, error) { - table := qb.table() - q := qb.selectDataset().Prepared(true).Where(table.Col(idColumn).In(ids)) - unsorted, err := qb.getMany(ctx, q) - if err != nil { - return nil, err - } - scenes := make([]*models.Scene, len(ids)) - for _, s := range unsorted { - i := intslice.IntIndex(ids, s.ID) - scenes[i] = s + table := qb.table() + if err := batchExec(ids, defaultBatchSize, func(batch []int) error { + q := qb.selectDataset().Prepared(true).Where(table.Col(idColumn).In(batch)) + unsorted, err := qb.getMany(ctx, q) + if err != nil { + return err + } + + for _, s := range unsorted { + i := intslice.IntIndex(ids, s.ID) + scenes[i] = s + } + + return nil + }); err != nil { + return nil, err } for i := range scenes { @@ -990,7 +1012,9 @@ func (qb *SceneStore) Query(ctx context.Context, options models.SceneQueryOption } filter := qb.makeFilter(ctx, sceneFilter) - query.addFilter(filter) + if err := query.addFilter(filter); err != nil { + return nil, err + } qb.setSceneSort(&query, findFilter) query.sortAndPagination += getPagination(findFilter) @@ -1178,6 +1202,8 @@ func sceneIsMissingCriterionHandler(qb *SceneStore, isMissing *string) criterion qb.addSceneFilesTable(f) f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'") f.addWhere("fingerprints_phash.fingerprint IS NULL") + case "cover": + f.addWhere("scenes.cover_blob IS NULL") default: f.addWhere("(scenes." + *isMissing + " IS NULL OR TRIM(scenes." + *isMissing + ") = '')") } @@ -1446,7 +1472,7 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF case "title": addFileTable() addFolderTable() - query.sortAndPagination += " ORDER BY scenes.title COLLATE NATURAL_CS " + direction + ", folders.path " + direction + ", files.basename COLLATE NATURAL_CS " + direction + query.sortAndPagination += " ORDER BY COALESCE(scenes.title, files.basename) COLLATE NATURAL_CS " + direction + ", folders.path " + direction case "play_count": // handle here since getSort has special handling for _count suffix query.sortAndPagination += " ORDER BY scenes.play_count " + direction @@ -1455,17 +1481,6 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF } } -func (qb *SceneStore) imageRepository() *imageRepository { - return &imageRepository{ - repository: repository{ - tx: qb.tx, - tableName: "scenes_cover", - idColumn: sceneIDColumn, - }, - imageColumn: "cover", - } -} - func (qb *SceneStore) getPlayCount(ctx context.Context, id int) (int, error) { q := dialect.From(qb.tableMgr.table).Select("play_count").Where(goqu.Ex{"id": id}) @@ -1523,15 +1538,19 @@ func (qb *SceneStore) IncrementWatchCount(ctx context.Context, id int) (int, err } func (qb *SceneStore) GetCover(ctx context.Context, sceneID int) ([]byte, error) { - return qb.imageRepository().get(ctx, sceneID) + return qb.GetImage(ctx, sceneID, sceneCoverBlobColumn) +} + +func (qb *SceneStore) HasCover(ctx context.Context, sceneID int) (bool, error) { + return qb.HasImage(ctx, sceneID, sceneCoverBlobColumn) } func (qb *SceneStore) UpdateCover(ctx context.Context, sceneID int, image []byte) error { - return qb.imageRepository().replace(ctx, sceneID, image) + return qb.UpdateImage(ctx, sceneID, sceneCoverBlobColumn, image) } -func (qb *SceneStore) DestroyCover(ctx context.Context, sceneID int) error { - return qb.imageRepository().destroy(ctx, []int{sceneID}) +func (qb *SceneStore) destroyCover(ctx context.Context, sceneID int) error { + return qb.DestroyImage(ctx, sceneID, sceneCoverBlobColumn) } func (qb *SceneStore) AssignFiles(ctx context.Context, sceneID int, fileIDs []file.ID) error { @@ -1704,5 +1723,26 @@ func (qb *SceneStore) FindDuplicates(ctx context.Context, distance int) ([][]*mo } } + sortByPath(duplicates) + return duplicates, nil } + +func sortByPath(scenes [][]*models.Scene) { + lessFunc := func(i int, j int) bool { + firstPathI := getFirstPath(scenes[i]) + firstPathJ := getFirstPath(scenes[j]) + return firstPathI < firstPathJ + } + sort.SliceStable(scenes, lessFunc) +} + +func getFirstPath(scenes []*models.Scene) string { + var firstPath string + for i, scene := range scenes { + if i == 0 || scene.Path < firstPath { + firstPath = scene.Path + } + } + return firstPath +} diff --git a/pkg/sqlite/scene_marker.go b/pkg/sqlite/scene_marker.go index b0ed5ac84..df3c73030 100644 --- a/pkg/sqlite/scene_marker.go +++ b/pkg/sqlite/scene_marker.go @@ -158,7 +158,9 @@ func (qb *sceneMarkerQueryBuilder) Query(ctx context.Context, sceneMarkerFilter filter := qb.makeFilter(ctx, sceneMarkerFilter) - query.addFilter(filter) + if err := query.addFilter(filter); err != nil { + return nil, 0, err + } query.sortAndPagination = qb.getSceneMarkerSort(&query, findFilter) + getPagination(findFilter) idsResult, countResult, err := query.executeFind(ctx) @@ -344,3 +346,11 @@ func (qb *sceneMarkerQueryBuilder) UpdateTags(ctx context.Context, id int, tagID // Delete the existing joins and then create new ones return qb.tagsRepository().replace(ctx, id, tagIDs) } + +func (qb *sceneMarkerQueryBuilder) Count(ctx context.Context) (int, error) { + return qb.runCountQuery(ctx, qb.buildCountQuery("SELECT scene_markers.id FROM scene_markers"), nil) +} + +func (qb *sceneMarkerQueryBuilder) All(ctx context.Context) ([]*models.SceneMarker, error) { + return qb.querySceneMarkers(ctx, selectAll("scene_markers")+qb.getSceneMarkerSort(nil, nil), nil) +} diff --git a/pkg/sqlite/scene_marker_test.go b/pkg/sqlite/scene_marker_test.go index 76a4dd845..9c5ae866f 100644 --- a/pkg/sqlite/scene_marker_test.go +++ b/pkg/sqlite/scene_marker_test.go @@ -222,4 +222,6 @@ func queryMarkers(ctx context.Context, t *testing.T, sqb models.SceneMarkerReade // TODO Find // TODO GetMarkerStrings // TODO Wall +// TODO Count +// TODO All // TODO Query diff --git a/pkg/sqlite/scene_test.go b/pkg/sqlite/scene_test.go index 697dba113..560d3fcfc 100644 --- a/pkg/sqlite/scene_test.go +++ b/pkg/sqlite/scene_test.go @@ -2836,21 +2836,23 @@ func verifyScenesOCounter(t *testing.T, oCounterCriterion models.IntCriterionInp }) } -func verifyInt(t *testing.T, value int, criterion models.IntCriterionInput) { +func verifyInt(t *testing.T, value int, criterion models.IntCriterionInput) bool { t.Helper() assert := assert.New(t) if criterion.Modifier == models.CriterionModifierEquals { - assert.Equal(criterion.Value, value) + return assert.Equal(criterion.Value, value) } if criterion.Modifier == models.CriterionModifierNotEquals { - assert.NotEqual(criterion.Value, value) + return assert.NotEqual(criterion.Value, value) } if criterion.Modifier == models.CriterionModifierGreaterThan { - assert.Greater(value, criterion.Value) + return assert.Greater(value, criterion.Value) } if criterion.Modifier == models.CriterionModifierLessThan { - assert.Less(value, criterion.Value) + return assert.Less(value, criterion.Value) } + + return true } func TestSceneQueryDuration(t *testing.T) { @@ -4088,53 +4090,7 @@ func TestSceneUpdateSceneCover(t *testing.T) { sceneID := sceneIDs[sceneIdxWithGallery] - image := []byte("image") - if err := qb.UpdateCover(ctx, sceneID, image); err != nil { - return fmt.Errorf("Error updating scene cover: %s", err.Error()) - } - - // ensure image set - storedImage, err := qb.GetCover(ctx, sceneID) - if err != nil { - return fmt.Errorf("Error getting image: %s", err.Error()) - } - assert.Equal(t, storedImage, image) - - // set nil image - err = qb.UpdateCover(ctx, sceneID, nil) - if err == nil { - return fmt.Errorf("Expected error setting nil image") - } - - return nil - }); err != nil { - t.Error(err.Error()) - } -} - -func TestSceneDestroySceneCover(t *testing.T) { - if err := withTxn(func(ctx context.Context) error { - qb := db.Scene - - sceneID := sceneIDs[sceneIdxWithGallery] - - image := []byte("image") - if err := qb.UpdateCover(ctx, sceneID, image); err != nil { - return fmt.Errorf("Error updating scene image: %s", err.Error()) - } - - if err := qb.DestroyCover(ctx, sceneID); err != nil { - return fmt.Errorf("Error destroying scene cover: %s", err.Error()) - } - - // image should be nil - storedImage, err := qb.GetCover(ctx, sceneID) - if err != nil { - return fmt.Errorf("Error getting image: %s", err.Error()) - } - assert.Nil(t, storedImage) - - return nil + return testUpdateImage(t, ctx, sceneID, qb.UpdateCover, qb.GetCover) }); err != nil { t.Error(err.Error()) } diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index 75d3360d0..4300111cf 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -17,7 +17,6 @@ import ( "github.com/stashapp/stash/pkg/file" "github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil/intslice" "github.com/stashapp/stash/pkg/sqlite" "github.com/stashapp/stash/pkg/txn" @@ -147,6 +146,7 @@ const ( const ( galleryIdxWithScene = iota + galleryIdxWithChapters galleryIdxWithImage galleryIdx1WithImage galleryIdx2WithImage @@ -237,6 +237,11 @@ const ( totalMarkers ) +const ( + chapterIdxWithGallery = iota + totalChapters +) + const ( savedFilterIdxDefaultScene = iota savedFilterIdxDefaultImage @@ -262,6 +267,7 @@ var ( sceneFileIDs []file.ID imageFileIDs []file.ID galleryFileIDs []file.ID + chapterIDs []int sceneIDs []int imageIDs []int @@ -373,6 +379,19 @@ var ( } ) +type chapterSpec struct { + galleryIdx int + title string + imageIndex int +} + +var ( + // indexed by chapter + chapterSpecs = []chapterSpec{ + {galleryIdxWithChapters, "Test1", 10}, + } +) + var ( imageGalleries = linkMap{ imageIdxWithGallery: {galleryIdxWithImage}, @@ -442,10 +461,9 @@ var ( ) var ( - performerTagLinks = [][2]int{ - {performerIdxWithTag, tagIdxWithPerformer}, - {performerIdxWithTwoTags, tagIdx1WithPerformer}, - {performerIdxWithTwoTags, tagIdx2WithPerformer}, + performerTags = linkMap{ + performerIdxWithTag: {tagIdxWithPerformer}, + performerIdxWithTwoTags: {tagIdx1WithPerformer, tagIdx2WithPerformer}, } ) @@ -519,6 +537,10 @@ func runTests(m *testing.M) int { f.Close() databaseFile := f.Name() db = sqlite.NewDatabase() + db.SetBlobStoreOptions(sqlite.BlobStoreOptions{ + UseDatabase: true, + // don't use filesystem + }) if err := db.Open(databaseFile); err != nil { panic(fmt.Sprintf("Could not initialize database: %s", err.Error())) @@ -548,19 +570,19 @@ func populateDB() error { // TODO - link folders to zip files - if err := createMovies(ctx, sqlite.MovieReaderWriter, moviesNameCase, moviesNameNoCase); err != nil { + if err := createMovies(ctx, db.Movie, moviesNameCase, moviesNameNoCase); err != nil { return fmt.Errorf("error creating movies: %s", err.Error()) } + if err := createTags(ctx, db.Tag, tagsNameCase, tagsNameNoCase); err != nil { + return fmt.Errorf("error creating tags: %s", err.Error()) + } + if err := createPerformers(ctx, performersNameCase, performersNameNoCase); err != nil { return fmt.Errorf("error creating performers: %s", err.Error()) } - if err := createTags(ctx, sqlite.TagReaderWriter, tagsNameCase, tagsNameNoCase); err != nil { - return fmt.Errorf("error creating tags: %s", err.Error()) - } - - if err := createStudios(ctx, sqlite.StudioReaderWriter, studiosNameCase, studiosNameNoCase); err != nil { + if err := createStudios(ctx, db.Studio, studiosNameCase, studiosNameNoCase); err != nil { return fmt.Errorf("error creating studios: %s", err.Error()) } @@ -576,7 +598,7 @@ func populateDB() error { return fmt.Errorf("error creating images: %s", err.Error()) } - if err := addTagImage(ctx, sqlite.TagReaderWriter, tagIdxWithCoverImage); err != nil { + if err := addTagImage(ctx, db.Tag, tagIdxWithCoverImage); err != nil { return fmt.Errorf("error adding tag image: %s", err.Error()) } @@ -584,19 +606,15 @@ func populateDB() error { return fmt.Errorf("error creating saved filters: %s", err.Error()) } - if err := linkPerformerTags(ctx); err != nil { - return fmt.Errorf("error linking performer tags: %s", err.Error()) - } - - if err := linkMovieStudios(ctx, sqlite.MovieReaderWriter); err != nil { + if err := linkMovieStudios(ctx, db.Movie); err != nil { return fmt.Errorf("error linking movie studios: %s", err.Error()) } - if err := linkStudiosParent(ctx, sqlite.StudioReaderWriter); err != nil { + if err := linkStudiosParent(ctx, db.Studio); err != nil { return fmt.Errorf("error linking studios parent: %s", err.Error()) } - if err := linkTagsParent(ctx, sqlite.TagReaderWriter); err != nil { + if err := linkTagsParent(ctx, db.Tag); err != nil { return fmt.Errorf("error linking tags parent: %s", err.Error()) } @@ -605,6 +623,11 @@ func populateDB() error { return fmt.Errorf("error creating scene marker: %s", err.Error()) } } + for _, cs := range chapterSpecs { + if err := createChapter(ctx, sqlite.GalleryChapterReaderWriter, cs); err != nil { + return fmt.Errorf("error creating gallery chapter: %s", err.Error()) + } + } return nil }); err != nil { @@ -891,9 +914,9 @@ func getObjectDate(index int) models.SQLiteDate { } } -func getObjectDateObject(index int) *models.Date { +func getObjectDateObject(index int, fromDB bool) *models.Date { d := getObjectDate(index) - if !d.Valid { + if !d.Valid || (fromDB && (d.String == "" || d.String == "0001-01-01")) { return nil } @@ -1004,7 +1027,7 @@ func makeScene(i int) *models.Scene { URL: getSceneEmptyString(i, urlField), Rating: getIntPtr(rating), OCounter: getOCounter(i), - Date: getObjectDateObject(i), + Date: getObjectDateObject(i, false), StudioID: studioID, GalleryIDs: models.NewRelatedIDs(gids), PerformerIDs: models.NewRelatedIDs(pids), @@ -1069,7 +1092,7 @@ func makeImageFile(i int) *file.ImageFile { } } -func makeImage(i int) *models.Image { +func makeImage(i int, fromDB bool) *models.Image { title := getImageStringValue(i, titleField) var studioID *int if _, ok := imageStudios[i]; ok { @@ -1084,6 +1107,8 @@ func makeImage(i int) *models.Image { return &models.Image{ Title: title, Rating: getIntPtr(getRating(i)), + Date: getObjectDateObject(i, fromDB), + URL: getImageStringValue(i, urlField), OCounter: getOCounter(i), StudioID: studioID, GalleryIDs: models.NewRelatedIDs(gids), @@ -1107,7 +1132,7 @@ func createImages(ctx context.Context, n int) error { } imageFileIDs = append(imageFileIDs, f.ID) - image := makeImage(i) + image := makeImage(i, false) err := qb.Create(ctx, &models.ImageCreateInput{ Image: image, @@ -1168,7 +1193,7 @@ func makeGallery(i int, includeScenes bool) *models.Gallery { Title: getGalleryStringValue(i, titleField), URL: getGalleryNullStringValue(i, urlField).String, Rating: getIntPtr(getRating(i)), - Date: getObjectDateObject(i), + Date: getObjectDateObject(i, false), StudioID: studioID, PerformerIDs: models.NewRelatedIDs(pids), TagIDs: models.NewRelatedIDs(tids), @@ -1335,17 +1360,21 @@ func createPerformers(ctx context.Context, n int, o int) error { } // so count backwards to 0 as needed // performers [ i ] and [ n + o - i - 1 ] should have similar names with only the Name!=NaMe part different + tids := indexesToIDs(tagIDs, performerTags[i]) + performer := models.Performer{ - Name: getPerformerStringValue(index, name), - Checksum: getPerformerStringValue(i, checksumField), - URL: getPerformerNullStringValue(i, urlField), - Favorite: getPerformerBoolValue(i), - Birthdate: getPerformerBirthdate(i), - DeathDate: getPerformerDeathDate(i), - Details: getPerformerStringValue(i, "Details"), - Ethnicity: getPerformerStringValue(i, "Ethnicity"), - Rating: getIntPtr(getRating(i)), - IgnoreAutoTag: getIgnoreAutoTag(i), + Name: getPerformerStringValue(index, name), + Disambiguation: getPerformerStringValue(index, "disambiguation"), + Aliases: models.NewRelatedStrings([]string{getPerformerStringValue(index, "alias")}), + URL: getPerformerNullStringValue(i, urlField), + Favorite: getPerformerBoolValue(i), + Birthdate: getPerformerBirthdate(i), + DeathDate: getPerformerDeathDate(i), + Details: getPerformerStringValue(i, "Details"), + Ethnicity: getPerformerStringValue(i, "Ethnicity"), + Rating: getIntPtr(getRating(i)), + IgnoreAutoTag: getIgnoreAutoTag(i), + TagIDs: models.NewRelatedIDs(tids), } careerLength := getPerformerCareerLength(i) @@ -1353,20 +1382,18 @@ func createPerformers(ctx context.Context, n int, o int) error { performer.CareerLength = *careerLength } + if (index+1)%5 != 0 { + performer.StashIDs = models.NewRelatedStashIDs([]models.StashID{ + performerStashID(i), + }) + } + err := pqb.Create(ctx, &performer) if err != nil { return fmt.Errorf("Error creating performer %v+: %s", performer, err.Error()) } - if (index+1)%5 != 0 { - if err := pqb.UpdateStashIDs(ctx, performer.ID, []models.StashID{ - performerStashID(i), - }); err != nil { - return fmt.Errorf("setting performer stash ids: %w", err) - } - } - performerIDs = append(performerIDs, performer.ID) performerNames = append(performerNames, performer.Name) } @@ -1582,6 +1609,24 @@ func createMarker(ctx context.Context, mqb models.SceneMarkerReaderWriter, marke return nil } +func createChapter(ctx context.Context, mqb models.GalleryChapterReaderWriter, chapterSpec chapterSpec) error { + chapter := models.GalleryChapter{ + GalleryID: sql.NullInt64{Int64: int64(sceneIDs[chapterSpec.galleryIdx]), Valid: true}, + Title: chapterSpec.title, + ImageIndex: chapterSpec.imageIndex, + } + + created, err := mqb.Create(ctx, chapter) + + if err != nil { + return fmt.Errorf("error creating chapter %v+: %w", chapter, err) + } + + chapterIDs = append(chapterIDs, created.ID) + + return nil +} + func getSavedFilterMode(index int) models.FilterMode { switch index { case savedFilterIdxScene, savedFilterIdxDefaultScene: @@ -1637,22 +1682,6 @@ func doLinks(links [][2]int, fn func(idx1, idx2 int) error) error { return nil } -func linkPerformerTags(ctx context.Context) error { - qb := db.Performer - return doLinks(performerTagLinks, func(performerIndex, tagIndex int) error { - performerID := performerIDs[performerIndex] - tagID := tagIDs[tagIndex] - tagIDs, err := qb.GetTagIDs(ctx, performerID) - if err != nil { - return err - } - - tagIDs = intslice.IntAppendUnique(tagIDs, tagID) - - return qb.UpdateTags(ctx, performerID, tagIDs) - }) -} - func linkMovieStudios(ctx context.Context, mqb models.MovieWriter) error { return doLinks(movieStudioLinks, func(movieIndex, studioIndex int) error { movie := models.MoviePartial{ diff --git a/pkg/sqlite/sql.go b/pkg/sqlite/sql.go index 8611f42fa..af864df01 100644 --- a/pkg/sqlite/sql.go +++ b/pkg/sqlite/sql.go @@ -1,9 +1,6 @@ package sqlite import ( - "context" - "database/sql" - "errors" "fmt" "math/rand" "regexp" @@ -197,9 +194,9 @@ func getDateWhereClause(column string, modifier models.CriterionModifier, value switch modifier { case models.CriterionModifierIsNull: - return fmt.Sprintf("(%s IS NULL OR %s = '')", column, column), nil + return fmt.Sprintf("(%s IS NULL OR %s = '' OR %s = '0001-01-01')", column, column, column), nil case models.CriterionModifierNotNull: - return fmt.Sprintf("(%s IS NOT NULL AND %s != '')", column, column), nil + return fmt.Sprintf("(%s IS NOT NULL AND %s != '' AND %s != '0001-01-01')", column, column, column), nil case models.CriterionModifierEquals: return fmt.Sprintf("%s = ?", column), args case models.CriterionModifierNotEquals: @@ -290,28 +287,6 @@ func getCountCriterionClause(primaryTable, joinTable, primaryFK string, criterio return getIntCriterionWhereClause(lhs, criterion) } -func getImage(ctx context.Context, tx dbWrapper, query string, args ...interface{}) ([]byte, error) { - rows, err := tx.Queryx(ctx, query, args...) - - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return nil, err - } - defer rows.Close() - - var ret []byte - if rows.Next() { - if err := rows.Scan(&ret); err != nil { - return nil, err - } - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return ret, nil -} - func coalesce(column string) string { return fmt.Sprintf("COALESCE(%s, '')", column) } diff --git a/pkg/sqlite/studio.go b/pkg/sqlite/studio.go index 8509b74d7..b8f783de1 100644 --- a/pkg/sqlite/studio.go +++ b/pkg/sqlite/studio.go @@ -13,20 +13,31 @@ import ( "github.com/stashapp/stash/pkg/sliceutil/intslice" ) -const studioTable = "studios" -const studioIDColumn = "studio_id" -const studioAliasesTable = "studio_aliases" -const studioAliasColumn = "alias" +const ( + studioTable = "studios" + studioIDColumn = "studio_id" + studioAliasesTable = "studio_aliases" + studioAliasColumn = "alias" + + studioImageBlobColumn = "image_blob" +) type studioQueryBuilder struct { repository + blobJoinQueryBuilder } -var StudioReaderWriter = &studioQueryBuilder{ - repository{ - tableName: studioTable, - idColumn: idColumn, - }, +func NewStudioReaderWriter(blobStore *BlobStore) *studioQueryBuilder { + return &studioQueryBuilder{ + repository{ + tableName: studioTable, + idColumn: idColumn, + }, + blobJoinQueryBuilder{ + blobStore: blobStore, + joinTable: studioTable, + }, + } } func (qb *studioQueryBuilder) Create(ctx context.Context, newObject models.Studio) (*models.Studio, error) { @@ -57,6 +68,11 @@ func (qb *studioQueryBuilder) UpdateFull(ctx context.Context, updatedObject mode } func (qb *studioQueryBuilder) Destroy(ctx context.Context, id int) error { + // must handle image checksums manually + if err := qb.destroyImage(ctx, id); err != nil { + return err + } + // TODO - set null on foreign key in scraped items // remove studio from scraped items _, err := qb.tx.Exec(ctx, "UPDATE scraped_items SET studio_id = null WHERE studio_id = ?", id) @@ -80,17 +96,23 @@ func (qb *studioQueryBuilder) Find(ctx context.Context, id int) (*models.Studio, func (qb *studioQueryBuilder) FindMany(ctx context.Context, ids []int) ([]*models.Studio, error) { tableMgr := studioTableMgr - q := goqu.Select("*").From(tableMgr.table).Where(tableMgr.byIDInts(ids...)) - unsorted, err := qb.getMany(ctx, q) - if err != nil { - return nil, err - } - ret := make([]*models.Studio, len(ids)) - for _, s := range unsorted { - i := intslice.IntIndex(ids, s.ID) - ret[i] = s + if err := batchExec(ids, defaultBatchSize, func(batch []int) error { + q := goqu.Select("*").From(tableMgr.table).Where(tableMgr.byIDInts(batch...)) + unsorted, err := qb.getMany(ctx, q) + if err != nil { + return err + } + + for _, s := range unsorted { + i := intslice.IntIndex(ids, s.ID) + ret[i] = s + } + + return nil + }); err != nil { + return nil, err } for i := range ret { @@ -287,7 +309,9 @@ func (qb *studioQueryBuilder) Query(ctx context.Context, studioFilter *models.St } filter := qb.makeFilter(ctx, studioFilter) - query.addFilter(filter) + if err := query.addFilter(filter); err != nil { + return nil, 0, err + } query.sortAndPagination = qb.getStudioSort(findFilter) + getPagination(findFilter) idsResult, countResult, err := query.executeFind(ctx) @@ -308,8 +332,7 @@ func studioIsMissingCriterionHandler(qb *studioQueryBuilder, isMissing *string) if isMissing != nil && *isMissing != "" { switch *isMissing { case "image": - f.addLeftJoin("studios_image", "", "studios_image.studio_id = studios.id") - f.addWhere("studios_image.studio_id IS NULL") + f.addWhere("studios.image_blob IS NULL") case "stash_id": qb.stashIDRepository().join(f, "studio_stash_ids", "studios.id") f.addWhere("studio_stash_ids.studio_id IS NULL") @@ -420,31 +443,20 @@ func (qb *studioQueryBuilder) queryStudios(ctx context.Context, query string, ar return []*models.Studio(ret), nil } -func (qb *studioQueryBuilder) imageRepository() *imageRepository { - return &imageRepository{ - repository: repository{ - tx: qb.tx, - tableName: "studios_image", - idColumn: studioIDColumn, - }, - imageColumn: "image", - } -} - func (qb *studioQueryBuilder) GetImage(ctx context.Context, studioID int) ([]byte, error) { - return qb.imageRepository().get(ctx, studioID) + return qb.blobJoinQueryBuilder.GetImage(ctx, studioID, studioImageBlobColumn) } func (qb *studioQueryBuilder) HasImage(ctx context.Context, studioID int) (bool, error) { - return qb.imageRepository().exists(ctx, studioID) + return qb.blobJoinQueryBuilder.HasImage(ctx, studioID, studioImageBlobColumn) } func (qb *studioQueryBuilder) UpdateImage(ctx context.Context, studioID int, image []byte) error { - return qb.imageRepository().replace(ctx, studioID, image) + return qb.blobJoinQueryBuilder.UpdateImage(ctx, studioID, studioImageBlobColumn, image) } -func (qb *studioQueryBuilder) DestroyImage(ctx context.Context, studioID int) error { - return qb.imageRepository().destroy(ctx, []int{studioID}) +func (qb *studioQueryBuilder) destroyImage(ctx context.Context, studioID int) error { + return qb.blobJoinQueryBuilder.DestroyImage(ctx, studioID, studioImageBlobColumn) } func (qb *studioQueryBuilder) stashIDRepository() *stashIDRepository { diff --git a/pkg/sqlite/studio_test.go b/pkg/sqlite/studio_test.go index 4bffe4517..334ad1a15 100644 --- a/pkg/sqlite/studio_test.go +++ b/pkg/sqlite/studio_test.go @@ -14,13 +14,12 @@ import ( "testing" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sqlite" "github.com/stretchr/testify/assert" ) func TestStudioFindByName(t *testing.T) { withTxn(func(ctx context.Context) error { - sqb := sqlite.StudioReaderWriter + sqb := db.Studio name := studioNames[studioIdxWithScene] // find a studio by name @@ -70,7 +69,7 @@ func TestStudioQueryNameOr(t *testing.T) { } withTxn(func(ctx context.Context) error { - sqb := sqlite.StudioReaderWriter + sqb := db.Studio studios := queryStudio(ctx, t, sqb, &studioFilter, nil) @@ -101,7 +100,7 @@ func TestStudioQueryNameAndUrl(t *testing.T) { } withTxn(func(ctx context.Context) error { - sqb := sqlite.StudioReaderWriter + sqb := db.Studio studios := queryStudio(ctx, t, sqb, &studioFilter, nil) @@ -136,7 +135,7 @@ func TestStudioQueryNameNotUrl(t *testing.T) { } withTxn(func(ctx context.Context) error { - sqb := sqlite.StudioReaderWriter + sqb := db.Studio studios := queryStudio(ctx, t, sqb, &studioFilter, nil) @@ -167,7 +166,7 @@ func TestStudioIllegalQuery(t *testing.T) { } withTxn(func(ctx context.Context) error { - sqb := sqlite.StudioReaderWriter + sqb := db.Studio _, _, err := sqb.Query(ctx, studioFilter, nil) assert.NotNil(err) @@ -193,7 +192,7 @@ func TestStudioQueryIgnoreAutoTag(t *testing.T) { IgnoreAutoTag: &ignoreAutoTag, } - sqb := sqlite.StudioReaderWriter + sqb := db.Studio studios := queryStudio(ctx, t, sqb, &studioFilter, nil) @@ -208,7 +207,7 @@ func TestStudioQueryIgnoreAutoTag(t *testing.T) { func TestStudioQueryForAutoTag(t *testing.T) { withTxn(func(ctx context.Context) error { - tqb := sqlite.StudioReaderWriter + tqb := db.Studio name := studioNames[studioIdxWithMovie] // find a studio by name @@ -239,7 +238,7 @@ func TestStudioQueryForAutoTag(t *testing.T) { func TestStudioQueryParent(t *testing.T) { withTxn(func(ctx context.Context) error { - sqb := sqlite.StudioReaderWriter + sqb := db.Studio studioCriterion := models.MultiCriterionInput{ Value: []string{ strconv.Itoa(studioIDs[studioIdxWithChildStudio]), @@ -289,18 +288,18 @@ func TestStudioDestroyParent(t *testing.T) { // create parent and child studios if err := withTxn(func(ctx context.Context) error { - createdParent, err := createStudio(ctx, sqlite.StudioReaderWriter, parentName, nil) + createdParent, err := createStudio(ctx, db.Studio, parentName, nil) if err != nil { return fmt.Errorf("Error creating parent studio: %s", err.Error()) } parentID := int64(createdParent.ID) - createdChild, err := createStudio(ctx, sqlite.StudioReaderWriter, childName, &parentID) + createdChild, err := createStudio(ctx, db.Studio, childName, &parentID) if err != nil { return fmt.Errorf("Error creating child studio: %s", err.Error()) } - sqb := sqlite.StudioReaderWriter + sqb := db.Studio // destroy the parent err = sqb.Destroy(ctx, createdParent.ID) @@ -322,7 +321,7 @@ func TestStudioDestroyParent(t *testing.T) { func TestStudioFindChildren(t *testing.T) { withTxn(func(ctx context.Context) error { - sqb := sqlite.StudioReaderWriter + sqb := db.Studio studios, err := sqb.FindChildren(ctx, studioIDs[studioIdxWithChildStudio]) @@ -351,18 +350,18 @@ func TestStudioUpdateClearParent(t *testing.T) { // create parent and child studios if err := withTxn(func(ctx context.Context) error { - createdParent, err := createStudio(ctx, sqlite.StudioReaderWriter, parentName, nil) + createdParent, err := createStudio(ctx, db.Studio, parentName, nil) if err != nil { return fmt.Errorf("Error creating parent studio: %s", err.Error()) } parentID := int64(createdParent.ID) - createdChild, err := createStudio(ctx, sqlite.StudioReaderWriter, childName, &parentID) + createdChild, err := createStudio(ctx, db.Studio, childName, &parentID) if err != nil { return fmt.Errorf("Error creating child studio: %s", err.Error()) } - sqb := sqlite.StudioReaderWriter + sqb := db.Studio // clear the parent id from the child updatePartial := models.StudioPartial{ @@ -388,70 +387,16 @@ func TestStudioUpdateClearParent(t *testing.T) { func TestStudioUpdateStudioImage(t *testing.T) { if err := withTxn(func(ctx context.Context) error { - qb := sqlite.StudioReaderWriter + qb := db.Studio - // create performer to test against + // create studio to test against const name = "TestStudioUpdateStudioImage" - created, err := createStudio(ctx, sqlite.StudioReaderWriter, name, nil) + created, err := createStudio(ctx, db.Studio, name, nil) if err != nil { return fmt.Errorf("Error creating studio: %s", err.Error()) } - image := []byte("image") - err = qb.UpdateImage(ctx, created.ID, image) - if err != nil { - return fmt.Errorf("Error updating studio image: %s", err.Error()) - } - - // ensure image set - storedImage, err := qb.GetImage(ctx, created.ID) - if err != nil { - return fmt.Errorf("Error getting image: %s", err.Error()) - } - assert.Equal(t, storedImage, image) - - // set nil image - err = qb.UpdateImage(ctx, created.ID, nil) - if err == nil { - return fmt.Errorf("Expected error setting nil image") - } - - return nil - }); err != nil { - t.Error(err.Error()) - } -} - -func TestStudioDestroyStudioImage(t *testing.T) { - if err := withTxn(func(ctx context.Context) error { - qb := sqlite.StudioReaderWriter - - // create performer to test against - const name = "TestStudioDestroyStudioImage" - created, err := createStudio(ctx, sqlite.StudioReaderWriter, name, nil) - if err != nil { - return fmt.Errorf("Error creating studio: %s", err.Error()) - } - - image := []byte("image") - err = qb.UpdateImage(ctx, created.ID, image) - if err != nil { - return fmt.Errorf("Error updating studio image: %s", err.Error()) - } - - err = qb.DestroyImage(ctx, created.ID) - if err != nil { - return fmt.Errorf("Error destroying studio image: %s", err.Error()) - } - - // image should be nil - storedImage, err := qb.GetImage(ctx, created.ID) - if err != nil { - return fmt.Errorf("Error getting image: %s", err.Error()) - } - assert.Nil(t, storedImage) - - return nil + return testUpdateImage(t, ctx, created.ID, qb.UpdateImage, qb.GetImage) }); err != nil { t.Error(err.Error()) } @@ -478,7 +423,7 @@ func TestStudioQuerySceneCount(t *testing.T) { func verifyStudiosSceneCount(t *testing.T, sceneCountCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { - sqb := sqlite.StudioReaderWriter + sqb := db.Studio studioFilter := models.StudioFilterType{ SceneCount: &sceneCountCriterion, } @@ -519,7 +464,7 @@ func TestStudioQueryImageCount(t *testing.T) { func verifyStudiosImageCount(t *testing.T, imageCountCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { - sqb := sqlite.StudioReaderWriter + sqb := db.Studio studioFilter := models.StudioFilterType{ ImageCount: &imageCountCriterion, } @@ -575,7 +520,7 @@ func TestStudioQueryGalleryCount(t *testing.T) { func verifyStudiosGalleryCount(t *testing.T, galleryCountCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { - sqb := sqlite.StudioReaderWriter + sqb := db.Studio studioFilter := models.StudioFilterType{ GalleryCount: &galleryCountCriterion, } @@ -606,11 +551,11 @@ func verifyStudiosGalleryCount(t *testing.T, galleryCountCriterion models.IntCri func TestStudioStashIDs(t *testing.T) { if err := withTxn(func(ctx context.Context) error { - qb := sqlite.StudioReaderWriter + qb := db.Studio // create studio to test against const name = "TestStudioStashIDs" - created, err := createStudio(ctx, sqlite.StudioReaderWriter, name, nil) + created, err := createStudio(ctx, db.Studio, name, nil) if err != nil { return fmt.Errorf("Error creating studio: %s", err.Error()) } @@ -688,7 +633,7 @@ func TestStudioQueryRating(t *testing.T) { func verifyStudioQuery(t *testing.T, filter models.StudioFilterType, verifyFn func(ctx context.Context, s *models.Studio)) { withTxn(func(ctx context.Context) error { t.Helper() - sqb := sqlite.StudioReaderWriter + sqb := db.Studio studios := queryStudio(ctx, t, sqb, &filter, nil) @@ -705,7 +650,7 @@ func verifyStudioQuery(t *testing.T, filter models.StudioFilterType, verifyFn fu func verifyStudiosRating(t *testing.T, ratingCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { - sqb := sqlite.StudioReaderWriter + sqb := db.Studio studioFilter := models.StudioFilterType{ Rating: &ratingCriterion, } @@ -726,7 +671,7 @@ func verifyStudiosRating(t *testing.T, ratingCriterion models.IntCriterionInput) func TestStudioQueryIsMissingRating(t *testing.T) { withTxn(func(ctx context.Context) error { - sqb := sqlite.StudioReaderWriter + sqb := db.Studio isMissing := "rating" studioFilter := models.StudioFilterType{ IsMissing: &isMissing, @@ -802,7 +747,7 @@ func TestStudioQueryAlias(t *testing.T) { verifyFn := func(ctx context.Context, studio *models.Studio) { t.Helper() - aliases, err := sqlite.StudioReaderWriter.GetAliases(ctx, studio.ID) + aliases, err := db.Studio.GetAliases(ctx, studio.ID) if err != nil { t.Errorf("Error querying studios: %s", err.Error()) } @@ -837,7 +782,7 @@ func TestStudioQueryAlias(t *testing.T) { func TestStudioUpdateAlias(t *testing.T) { if err := withTxn(func(ctx context.Context) error { - qb := sqlite.StudioReaderWriter + qb := db.Studio // create studio to test against const name = "TestStudioUpdateAlias" @@ -934,7 +879,7 @@ func TestStudioQueryFast(t *testing.T) { } withTxn(func(ctx context.Context) error { - sqb := sqlite.StudioReaderWriter + sqb := db.Studio for _, f := range filters { for _, ff := range findFilters { _, _, err := sqb.Query(ctx, &f, &ff) diff --git a/pkg/sqlite/table.go b/pkg/sqlite/table.go index 31cdefcf9..1a33ee2bf 100644 --- a/pkg/sqlite/table.go +++ b/pkg/sqlite/table.go @@ -15,6 +15,7 @@ import ( "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/sliceutil/intslice" + "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) type table struct { @@ -129,6 +130,15 @@ func (t *table) destroy(ctx context.Context, ids []int) error { return nil } +func (t *table) join(j joiner, as string, parentIDCol string) { + tableName := t.table.GetTable() + tt := tableName + if as != "" { + tt = as + } + j.addLeftJoin(tableName, as, fmt.Sprintf("%s.%s = %s", tt, t.idColumn.GetCol(), parentIDCol)) +} + // func (t *table) get(ctx context.Context, q *goqu.SelectDataset, dest interface{}) error { // tx, err := getTx(ctx) // if err != nil { @@ -258,18 +268,18 @@ type stashIDRow struct { Endpoint null.String `db:"endpoint"` } -func (r *stashIDRow) resolve() *models.StashID { - return &models.StashID{ +func (r *stashIDRow) resolve() models.StashID { + return models.StashID{ StashID: r.StashID.String, Endpoint: r.Endpoint.String, } } -func (t *stashIDTable) get(ctx context.Context, id int) ([]*models.StashID, error) { +func (t *stashIDTable) get(ctx context.Context, id int) ([]models.StashID, error) { q := dialect.Select("endpoint", "stash_id").From(t.table.table).Where(t.idColumn.Eq(id)) const single = false - var ret []*models.StashID + var ret []models.StashID if err := queryFunc(ctx, q, single, func(rows *sqlx.Rows) error { var v stashIDRow if err := rows.StructScan(&v); err != nil { @@ -366,6 +376,102 @@ func (t *stashIDTable) modifyJoins(ctx context.Context, id int, v []models.Stash return nil } +type stringTable struct { + table + stringColumn exp.IdentifierExpression +} + +func (t *stringTable) get(ctx context.Context, id int) ([]string, error) { + q := dialect.Select(t.stringColumn).From(t.table.table).Where(t.idColumn.Eq(id)) + + const single = false + var ret []string + if err := queryFunc(ctx, q, single, func(rows *sqlx.Rows) error { + var v string + if err := rows.Scan(&v); err != nil { + return err + } + + ret = append(ret, v) + + return nil + }); err != nil { + return nil, fmt.Errorf("getting stash ids from %s: %w", t.table.table.GetTable(), err) + } + + return ret, nil +} + +func (t *stringTable) insertJoin(ctx context.Context, id int, v string) (sql.Result, error) { + q := dialect.Insert(t.table.table).Cols(t.idColumn.GetCol(), t.stringColumn.GetCol()).Vals( + goqu.Vals{id, v}, + ) + ret, err := exec(ctx, q) + if err != nil { + return nil, fmt.Errorf("inserting into %s: %w", t.table.table.GetTable(), err) + } + + return ret, nil +} + +func (t *stringTable) insertJoins(ctx context.Context, id int, v []string) error { + for _, fk := range v { + if _, err := t.insertJoin(ctx, id, fk); err != nil { + return err + } + } + + return nil +} + +func (t *stringTable) replaceJoins(ctx context.Context, id int, v []string) error { + if err := t.destroy(ctx, []int{id}); err != nil { + return err + } + + return t.insertJoins(ctx, id, v) +} + +func (t *stringTable) addJoins(ctx context.Context, id int, v []string) error { + // get existing foreign keys + existing, err := t.get(ctx, id) + if err != nil { + return err + } + + // only add values that are not already present + filtered := stringslice.StrExclude(v, existing) + return t.insertJoins(ctx, id, filtered) +} + +func (t *stringTable) destroyJoins(ctx context.Context, id int, v []string) error { + for _, vv := range v { + q := dialect.Delete(t.table.table).Where( + t.idColumn.Eq(id), + t.stringColumn.Eq(vv), + ) + + if _, err := exec(ctx, q); err != nil { + return fmt.Errorf("destroying %s: %w", t.table.table.GetTable(), err) + } + } + + return nil +} + +func (t *stringTable) modifyJoins(ctx context.Context, id int, v []string, mode models.RelationshipUpdateMode) error { + switch mode { + case models.RelationshipUpdateModeSet: + return t.replaceJoins(ctx, id, v) + case models.RelationshipUpdateModeAdd: + return t.addJoins(ctx, id, v) + case models.RelationshipUpdateModeRemove: + return t.destroyJoins(ctx, id, v) + } + + return nil +} + type scenesMoviesTable struct { table } diff --git a/pkg/sqlite/tables.go b/pkg/sqlite/tables.go index 99301d046..2bf1bfd16 100644 --- a/pkg/sqlite/tables.go +++ b/pkg/sqlite/tables.go @@ -25,6 +25,7 @@ var ( scenesStashIDsJoinTable = goqu.T("scene_stash_ids") scenesMoviesJoinTable = goqu.T(moviesScenesTable) + performersAliasesJoinTable = goqu.T(performersAliasesTable) performersTagsJoinTable = goqu.T(performersTagsTable) performersStashIDsJoinTable = goqu.T("performer_stash_ids") ) @@ -111,6 +112,11 @@ var ( idColumn: goqu.T(sceneTable).Col(idColumn), } + sceneMarkerTableMgr = &table{ + table: goqu.T(sceneMarkerTable), + idColumn: goqu.T(sceneMarkerTable).Col(idColumn), + } + scenesFilesTableMgr = &relatedFilesTable{ table: table{ table: scenesFilesJoinTable, @@ -183,6 +189,29 @@ var ( table: goqu.T(performerTable), idColumn: goqu.T(performerTable).Col(idColumn), } + + performersAliasesTableMgr = &stringTable{ + table: table{ + table: performersAliasesJoinTable, + idColumn: performersAliasesJoinTable.Col(performerIDColumn), + }, + stringColumn: performersAliasesJoinTable.Col(performerAliasColumn), + } + + performersTagsTableMgr = &joinTable{ + table: table{ + table: performersTagsJoinTable, + idColumn: performersTagsJoinTable.Col(performerIDColumn), + }, + fkColumn: performersTagsJoinTable.Col(tagIDColumn), + } + + performersStashIDsTableMgr = &stashIDTable{ + table: table{ + table: performersStashIDsJoinTable, + idColumn: performersStashIDsJoinTable.Col(performerIDColumn), + }, + } ) var ( @@ -205,3 +234,10 @@ var ( idColumn: goqu.T(movieTable).Col(idColumn), } ) + +var ( + blobTableMgr = &table{ + table: goqu.T(blobTable), + idColumn: goqu.T(blobTable).Col(blobChecksumColumn), + } +) diff --git a/pkg/sqlite/tag.go b/pkg/sqlite/tag.go index 9521e8a79..9ad4abcaf 100644 --- a/pkg/sqlite/tag.go +++ b/pkg/sqlite/tag.go @@ -13,20 +13,31 @@ import ( "github.com/stashapp/stash/pkg/sliceutil/intslice" ) -const tagTable = "tags" -const tagIDColumn = "tag_id" -const tagAliasesTable = "tag_aliases" -const tagAliasColumn = "alias" +const ( + tagTable = "tags" + tagIDColumn = "tag_id" + tagAliasesTable = "tag_aliases" + tagAliasColumn = "alias" + + tagImageBlobColumn = "image_blob" +) type tagQueryBuilder struct { repository + blobJoinQueryBuilder } -var TagReaderWriter = &tagQueryBuilder{ - repository{ - tableName: tagTable, - idColumn: idColumn, - }, +func NewTagReaderWriter(blobStore *BlobStore) *tagQueryBuilder { + return &tagQueryBuilder{ + repository{ + tableName: tagTable, + idColumn: idColumn, + }, + blobJoinQueryBuilder{ + blobStore: blobStore, + joinTable: tagTable, + }, + } } func (qb *tagQueryBuilder) Create(ctx context.Context, newObject models.Tag) (*models.Tag, error) { @@ -57,16 +68,8 @@ func (qb *tagQueryBuilder) UpdateFull(ctx context.Context, updatedObject models. } func (qb *tagQueryBuilder) Destroy(ctx context.Context, id int) error { - // TODO - add delete cascade to foreign key - // delete tag from scenes and markers first - _, err := qb.tx.Exec(ctx, "DELETE FROM scenes_tags WHERE tag_id = ?", id) - if err != nil { - return err - } - - // TODO - add delete cascade to foreign key - _, err = qb.tx.Exec(ctx, "DELETE FROM scene_markers_tags WHERE tag_id = ?", id) - if err != nil { + // must handle image checksums manually + if err := qb.destroyImage(ctx, id); err != nil { return err } @@ -98,17 +101,23 @@ func (qb *tagQueryBuilder) Find(ctx context.Context, id int) (*models.Tag, error func (qb *tagQueryBuilder) FindMany(ctx context.Context, ids []int) ([]*models.Tag, error) { tableMgr := tagTableMgr - q := goqu.Select("*").From(tableMgr.table).Where(tableMgr.byIDInts(ids...)) - unsorted, err := qb.getMany(ctx, q) - if err != nil { - return nil, err - } - ret := make([]*models.Tag, len(ids)) - for _, s := range unsorted { - i := intslice.IntIndex(ids, s.ID) - ret[i] = s + if err := batchExec(ids, defaultBatchSize, func(batch []int) error { + q := goqu.Select("*").From(tableMgr.table).Where(tableMgr.byIDInts(batch...)) + unsorted, err := qb.getMany(ctx, q) + if err != nil { + return err + } + + for _, s := range unsorted { + i := intslice.IntIndex(ids, s.ID) + ret[i] = s + } + + return nil + }); err != nil { + return nil, err } for i := range ret { @@ -366,7 +375,9 @@ func (qb *tagQueryBuilder) Query(ctx context.Context, tagFilter *models.TagFilte } filter := qb.makeFilter(ctx, tagFilter) - query.addFilter(filter) + if err := query.addFilter(filter); err != nil { + return nil, 0, err + } query.sortAndPagination = qb.getTagSort(&query, findFilter) + getPagination(findFilter) idsResult, countResult, err := query.executeFind(ctx) @@ -399,8 +410,7 @@ func tagIsMissingCriterionHandler(qb *tagQueryBuilder, isMissing *string) criter if isMissing != nil && *isMissing != "" { switch *isMissing { case "image": - qb.imageRepository().join(f, "", "tags.id") - f.addWhere("tags_image.tag_id IS NULL") + f.addWhere("tags.image_blob IS NULL") default: f.addWhere("(tags." + *isMissing + " IS NULL OR TRIM(tags." + *isMissing + ") = '')") } @@ -634,31 +644,16 @@ func (qb *tagQueryBuilder) queryTags(ctx context.Context, query string, args []i return []*models.Tag(ret), nil } -func (qb *tagQueryBuilder) imageRepository() *imageRepository { - return &imageRepository{ - repository: repository{ - tx: qb.tx, - tableName: "tags_image", - idColumn: tagIDColumn, - }, - imageColumn: "image", - } -} - func (qb *tagQueryBuilder) GetImage(ctx context.Context, tagID int) ([]byte, error) { - return qb.imageRepository().get(ctx, tagID) -} - -func (qb *tagQueryBuilder) HasImage(ctx context.Context, tagID int) (bool, error) { - return qb.imageRepository().exists(ctx, tagID) + return qb.blobJoinQueryBuilder.GetImage(ctx, tagID, tagImageBlobColumn) } func (qb *tagQueryBuilder) UpdateImage(ctx context.Context, tagID int, image []byte) error { - return qb.imageRepository().replace(ctx, tagID, image) + return qb.blobJoinQueryBuilder.UpdateImage(ctx, tagID, tagImageBlobColumn, image) } -func (qb *tagQueryBuilder) DestroyImage(ctx context.Context, tagID int) error { - return qb.imageRepository().destroy(ctx, []int{tagID}) +func (qb *tagQueryBuilder) destroyImage(ctx context.Context, tagID int) error { + return qb.blobJoinQueryBuilder.DestroyImage(ctx, tagID, tagImageBlobColumn) } func (qb *tagQueryBuilder) aliasRepository() *stringRepository { diff --git a/pkg/sqlite/tag_test.go b/pkg/sqlite/tag_test.go index a351f28f4..d3ff5459f 100644 --- a/pkg/sqlite/tag_test.go +++ b/pkg/sqlite/tag_test.go @@ -19,7 +19,7 @@ import ( func TestMarkerFindBySceneMarkerID(t *testing.T) { withTxn(func(ctx context.Context) error { - tqb := sqlite.TagReaderWriter + tqb := db.Tag markerID := markerIDs[markerIdxWithTag] @@ -46,7 +46,7 @@ func TestMarkerFindBySceneMarkerID(t *testing.T) { func TestTagFindByName(t *testing.T) { withTxn(func(ctx context.Context) error { - tqb := sqlite.TagReaderWriter + tqb := db.Tag name := tagNames[tagIdxWithScene] // find a tag by name @@ -82,7 +82,7 @@ func TestTagQueryIgnoreAutoTag(t *testing.T) { IgnoreAutoTag: &ignoreAutoTag, } - sqb := sqlite.TagReaderWriter + sqb := db.Tag tags := queryTags(ctx, t, sqb, &tagFilter, nil) @@ -97,7 +97,7 @@ func TestTagQueryIgnoreAutoTag(t *testing.T) { func TestTagQueryForAutoTag(t *testing.T) { withTxn(func(ctx context.Context) error { - tqb := sqlite.TagReaderWriter + tqb := db.Tag name := tagNames[tagIdx1WithScene] // find a tag by name @@ -131,7 +131,7 @@ func TestTagFindByNames(t *testing.T) { var names []string withTxn(func(ctx context.Context) error { - tqb := sqlite.TagReaderWriter + tqb := db.Tag names = append(names, tagNames[tagIdxWithScene]) // find tags by names @@ -176,7 +176,7 @@ func TestTagFindByNames(t *testing.T) { func TestTagQuerySort(t *testing.T) { withTxn(func(ctx context.Context) error { - sqb := sqlite.TagReaderWriter + sqb := db.Tag sortBy := "scenes_count" dir := models.SortDirectionEnumDesc @@ -253,7 +253,7 @@ func TestTagQueryAlias(t *testing.T) { } verifyFn := func(ctx context.Context, tag *models.Tag) { - aliases, err := sqlite.TagReaderWriter.GetAliases(ctx, tag.ID) + aliases, err := db.Tag.GetAliases(ctx, tag.ID) if err != nil { t.Errorf("Error querying tags: %s", err.Error()) } @@ -288,7 +288,7 @@ func TestTagQueryAlias(t *testing.T) { func verifyTagQuery(t *testing.T, tagFilter *models.TagFilterType, findFilter *models.FindFilterType, verifyFn func(ctx context.Context, t *models.Tag)) { withTxn(func(ctx context.Context) error { - sqb := sqlite.TagReaderWriter + sqb := db.Tag tags := queryTags(ctx, t, sqb, tagFilter, findFilter) @@ -312,7 +312,7 @@ func queryTags(ctx context.Context, t *testing.T, qb models.TagReader, tagFilter func TestTagQueryIsMissingImage(t *testing.T) { withTxn(func(ctx context.Context) error { - qb := sqlite.TagReaderWriter + qb := db.Tag isMissing := "image" tagFilter := models.TagFilterType{ IsMissing: &isMissing, @@ -366,7 +366,7 @@ func TestTagQuerySceneCount(t *testing.T) { func verifyTagSceneCount(t *testing.T, sceneCountCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { - qb := sqlite.TagReaderWriter + qb := db.Tag tagFilter := models.TagFilterType{ SceneCount: &sceneCountCriterion, } @@ -408,7 +408,7 @@ func TestTagQueryMarkerCount(t *testing.T) { func verifyTagMarkerCount(t *testing.T, markerCountCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { - qb := sqlite.TagReaderWriter + qb := db.Tag tagFilter := models.TagFilterType{ MarkerCount: &markerCountCriterion, } @@ -450,7 +450,7 @@ func TestTagQueryImageCount(t *testing.T) { func verifyTagImageCount(t *testing.T, imageCountCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { - qb := sqlite.TagReaderWriter + qb := db.Tag tagFilter := models.TagFilterType{ ImageCount: &imageCountCriterion, } @@ -492,7 +492,7 @@ func TestTagQueryGalleryCount(t *testing.T) { func verifyTagGalleryCount(t *testing.T, imageCountCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { - qb := sqlite.TagReaderWriter + qb := db.Tag tagFilter := models.TagFilterType{ GalleryCount: &imageCountCriterion, } @@ -534,7 +534,7 @@ func TestTagQueryPerformerCount(t *testing.T) { func verifyTagPerformerCount(t *testing.T, imageCountCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { - qb := sqlite.TagReaderWriter + qb := db.Tag tagFilter := models.TagFilterType{ PerformerCount: &imageCountCriterion, } @@ -576,7 +576,7 @@ func TestTagQueryParentCount(t *testing.T) { func verifyTagParentCount(t *testing.T, sceneCountCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { - qb := sqlite.TagReaderWriter + qb := db.Tag tagFilter := models.TagFilterType{ ParentCount: &sceneCountCriterion, } @@ -619,7 +619,7 @@ func TestTagQueryChildCount(t *testing.T) { func verifyTagChildCount(t *testing.T, sceneCountCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { - qb := sqlite.TagReaderWriter + qb := db.Tag tagFilter := models.TagFilterType{ ChildCount: &sceneCountCriterion, } @@ -644,7 +644,7 @@ func verifyTagChildCount(t *testing.T, sceneCountCriterion models.IntCriterionIn func TestTagQueryParent(t *testing.T) { withTxn(func(ctx context.Context) error { const nameField = "Name" - sqb := sqlite.TagReaderWriter + sqb := db.Tag tagCriterion := models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(tagIDs[tagIdxWithChildTag]), @@ -722,7 +722,7 @@ func TestTagQueryChild(t *testing.T) { withTxn(func(ctx context.Context) error { const nameField = "Name" - sqb := sqlite.TagReaderWriter + sqb := db.Tag tagCriterion := models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(tagIDs[tagIdxWithParentTag]), @@ -798,7 +798,7 @@ func TestTagQueryChild(t *testing.T) { func TestTagUpdateTagImage(t *testing.T) { if err := withTxn(func(ctx context.Context) error { - qb := sqlite.TagReaderWriter + qb := db.Tag // create tag to test against const name = "TestTagUpdateTagImage" @@ -810,64 +810,7 @@ func TestTagUpdateTagImage(t *testing.T) { return fmt.Errorf("Error creating tag: %s", err.Error()) } - image := []byte("image") - err = qb.UpdateImage(ctx, created.ID, image) - if err != nil { - return fmt.Errorf("Error updating studio image: %s", err.Error()) - } - - // ensure image set - storedImage, err := qb.GetImage(ctx, created.ID) - if err != nil { - return fmt.Errorf("Error getting image: %s", err.Error()) - } - assert.Equal(t, storedImage, image) - - // set nil image - err = qb.UpdateImage(ctx, created.ID, nil) - if err == nil { - return fmt.Errorf("Expected error setting nil image") - } - - return nil - }); err != nil { - t.Error(err.Error()) - } -} - -func TestTagDestroyTagImage(t *testing.T) { - if err := withTxn(func(ctx context.Context) error { - qb := sqlite.TagReaderWriter - - // create performer to test against - const name = "TestTagDestroyTagImage" - tag := models.Tag{ - Name: name, - } - created, err := qb.Create(ctx, tag) - if err != nil { - return fmt.Errorf("Error creating tag: %s", err.Error()) - } - - image := []byte("image") - err = qb.UpdateImage(ctx, created.ID, image) - if err != nil { - return fmt.Errorf("Error updating studio image: %s", err.Error()) - } - - err = qb.DestroyImage(ctx, created.ID) - if err != nil { - return fmt.Errorf("Error destroying studio image: %s", err.Error()) - } - - // image should be nil - storedImage, err := qb.GetImage(ctx, created.ID) - if err != nil { - return fmt.Errorf("Error getting image: %s", err.Error()) - } - assert.Nil(t, storedImage) - - return nil + return testUpdateImage(t, ctx, created.ID, qb.UpdateImage, qb.GetImage) }); err != nil { t.Error(err.Error()) } @@ -875,7 +818,7 @@ func TestTagDestroyTagImage(t *testing.T) { func TestTagUpdateAlias(t *testing.T) { if err := withTxn(func(ctx context.Context) error { - qb := sqlite.TagReaderWriter + qb := db.Tag // create tag to test against const name = "TestTagUpdateAlias" @@ -911,7 +854,7 @@ func TestTagMerge(t *testing.T) { // merge tests - perform these in a transaction that we'll rollback if err := withRollbackTxn(func(ctx context.Context) error { - qb := sqlite.TagReaderWriter + qb := db.Tag // try merging into same tag err := qb.Merge(ctx, []int{tagIDs[tagIdx1WithScene]}, tagIDs[tagIdx1WithScene]) diff --git a/pkg/sqlite/transaction.go b/pkg/sqlite/transaction.go index c56b2fcd9..743ccce04 100644 --- a/pkg/sqlite/transaction.go +++ b/pkg/sqlite/transaction.go @@ -125,18 +125,19 @@ func (db *Database) IsLocked(err error) bool { func (db *Database) TxnRepository() models.Repository { return models.Repository{ - TxnManager: db, - File: db.File, - Folder: db.Folder, - Gallery: db.Gallery, - Image: db.Image, - Movie: MovieReaderWriter, - Performer: db.Performer, - Scene: db.Scene, - SceneMarker: SceneMarkerReaderWriter, - ScrapedItem: ScrapedItemReaderWriter, - Studio: StudioReaderWriter, - Tag: TagReaderWriter, - SavedFilter: SavedFilterReaderWriter, + TxnManager: db, + File: db.File, + Folder: db.Folder, + Gallery: db.Gallery, + GalleryChapter: GalleryChapterReaderWriter, + Image: db.Image, + Movie: db.Movie, + Performer: db.Performer, + Scene: db.Scene, + SceneMarker: SceneMarkerReaderWriter, + ScrapedItem: ScrapedItemReaderWriter, + Studio: db.Studio, + Tag: db.Tag, + SavedFilter: SavedFilterReaderWriter, } } diff --git a/pkg/studio/export.go b/pkg/studio/export.go index 27cbaeb38..f0cad2eef 100644 --- a/pkg/studio/export.go +++ b/pkg/studio/export.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/json" "github.com/stashapp/stash/pkg/models/jsonschema" @@ -61,7 +62,7 @@ func ToJSON(ctx context.Context, reader FinderImageStashIDGetter, studio *models image, err := reader.GetImage(ctx, studio.ID) if err != nil { - return nil, fmt.Errorf("error getting studio image: %v", err) + logger.Errorf("Error getting studio image: %v", err) } if len(image) > 0 { diff --git a/pkg/studio/export_test.go b/pkg/studio/export_test.go index 8b329668e..702bab863 100644 --- a/pkg/studio/export_test.go +++ b/pkg/studio/export_test.go @@ -147,8 +147,9 @@ func initTestTable() { }, { createFullStudio(errImageID, parentStudioID), - nil, - true, + createFullJSONStudio(parentStudioName, "", nil), + // failure to get image is not an error + false, }, { createFullStudio(missingParentStudioID, missingStudioID), @@ -200,6 +201,7 @@ func TestToJSON(t *testing.T) { mockStudioReader.On("GetStashIDs", ctx, studioID).Return(stashIDs, nil).Once() mockStudioReader.On("GetStashIDs", ctx, noImageID).Return(nil, nil).Once() mockStudioReader.On("GetStashIDs", ctx, missingParentStudioID).Return(stashIDs, nil).Once() + mockStudioReader.On("GetStashIDs", ctx, errImageID).Return(stashIDs, nil).Once() for i, s := range scenarios { studio := s.input diff --git a/pkg/tag/export.go b/pkg/tag/export.go index 93f5e97b3..fc37ae43f 100644 --- a/pkg/tag/export.go +++ b/pkg/tag/export.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/json" "github.com/stashapp/stash/pkg/models/jsonschema" @@ -35,7 +36,7 @@ func ToJSON(ctx context.Context, reader FinderAliasImageGetter, tag *models.Tag) image, err := reader.GetImage(ctx, tag.ID) if err != nil { - return nil, fmt.Errorf("error getting tag image: %v", err) + logger.Errorf("Error getting tag image: %v", err) } if len(image) > 0 { diff --git a/pkg/tag/export_test.go b/pkg/tag/export_test.go index e7a68aaf1..e207db7a5 100644 --- a/pkg/tag/export_test.go +++ b/pkg/tag/export_test.go @@ -92,8 +92,9 @@ func initTestTable() { }, { createTag(errImageID), - nil, - true, + createJSONTag(nil, "", nil), + // getting the image should not cause an error + false, }, { createTag(errAliasID), @@ -140,6 +141,7 @@ func TestToJSON(t *testing.T) { mockTagReader.On("FindByChildTagID", ctx, noImageID).Return(nil, nil).Once() mockTagReader.On("FindByChildTagID", ctx, withParentsID).Return([]*models.Tag{{Name: "parent"}}, nil).Once() mockTagReader.On("FindByChildTagID", ctx, errParentsID).Return(nil, parentsErr).Once() + mockTagReader.On("FindByChildTagID", ctx, errImageID).Return(nil, nil).Once() for i, s := range scenarios { tag := s.tag diff --git a/pkg/txn/hooks.go b/pkg/txn/hooks.go index 13cc85d05..2327349cb 100644 --- a/pkg/txn/hooks.go +++ b/pkg/txn/hooks.go @@ -11,9 +11,10 @@ const ( ) type hookManager struct { - postCommitHooks []TxnFunc - postRollbackHooks []TxnFunc - postCompleteHooks []TxnFunc + preCommitHooks []TxnFunc + postCommitHooks []MustFunc + postRollbackHooks []MustFunc + postCompleteHooks []MustFunc } func (m *hookManager) register(ctx context.Context) context.Context { @@ -28,39 +29,55 @@ func hookManagerCtx(ctx context.Context) *hookManager { return m } -func executeHooks(ctx context.Context, hooks []TxnFunc) { +func executeHooks(ctx context.Context, hooks []TxnFunc) error { + // we need to return the first error for _, h := range hooks { - // ignore errors - _ = h(ctx) + if err := h(ctx); err != nil { + return err + } + } + + return nil +} + +func executeMustHooks(ctx context.Context, hooks []MustFunc) { + for _, h := range hooks { + h(ctx) } } -func executePostCommitHooks(ctx context.Context) { - m := hookManagerCtx(ctx) - executeHooks(ctx, m.postCommitHooks) +func (m *hookManager) executePostCommitHooks(ctx context.Context) { + executeMustHooks(ctx, m.postCommitHooks) } -func executePostRollbackHooks(ctx context.Context) { - m := hookManagerCtx(ctx) - executeHooks(ctx, m.postRollbackHooks) +func (m *hookManager) executePostRollbackHooks(ctx context.Context) { + executeMustHooks(ctx, m.postRollbackHooks) } -func executePostCompleteHooks(ctx context.Context) { - m := hookManagerCtx(ctx) - executeHooks(ctx, m.postCompleteHooks) +func (m *hookManager) executePreCommitHooks(ctx context.Context) error { + return executeHooks(ctx, m.preCommitHooks) } -func AddPostCommitHook(ctx context.Context, hook TxnFunc) { +func (m *hookManager) executePostCompleteHooks(ctx context.Context) { + executeMustHooks(ctx, m.postCompleteHooks) +} + +func AddPreCommitHook(ctx context.Context, hook TxnFunc) { + m := hookManagerCtx(ctx) + m.preCommitHooks = append(m.preCommitHooks, hook) +} + +func AddPostCommitHook(ctx context.Context, hook MustFunc) { m := hookManagerCtx(ctx) m.postCommitHooks = append(m.postCommitHooks, hook) } -func AddPostRollbackHook(ctx context.Context, hook TxnFunc) { +func AddPostRollbackHook(ctx context.Context, hook MustFunc) { m := hookManagerCtx(ctx) m.postRollbackHooks = append(m.postRollbackHooks, hook) } -func AddPostCompleteHook(ctx context.Context, hook TxnFunc) { +func AddPostCompleteHook(ctx context.Context, hook MustFunc) { m := hookManagerCtx(ctx) m.postCompleteHooks = append(m.postCompleteHooks, hook) } diff --git a/pkg/txn/transaction.go b/pkg/txn/transaction.go index 2a78da721..751588eff 100644 --- a/pkg/txn/transaction.go +++ b/pkg/txn/transaction.go @@ -17,13 +17,14 @@ type DatabaseProvider interface { WithDatabase(ctx context.Context) (context.Context, error) } -type DatabaseProviderManager interface { - DatabaseProvider - Manager -} - +// TxnFunc is a function that is used in transaction hooks. +// It should return an error if something went wrong. type TxnFunc func(ctx context.Context) error +// MustFunc is a function that is used in transaction hooks. +// It does not return an error. +type MustFunc func(ctx context.Context) + // WithTxn executes fn in a transaction. If fn returns an error then // the transaction is rolled back. Otherwise it is committed. // Transaction is exclusive. Only one thread may run a transaction @@ -52,35 +53,43 @@ func WithReadTxn(ctx context.Context, m Manager, fn TxnFunc) error { } func withTxn(ctx context.Context, m Manager, fn TxnFunc, exclusive bool, execCompleteOnLocked bool) error { - var err error - ctx, err = begin(ctx, m, exclusive) + // post-hooks should be executed with the outside context + txnCtx, err := begin(ctx, m, exclusive) if err != nil { return err } + hookMgr := hookManagerCtx(txnCtx) + defer func() { if p := recover(); p != nil { // a panic occurred, rollback and repanic - rollback(ctx, m) + rollback(txnCtx, m) panic(p) } if err != nil { // something went wrong, rollback - rollback(ctx, m) + rollback(txnCtx, m) + + // execute post-hooks with outside context + hookMgr.executePostRollbackHooks(ctx) if execCompleteOnLocked || !m.IsLocked(err) { - executePostCompleteHooks(ctx) + hookMgr.executePostCompleteHooks(ctx) } } else { // all good, commit - err = commit(ctx, m) - executePostCompleteHooks(ctx) + err = commit(txnCtx, m) + + // execute post-hooks with outside context + hookMgr.executePostCommitHooks(ctx) + hookMgr.executePostCompleteHooks(ctx) } }() - err = fn(ctx) + err = fn(txnCtx) return err } @@ -98,11 +107,15 @@ func begin(ctx context.Context, m Manager, exclusive bool) (context.Context, err } func commit(ctx context.Context, m Manager) error { + hookMgr := hookManagerCtx(ctx) + if err := hookMgr.executePreCommitHooks(ctx); err != nil { + return err + } + if err := m.Commit(ctx); err != nil { return err } - executePostCommitHooks(ctx) return nil } @@ -110,8 +123,6 @@ func rollback(ctx context.Context, m Manager) { if err := m.Rollback(ctx); err != nil { return } - - executePostRollbackHooks(ctx) } // WithDatabase executes fn with the context provided by p.WithDatabase. diff --git a/pkg/utils/context.go b/pkg/utils/context.go new file mode 100644 index 000000000..2a3862b5d --- /dev/null +++ b/pkg/utils/context.go @@ -0,0 +1,22 @@ +package utils + +import ( + "context" + "time" +) + +type ValueOnlyContext struct { + context.Context +} + +func (ValueOnlyContext) Deadline() (deadline time.Time, ok bool) { + return +} + +func (ValueOnlyContext) Done() <-chan struct{} { + return nil +} + +func (ValueOnlyContext) Err() error { + return nil +} diff --git a/pkg/utils/func.go b/pkg/utils/func.go new file mode 100644 index 000000000..a84091721 --- /dev/null +++ b/pkg/utils/func.go @@ -0,0 +1,12 @@ +package utils + +// Do executes each function in the slice in order. If any function returns an error, it is returned immediately. +func Do(fn []func() error) error { + for _, f := range fn { + if err := f(); err != nil { + return err + } + } + + return nil +} diff --git a/scripts/test_db_generator/config.yml b/scripts/test_db_generator/config.yml index ac072c459..a5d94b98f 100644 --- a/scripts/test_db_generator/config.yml +++ b/scripts/test_db_generator/config.yml @@ -2,6 +2,7 @@ database: generated.sqlite scenes: 30000 images: 4000000 galleries: 1500 +chapters: 3000 markers: 3000 performers: 10000 studios: 1500 @@ -15,4 +16,4 @@ naming: galleries: scene.txt studios: studio.txt tags: scene.txt - images: scene.txt \ No newline at end of file + images: scene.txt diff --git a/scripts/test_db_generator/makeTestDB.go b/scripts/test_db_generator/makeTestDB.go index 347e85873..bfdb042df 100644 --- a/scripts/test_db_generator/makeTestDB.go +++ b/scripts/test_db_generator/makeTestDB.go @@ -28,7 +28,7 @@ import ( const batchSize = 50000 // create an example database by generating a number of scenes, markers, -// performers, studios and tags, and associating between them all +// performers, studios, galleries, chapters and tags, and associating between them all type config struct { Database string `yaml:"database"` @@ -36,6 +36,7 @@ type config struct { Markers int `yaml:"markers"` Images int `yaml:"images"` Galleries int `yaml:"galleries"` + Chapters int `yaml:"chapters"` Performers int `yaml:"performers"` Studios int `yaml:"studios"` Tags int `yaml:"tags"` @@ -97,6 +98,7 @@ func populateDB() { makeScenes(c.Scenes) makeImages(c.Images) makeGalleries(c.Galleries) + makeChapters(c.Chapters) makeMarkers(c.Markers) } @@ -496,6 +498,38 @@ func generateGallery(i int) models.Gallery { } } +func makeChapters(n int) { + logf("creating %d chapters...", n) + for i := 0; i < n; { + // do in batches of 1000 + batch := i + batchSize + if err := withTxn(func(ctx context.Context) error { + for ; i < batch && i < n; i++ { + chapter := generateChapter(i) + chapter.GalleryID = models.NullInt64(int64(getRandomGallery())) + + created, err := repo.GalleryChapter.Create(ctx, chapter) + if err != nil { + return err + } + } + + logf("... created %d chapters", i) + + return nil + }); err != nil { + panic(err) + } + } +} + +func generateChapter(i int) models.GalleryChapter { + return models.GalleryChapter{ + Title: names[c.Naming.Galleries].generateName(rand.Intn(7) + 1), + ImageIndex: rand.Intn(200), + } +} + func makeMarkers(n int) { logf("creating %d markers...", n) for i := 0; i < n; { @@ -617,6 +651,10 @@ func getRandomScene() int { return rand.Intn(c.Scenes) + 1 } +func getRandomGallery() int { + return rand.Intn(c.Galleries) + 1 +} + func getRandomTags(ctx context.Context, min, max int) []int { var n int if min == max { diff --git a/tools.go b/tools.go index adc47d7e9..cb92a06fb 100644 --- a/tools.go +++ b/tools.go @@ -5,6 +5,7 @@ package main import ( _ "github.com/99designs/gqlgen" + _ "github.com/99designs/gqlgen/graphql/introspection" _ "github.com/Yamashou/gqlgenc" _ "github.com/vektah/dataloaden" _ "github.com/vektra/mockery/v2" diff --git a/ui/login/login.css b/ui/login/login.css index cddf4c8d3..dcf41ddbe 100644 --- a/ui/login/login.css +++ b/ui/login/login.css @@ -115,3 +115,18 @@ button, input { font-weight: 500; padding-bottom: 1rem; } + +@media (max-width: 576px) { + .card { + width: 100%; + } + + .dialog { + height: auto; + margin-top: 50%; + } + + .btn-primary { + width: 100%; + } +} \ No newline at end of file diff --git a/ui/v2.5/.env b/ui/v2.5/.env index 66df2e28e..6e49b46e4 100644 --- a/ui/v2.5/.env +++ b/ui/v2.5/.env @@ -1,3 +1,2 @@ BROWSER=none -PORT=3000 ESLINT_NO_DEV_ERRORS=true diff --git a/ui/v2.5/.eslintrc.json b/ui/v2.5/.eslintrc.json index ca3e8c0ce..f37f8028c 100644 --- a/ui/v2.5/.eslintrc.json +++ b/ui/v2.5/.eslintrc.json @@ -9,61 +9,73 @@ "parserOptions": { "project": "./tsconfig.json" }, - "plugins": [ - "@typescript-eslint", - "jsx-a11y" - ], + "plugins": ["@typescript-eslint", "jsx-a11y"], "extends": [ - "airbnb-typescript", - "airbnb/hooks", - "plugin:react/recommended", - "plugin:import/recommended", - "prettier", - "prettier/prettier" + "airbnb-typescript", + "plugin:import/recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "airbnb/hooks", + "prettier" ], "settings": { "react": { "version": "detect" } }, + "ignorePatterns": ["node_modules/", "src/core/generated-graphql.tsx"], "rules": { - "@typescript-eslint/no-explicit-any": 2, - "@typescript-eslint/naming-convention": [ - "error", - { + "@typescript-eslint/no-explicit-any": 2, + "@typescript-eslint/naming-convention": [ + "error", + { "selector": "interface", "format": ["PascalCase"], "custom": { "regex": "^I[A-Z]", "match": true - } } - ], - "lines-between-class-members": "off", - "@typescript-eslint/lines-between-class-members": "off", - "import/extensions": [ - "error", - "ignorePackages", - { - "js": "never", - "jsx": "never", - "ts": "never", - "tsx": "never" - } - ], - "import/named": "off", - "import/namespace": "off", - "import/no-unresolved": "off", - "react/display-name": "off", - "react/prop-types": "off", - "react/style-prop-object": ["error", { + } + ], + "lines-between-class-members": "off", + "@typescript-eslint/lines-between-class-members": "off", + "import/extensions": [ + "error", + "ignorePackages", + { + "js": "never", + "jsx": "never", + "ts": "never", + "tsx": "never" + } + ], + "import/named": "off", + "import/namespace": "off", + "import/no-unresolved": "off", + "react/display-name": "off", + "react-hooks/exhaustive-deps": [ + "error", + { "additionalHooks": "^(useDebounce)$" } + ], + "react/prop-types": "off", + "react/style-prop-object": [ + "error", + { "allow": ["FormattedNumber"] - }], - "spaced-comment": ["error", "always", { - "markers": ["/"] - }], - "prefer-destructuring": ["error", {"object": true, "array": false}], - "@typescript-eslint/no-use-before-define": ["error", { "functions": false, "classes": true }], - "no-nested-ternary": "off" + } + ], + "spaced-comment": [ + "error", + "always", + { + "markers": ["/"] + } + ], + "prefer-destructuring": ["error", { "object": true, "array": false }], + "@typescript-eslint/no-use-before-define": [ + "error", + { "functions": false, "classes": true } + ], + "no-nested-ternary": "off" } } diff --git a/ui/v2.5/.gitignore b/ui/v2.5/.gitignore index d7b8f1bd7..baf52f432 100755 --- a/ui/v2.5/.gitignore +++ b/ui/v2.5/.gitignore @@ -1,4 +1,5 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# generated +src/core/generated-*.tsx # dependencies /node_modules @@ -12,6 +13,7 @@ /build # misc +.gitignore .DS_Store .env.local .env.development.local @@ -23,3 +25,4 @@ yarn-debug.log* yarn-error.log* .eslintcache +.stylelintcache diff --git a/ui/v2.5/.prettierignore b/ui/v2.5/.prettierignore new file mode 100644 index 000000000..aeb40cd1c --- /dev/null +++ b/ui/v2.5/.prettierignore @@ -0,0 +1,18 @@ +*.md + +# dependencies +/node_modules +/.pnp +.pnp.js + +# locales +src/locales/**/*.json + +# testing +/coverage + +# production +/build + +# generated +src/core/generated-graphql.tsx \ No newline at end of file diff --git a/ui/v2.5/.stylelintrc b/ui/v2.5/.stylelintrc index 1357ed90e..de2f58dac 100644 --- a/ui/v2.5/.stylelintrc +++ b/ui/v2.5/.stylelintrc @@ -1,91 +1,55 @@ { - "plugins": [ - "stylelint-order" - ], - "extends": "stylelint-config-prettier", + "plugins": ["stylelint-order"], + "customSyntax": "postcss-scss", "rules": { - "indentation": null, - "at-rule-empty-line-before": [ "always", { - except: ["after-same-name", "first-nested" ], - ignore: ["after-comment"], - } ], + "at-rule-empty-line-before": [ + "always", + { + "except": ["after-same-name", "first-nested"], + "ignore": ["after-comment"] + } + ], "at-rule-no-vendor-prefix": true, "selector-no-vendor-prefix": true, - "block-closing-brace-newline-after": "always", - "block-closing-brace-newline-before": "always-multi-line", - "block-closing-brace-space-before": "always-single-line", "block-no-empty": true, - "block-opening-brace-newline-after": "always-multi-line", - "block-opening-brace-space-after": "always-single-line", - "block-opening-brace-space-before": "always", - "color-hex-case": "lower", "color-hex-length": "short", "color-no-invalid-hex": true, - "comment-empty-line-before": [ "always", { - except: ["first-nested"], - ignore: ["stylelint-commands"], - } ], + "comment-empty-line-before": [ + "always", + { + "except": ["first-nested"], + "ignore": ["stylelint-commands"] + } + ], "comment-whitespace-inside": "always", - "declaration-bang-space-after": "never", - "declaration-bang-space-before": "always", "declaration-block-no-shorthand-property-overrides": true, - "declaration-block-semicolon-newline-after": "always-multi-line", - "declaration-block-semicolon-space-after": "always-single-line", - "declaration-block-semicolon-space-before": "never", "declaration-block-single-line-max-declarations": 1, - "declaration-block-trailing-semicolon": "always", - "declaration-colon-space-after": "always-single-line", - "declaration-colon-space-before": "never", "declaration-no-important": true, "font-family-name-quotes": "always-where-recommended", "function-calc-no-unspaced-operator": true, - "function-comma-newline-after": "always-multi-line", - "function-comma-space-after": "always-single-line", - "function-comma-space-before": "never", "function-linear-gradient-no-nonstandard-direction": true, - "function-parentheses-newline-inside": "always-multi-line", - "function-parentheses-space-inside": "never-single-line", "function-url-quotes": "always", - "function-whitespace-after": "always", "length-zero-no-unit": true, - "max-empty-lines": 1, "max-nesting-depth": 4, - "media-feature-colon-space-after": "always", - "media-feature-colon-space-before": "never", - "media-feature-range-operator-space-after": "always", - "media-feature-range-operator-space-before": "always", - "media-query-list-comma-newline-after": "always-multi-line", - "media-query-list-comma-space-after": "always-single-line", - "media-query-list-comma-space-before": "never", - "media-feature-parentheses-space-inside": "never", "no-descending-specificity": null, "no-invalid-double-slash-comments": true, - "no-missing-end-of-source-newline": true, "number-max-precision": 3, - "number-no-trailing-zeros": true, - "order/order": [ - "custom-properties", - "declarations" - ], + "order/order": ["custom-properties", "declarations"], "order/properties-alphabetical-order": true, - "rule-empty-line-before": ["always-multi-line", { - except: ["after-single-line-comment", "first-nested" ], - ignore: ["after-comment"], - }], + "rule-empty-line-before": [ + "always-multi-line", + { + "except": ["after-single-line-comment", "first-nested"], + "ignore": ["after-comment"] + } + ], "selector-max-id": 1, "selector-max-type": 2, "selector-class-pattern": "^(\\.*[A-Z]*[a-z]+)+(-[a-z0-9]+)*$", - "selector-combinator-space-after": "always", - "selector-combinator-space-before": "always", - "selector-list-comma-newline-after": "always", - "selector-list-comma-space-before": "never", "selector-max-universal": 0, "selector-type-case": "lower", "selector-pseudo-element-colon-notation": "double", "string-no-newline": true, - "string-quotes": "double", - "time-min-milliseconds": 100, - "value-list-comma-space-after": "always-single-line", - "value-list-comma-space-before": "never" - }, + "time-min-milliseconds": 100 + } } diff --git a/ui/v2.5/.vscode/launch.json b/ui/v2.5/.vscode/launch.json deleted file mode 100644 index a0b21cbef..000000000 --- a/ui/v2.5/.vscode/launch.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "chrome", - "request": "launch", - "name": "Chrome", - "url": "http://localhost:3000", - "webRoot": "${workspaceFolder}/src", - "sourceMapPathOverrides": { - "webpack:///src/*": "${webRoot}/*" - } - } - ] -} \ No newline at end of file diff --git a/ui/v2.5/.vscode/settings.json b/ui/v2.5/.vscode/settings.json deleted file mode 100644 index ba5be62ee..000000000 --- a/ui/v2.5/.vscode/settings.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "typescript.tsdk": "node_modules/typescript/lib", - "editor.tabSize": 2, - "editor.renderWhitespace": "boundary", - "editor.wordWrap": "bounded", - "javascript.preferences.importModuleSpecifier": "relative", - "typescript.preferences.importModuleSpecifier": "relative", - "editor.wordWrapColumn": 120, - "editor.rulers": [ - 120 - ], - "i18n-ally.localesPaths": [ - "src/locales" - ], - "i18n-ally.keystyle": "nested", - "i18n-ally.sourceLanguage": "en-GB", - "spellright.language": [ - "en" - ], - "spellright.documentTypes": [ - "markdown", - "latex", - "plaintext", - "typescriptreact" - ] -} \ No newline at end of file diff --git a/ui/v2.5/codegen.yml b/ui/v2.5/codegen.yml index d648406a9..08b8c54fd 100644 --- a/ui/v2.5/codegen.yml +++ b/ui/v2.5/codegen.yml @@ -4,11 +4,9 @@ documents: "../../graphql/documents/**/*.graphql" generates: src/core/generated-graphql.tsx: plugins: - - add: - content: "/* eslint-disable */" - time - typescript - typescript-operations - typescript-react-apollo config: - withRefetchFn: true + withRefetchFn: true diff --git a/ui/v2.5/index.html b/ui/v2.5/index.html index b25ebc3d0..11bbb270d 100755 --- a/ui/v2.5/index.html +++ b/ui/v2.5/index.html @@ -1,7 +1,7 @@ - + @@ -10,27 +10,15 @@ content="width=device-width, initial-scale=1, maximum-scale=1" /> - Stash - +
- diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index 21c25ab58..7236dda18 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -3,125 +3,119 @@ "version": "0.1.0", "private": true, "homepage": "./", - "sideEffects": false, "scripts": { "start": "vite", "build": "vite build", - "build-ci": "yarn validate && yarn build", - "validate": "yarn lint && tsc --noEmit && yarn format-check", - "lint": "yarn lint:css && yarn lint:js", - "lint:js": "eslint --cache src/**/*.{ts,tsx}", - "lint:css": "stylelint \"src/**/*.scss\"", - "format": "prettier --write \"src/**/!(generated-graphql).{js,jsx,ts,tsx,scss}\"", - "format-check": "prettier --check \"src/**/!(generated-graphql).{js,jsx,ts,tsx,scss}\"", + "build-ci": "yarn run validate && yarn run build", + "validate": "yarn run lint && yarn run check && yarn run format-check", + "lint": "yarn run lint:css && yarn run lint:js", + "lint:css": "stylelint --cache \"src/**/*.scss\"", + "lint:js": "eslint --cache src/", + "check": "tsc --noEmit", + "format": "prettier --write .", + "format-check": "prettier --check .", "gqlgen": "gql-gen --config codegen.yml", "extract": "NODE_ENV=development extract-messages -l=en,de -o src/locale -d en --flat false 'src/**/!(*.test).tsx'" }, - "browserslist": [ - ">0.2%", - "not dead", - "not ie <= 11", - "not op_mini all" - ], "dependencies": { - "@apollo/client": "^3.3.7", - "@formatjs/intl-getcanonicallocales": "^1.5.3", - "@formatjs/intl-locale": "^2.4.14", - "@formatjs/intl-numberformat": "^6.1.3", - "@formatjs/intl-pluralrules": "^4.0.6", - "@fortawesome/fontawesome-svg-core": "^1.2.34", - "@fortawesome/free-regular-svg-icons": "^5.15.2", - "@fortawesome/free-solid-svg-icons": "^5.15.2", - "@fortawesome/react-fontawesome": "^0.1.14", - "@types/react-select": "^4.0.8", - "ansi-regex": "^5.0.1", - "apollo-upload-client": "^14.1.3", - "axios": "^1.1.3", + "@ant-design/react-slick": "^1.0.0", + "@apollo/client": "^3.7.8", + "@formatjs/intl-getcanonicallocales": "^2.0.5", + "@formatjs/intl-locale": "^3.0.11", + "@formatjs/intl-numberformat": "^8.3.3", + "@formatjs/intl-pluralrules": "^5.1.8", + "@fortawesome/fontawesome-svg-core": "^6.3.0", + "@fortawesome/free-brands-svg-icons": "^6.3.0", + "@fortawesome/free-regular-svg-icons": "^6.3.0", + "@fortawesome/free-solid-svg-icons": "^6.3.0", + "@fortawesome/react-fontawesome": "^0.2.0", + "apollo-upload-client": "^17.0.0", + "axios": "^1.3.3", "base64-blob": "^1.4.1", - "bootstrap": "^4.6.0", - "classnames": "^2.2.6", - "flag-icon-css": "^3.5.0", + "bootstrap": "^4.6.2", + "classnames": "^2.3.2", + "flag-icons": "^6.6.6", "flexbin": "^0.2.0", - "formik": "^2.2.6", - "graphql": "^15.4.0", - "graphql-tag": "^2.11.0", - "i18n-iso-countries": "^6.4.0", - "intersection-observer": "^0.12.0", - "localforage": "^1.9.0", + "formik": "^2.2.9", + "graphql": "^16.6.0", + "graphql-tag": "^2.12.6", + "graphql-ws": "^5.11.3", + "i18n-iso-countries": "^7.5.0", + "intersection-observer": "^0.12.2", + "localforage": "^1.10.0", "lodash-es": "^4.17.21", "mousetrap": "^1.6.5", "mousetrap-pause": "^1.0.0", "normalize-url": "^4.5.1", - "postcss": "^8.2.10", - "query-string": "6.13.8", - "react": "17.0.2", - "react-bootstrap": "1.4.3", - "react-dom": "17.0.2", + "react": "^17.0.2", + "react-bootstrap": "^1.6.6", + "react-datepicker": "^4.10.0", + "react-dom": "^17.0.2", "react-helmet": "^6.1.0", - "react-intl": "^5.10.16", - "react-markdown": "^7.1.0", + "react-intl": "^6.2.8", + "react-photo-gallery": "^8.0.0", + "react-remark": "^2.1.0", "react-router-bootstrap": "^0.25.0", - "react-router-dom": "^5.2.0", - "react-router-hash-link": "^2.3.1", - "react-select": "^4.0.2", - "react-slick": "^0.29.0", + "react-router-dom": "^5.3.4", + "react-router-hash-link": "^2.4.3", + "react-select": "^5.7.0", "remark-gfm": "^1.0.0", "resize-observer-polyfill": "^1.5.1", - "sass": "^1.32.5", "slick-carousel": "^1.8.1", - "string.prototype.replaceall": "^1.0.4", - "subscriptions-transport-ws": "^0.9.18", + "string.prototype.replaceall": "^1.0.7", "thehandy": "^1.0.3", "universal-cookie": "^4.0.4", - "video.js": "^7.20.3", + "video.js": "^7.21.3", + "videojs-contrib-dash": "^5.1.1", "videojs-mobile-ui": "^0.8.0", "videojs-seek-buttons": "^3.0.1", "videojs-vtt.js": "^0.15.4", - "vite": "^2.9.13", - "vite-plugin-compression": "^0.3.5", - "vite-tsconfig-paths": "^3.3.17", - "ws": "^7.4.6", - "yup": "^0.32.9" + "yup": "^1.0.0" }, "devDependencies": { - "@graphql-codegen/add": "^2.0.2", - "@graphql-codegen/cli": "^1.20.0", - "@graphql-codegen/time": "^2.0.2", - "@graphql-codegen/typescript": "^1.20.00", - "@graphql-codegen/typescript-operations": "^1.17.13", - "@graphql-codegen/typescript-react-apollo": "^2.2.1", - "@types/apollo-upload-client": "^14.1.0", - "@types/classnames": "^2.2.11", - "@types/fslightbox-react": "^1.4.0", + "@babel/core": "^7.20.12", + "@graphql-codegen/cli": "^3.0.0", + "@graphql-codegen/time": "^4.0.0", + "@graphql-codegen/typescript": "^3.0.0", + "@graphql-codegen/typescript-operations": "^3.0.0", + "@graphql-codegen/typescript-react-apollo": "^3.3.7", + "@types/apollo-upload-client": "^17.0.2", "@types/lodash-es": "^4.17.6", - "@types/mousetrap": "^1.6.5", - "@types/node": "14.14.22", - "@types/react": "17.0.31", - "@types/react-dom": "^17.0.10", - "@types/react-helmet": "^6.1.3", + "@types/mousetrap": "^1.6.11", + "@types/node": "^18.13.0", + "@types/react": "^17.0.53", + "@types/react-datepicker": "^4.10.0", + "@types/react-dom": "^17.0.19", + "@types/react-helmet": "^6.1.6", "@types/react-router-bootstrap": "^0.24.5", - "@types/react-router-dom": "5.1.7", - "@types/react-router-hash-link": "^1.2.1", - "@types/react-slick": "^0.23.8", - "@types/video.js": "^7.3.49", + "@types/react-router-hash-link": "^2.4.5", + "@types/video.js": "^7.3.51", "@types/videojs-mobile-ui": "^0.5.0", "@types/videojs-seek-buttons": "^2.1.0", - "@typescript-eslint/eslint-plugin": "^4.33.0", - "@typescript-eslint/parser": "^4.33.0", - "eslint": "^7.32.0", - "eslint-config-airbnb": "^18.2.1", - "eslint-config-airbnb-typescript": "^14.0.1", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-jsx-a11y": "^6.4.1", - "eslint-plugin-react": "^7.26.1", - "eslint-plugin-react-hooks": "^4.2.0", + "@typescript-eslint/eslint-plugin": "^5.52.0", + "@typescript-eslint/parser": "^5.52.0", + "@vitejs/plugin-legacy": "^4.0.1", + "@vitejs/plugin-react": "^3.1.0", + "eslint": "^8.34.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-typescript": "^17.0.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", "extract-react-intl-messages": "^4.1.1", - "postcss-safe-parser": "^5.0.2", - "prettier": "2.2.1", - "stylelint": "^13.9.0", - "stylelint-config-prettier": "^8.0.2", - "stylelint-order": "^4.1.0", - "typescript": "~4.4.4" + "postcss": "^8.4.21", + "postcss-scss": "^4.0.6", + "prettier": "^2.8.4", + "sass": "^1.58.1", + "stylelint": "^15.1.0", + "stylelint-order": "^6.0.2", + "terser": "^5.9.0", + "ts-node": "^10.9.1", + "typescript": "~4.8.4", + "vite": "^4.1.1", + "vite-plugin-compression": "^0.5.1", + "vite-tsconfig-paths": "^4.0.5" } } diff --git a/ui/v2.5/src/@types/mousetrap-pause.d.ts b/ui/v2.5/src/@types/mousetrap-pause.d.ts new file mode 100644 index 000000000..8abd93fcb --- /dev/null +++ b/ui/v2.5/src/@types/mousetrap-pause.d.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +declare module "mousetrap-pause" { + import { MousetrapStatic } from "mousetrap"; + + function MousetrapPause(mousetrap: MousetrapStatic): MousetrapStatic; + + export default MousetrapPause; + + module "mousetrap" { + interface MousetrapStatic { + pause(): void; + unpause(): void; + pauseCombo(combo: string): void; + unpauseCombo(combo: string): void; + } + interface MousetrapInstance { + pause(): void; + unpause(): void; + pauseCombo(combo: string): void; + unpauseCombo(combo: string): void; + } + } +} diff --git a/ui/v2.5/src/@types/string.prototype.replaceall.d.ts b/ui/v2.5/src/@types/string.prototype.replaceall.d.ts new file mode 100644 index 000000000..fa87eec06 --- /dev/null +++ b/ui/v2.5/src/@types/string.prototype.replaceall.d.ts @@ -0,0 +1,19 @@ +declare module "string.prototype.replaceall" { + function replaceAll( + searchValue: string | RegExp, + replaceValue: string + ): string; + function replaceAll( + searchValue: string | RegExp, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + replacer: (substring: string, ...args: any[]) => string + ): string; + + namespace replaceAll { + function getPolyfill(): typeof replaceAll; + function implementation(): typeof replaceAll; + function shim(): void; + } + + export default replaceAll; +} diff --git a/ui/v2.5/src/@types/videojs-contrib-dash.d.ts b/ui/v2.5/src/@types/videojs-contrib-dash.d.ts new file mode 100644 index 000000000..b791d1edb --- /dev/null +++ b/ui/v2.5/src/@types/videojs-contrib-dash.d.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +declare module "videojs-contrib-dash" { + class Html5DashJS { + /** + * Get a list of hooks for a specific lifecycle. + * + * @param type the lifecycle to get hooks from + * @param hook optionally add a hook to the lifecycle + * @return an array of hooks or empty if none + */ + static hooks(type: string, hook: Function | Function[]): Function[]; + + /** + * Add a function hook to a specific dash lifecycle. + * + * @param type the lifecycle to hook the function to + * @param hook the function or array of functions to attach + */ + static hook(type: string, hook: Function | Function[]): void; + + /** + * Remove a hook from a specific dash lifecycle. + * + * @param type the lifecycle that the function hooked to + * @param hook the hooked function to remove + * @return true if the function was removed, false if not found + */ + static removeHook(type: string, hook: Function): boolean; + } +} diff --git a/ui/v2.5/src/@types/videojs-vtt.d.ts b/ui/v2.5/src/@types/videojs-vtt.d.ts index 7140e5b6e..191c0d27d 100644 --- a/ui/v2.5/src/@types/videojs-vtt.d.ts +++ b/ui/v2.5/src/@types/videojs-vtt.d.ts @@ -1,111 +1,111 @@ /* eslint-disable @typescript-eslint/naming-convention */ declare module "videojs-vtt.js" { - namespace vttjs { - /** - * A custom JS error object that is reported through the parser's `onparsingerror` callback. - * It has a name, code, and message property, along with all the regular properties that come with a JavaScript error object. - * - * There are two error codes that can be reported back currently: - * * 0 BadSignature - * * 1 BadTimeStamp - * - * Note: Exceptions other then ParsingError will be thrown and not reported. - */ - class ParsingError extends Error { - readonly name: string; - readonly code: number; - readonly message: string; - } - - namespace WebVTT { - /** - * A parser for the WebVTT spec in JavaScript. - */ - class Parser { - /** - * The Parser constructor is passed a window object with which it will create new `VTTCues` and `VTTRegions` - * as well as an optional `StringDecoder` object which it will use to decode the data that the `parse()` function receives. - * For ease of use, a `StringDecoder` is provided via `WebVTT.StringDecoder()`. - * If a custom `StringDecoder` object is passed in it must support the API specified by the #whatwg string encoding spec. - * - * @param window the window object to use - * @param vttjs the vtt.js module - * @param decoder the decoder to decode `parse()` data with - */ - constructor(window: Window); - constructor(window: Window, decoder: TextDecoder); - constructor(window: Window, vttjs: vttjs, decoder: TextDecoder); - - /** - * Callback that is invoked for every region that is correctly parsed. Is passed a `VTTRegion` object. - */ - onregion?: (cue: VTTRegion) => void; - - /** - * Callback that is invoked for every cue that is fully parsed. In case of streaming parsing, - * `oncue` is delayed until the cue has been completely received. Is passed a `VTTCue` object. - */ - oncue?: (cue: VTTCue) => void; - - /** - * Is invoked in response to `flush()` and after the content was parsed completely. - */ - onflush?: () => void; - - /** - * Is invoked when a parsing error has occurred. This means that some part of the WebVTT file markup is badly formed. - * Is passed a `ParsingError` object. - */ - onparsingerror?: (e: ParsingError) => void; - - /** - * Hands data in some format to the parser for parsing. The passed data format is expected to be decodable by the - * StringDecoder object that it has. The parser decodes the data and reassembles partial data (streaming), even across line breaks. - * - * @param data data to be parsed - */ - parse(data: string): this; - - /** - * Indicates that no more data is expected and will force the parser to parse any unparsed data that it may have. - * Will also trigger `onflush`. - */ - flush(): this; - } - - /** - * Helper to allow strings to be decoded instead of the default binary utf8 data. - */ - function StringDecoder(): TextDecoder; - - /** - * Parses the cue text handed to it into a tree of DOM nodes that mirrors the internal WebVTT node structure of the cue text. - * It uses the window object handed to it to construct new HTMLElements and returns a tree of DOM nodes attached to a top level div. - * - * @param window window object to use - * @param cuetext cue text to parse - */ - function convertCueToDOMTree( - window: Window, - cuetext: string - ): HTMLDivElement | null; - - /** - * Converts the cuetext of the cues passed to it to DOM trees - by calling convertCueToDOMTree - and then runs the - * processing model steps of the WebVTT specification on the divs. The processing model applies the necessary CSS styles - * to the cue divs to prepare them for display on the web page. During this process the cue divs get added to a block level element (overlay). - * The overlay should be a part of the live DOM as the algorithm will use the computed styles (only of the divs) to do overlap avoidance. - * - * @param overlay A block level element (usually a div) that the computed cues and regions will be placed into. - */ - function processCues( - window: Window, - cues: VTTCue[], - overlay: Element - ): void; - } + /** + * A custom JS error object that is reported through the parser's `onparsingerror` callback. + * It has a name, code, and message property, along with all the regular properties that come with a JavaScript error object. + * + * There are two error codes that can be reported back currently: + * * 0 BadSignature + * * 1 BadTimeStamp + * + * Note: Exceptions other then ParsingError will be thrown and not reported. + */ + class ParsingError extends Error { + readonly name: string; + readonly code: number; + readonly message: string; } - export = vttjs; + export namespace WebVTT { + /** + * A parser for the WebVTT spec in JavaScript. + */ + class Parser { + /** + * The Parser constructor is passed a window object with which it will create new `VTTCues` and `VTTRegions` + * as well as an optional `StringDecoder` object which it will use to decode the data that the `parse()` function receives. + * For ease of use, a `StringDecoder` is provided via `WebVTT.StringDecoder()`. + * If a custom `StringDecoder` object is passed in it must support the API specified by the #whatwg string encoding spec. + * + * @param window the window object to use + * @param vttjs the vtt.js module + * @param decoder the decoder to decode `parse()` data with + */ + constructor(window: Window); + constructor(window: Window, decoder: TextDecoder); + constructor( + window: Window, + vttjs: typeof import("videojs-vtt.js"), + decoder: TextDecoder + ); + + /** + * Callback that is invoked for every region that is correctly parsed. Is passed a `VTTRegion` object. + */ + onregion?: (cue: VTTRegion) => void; + + /** + * Callback that is invoked for every cue that is fully parsed. In case of streaming parsing, + * `oncue` is delayed until the cue has been completely received. Is passed a `VTTCue` object. + */ + oncue?: (cue: VTTCue) => void; + + /** + * Is invoked in response to `flush()` and after the content was parsed completely. + */ + onflush?: () => void; + + /** + * Is invoked when a parsing error has occurred. This means that some part of the WebVTT file markup is badly formed. + * Is passed a `ParsingError` object. + */ + onparsingerror?: (e: ParsingError) => void; + + /** + * Hands data in some format to the parser for parsing. The passed data format is expected to be decodable by the + * StringDecoder object that it has. The parser decodes the data and reassembles partial data (streaming), even across line breaks. + * + * @param data data to be parsed + */ + parse(data: string): this; + + /** + * Indicates that no more data is expected and will force the parser to parse any unparsed data that it may have. + * Will also trigger `onflush`. + */ + flush(): this; + } + + /** + * Helper to allow strings to be decoded instead of the default binary utf8 data. + */ + function StringDecoder(): TextDecoder; + + /** + * Parses the cue text handed to it into a tree of DOM nodes that mirrors the internal WebVTT node structure of the cue text. + * It uses the window object handed to it to construct new HTMLElements and returns a tree of DOM nodes attached to a top level div. + * + * @param window window object to use + * @param cuetext cue text to parse + */ + function convertCueToDOMTree( + window: Window, + cuetext: string + ): HTMLDivElement | null; + + /** + * Converts the cuetext of the cues passed to it to DOM trees - by calling convertCueToDOMTree - and then runs the + * processing model steps of the WebVTT specification on the divs. The processing model applies the necessary CSS styles + * to the cue divs to prepare them for display on the web page. During this process the cue divs get added to a block level element (overlay). + * The overlay should be a part of the live DOM as the algorithm will use the computed styles (only of the divs) to do overlap avoidance. + * + * @param overlay A block level element (usually a div) that the computed cues and regions will be placed into. + */ + function processCues( + window: Window, + cues: VTTCue[], + overlay: Element + ): void; + } } diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index 71a0755e8..4a374065c 100644 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -1,11 +1,17 @@ -import React, { lazy, Suspense, useEffect, useState } from "react"; -import { Route, Switch, useRouteMatch } from "react-router-dom"; +import React, { Suspense, useEffect, useState } from "react"; +import { + Route, + Switch, + useHistory, + useLocation, + useRouteMatch, +} from "react-router-dom"; import { IntlProvider, CustomFormats } from "react-intl"; import { Helmet } from "react-helmet"; import cloneDeep from "lodash-es/cloneDeep"; import mergeWith from "lodash-es/mergeWith"; import { ToastProvider } from "src/hooks/Toast"; -import LightboxProvider from "src/hooks/Lightbox/context"; +import { LightboxProvider } from "src/hooks/Lightbox/context"; import { initPolyfills } from "src/polyfills"; import locales, { registerCountry } from "src/locales"; @@ -14,14 +20,16 @@ import { useConfigureUI, useSystemStatus, } from "src/core/StashService"; -import { flattenMessages } from "src/utils"; +import flattenMessages from "./utils/flattenMessages"; +import * as yup from "yup"; import Mousetrap from "mousetrap"; import MousetrapPause from "mousetrap-pause"; import { ErrorBoundary } from "./components/ErrorBoundary"; import { MainNavbar } from "./components/MainNavbar"; import { PageNotFound } from "./components/PageNotFound"; import * as GQL from "./core/generated-graphql"; -import { LoadingIndicator, TITLE_SUFFIX } from "./components/Shared"; +import { TITLE_SUFFIX } from "./components/Shared/constants"; +import { LoadingIndicator } from "./components/Shared/LoadingIndicator"; import { ConfigurationProvider } from "./hooks/Config"; import { ManualProvider } from "./components/Help/context"; @@ -29,26 +37,33 @@ import { InteractiveProvider } from "./hooks/Interactive/context"; import { ReleaseNotesDialog } from "./components/Dialogs/ReleaseNotesDialog"; import { IUIConfig } from "./core/config"; import { releaseNotes } from "./docs/en/ReleaseNotes"; -import { getPlatformURL, getBaseURL } from "./core/createClient"; +import { getPlatformURL } from "./core/createClient"; +import { lazyComponent } from "./utils/lazyComponent"; -const Performers = lazy(() => import("./components/Performers/Performers")); -const FrontPage = lazy(() => import("./components/FrontPage/FrontPage")); -const Scenes = lazy(() => import("./components/Scenes/Scenes")); -const Settings = lazy(() => import("./components/Settings/Settings")); -const Stats = lazy(() => import("./components/Stats")); -const Studios = lazy(() => import("./components/Studios/Studios")); -const Galleries = lazy(() => import("./components/Galleries/Galleries")); +const Performers = lazyComponent( + () => import("./components/Performers/Performers") +); +const FrontPage = lazyComponent( + () => import("./components/FrontPage/FrontPage") +); +const Scenes = lazyComponent(() => import("./components/Scenes/Scenes")); +const Settings = lazyComponent(() => import("./components/Settings/Settings")); +const Stats = lazyComponent(() => import("./components/Stats")); +const Studios = lazyComponent(() => import("./components/Studios/Studios")); +const Galleries = lazyComponent( + () => import("./components/Galleries/Galleries") +); -const Movies = lazy(() => import("./components/Movies/Movies")); -const Tags = lazy(() => import("./components/Tags/Tags")); -const Images = lazy(() => import("./components/Images/Images")); -const Setup = lazy(() => import("./components/Setup/Setup")); -const Migrate = lazy(() => import("./components/Setup/Migrate")); +const Movies = lazyComponent(() => import("./components/Movies/Movies")); +const Tags = lazyComponent(() => import("./components/Tags/Tags")); +const Images = lazyComponent(() => import("./components/Images/Images")); +const Setup = lazyComponent(() => import("./components/Setup/Setup")); +const Migrate = lazyComponent(() => import("./components/Setup/Migrate")); -const SceneFilenameParser = lazy( +const SceneFilenameParser = lazyComponent( () => import("./components/SceneFilenameParser/SceneFilenameParser") ); -const SceneDuplicateChecker = lazy( +const SceneDuplicateChecker = lazyComponent( () => import("./components/SceneDuplicateChecker/SceneDuplicateChecker") ); @@ -91,10 +106,12 @@ export const App: React.FC = () => { const defaultMessages = (await locales[defaultMessageLanguage]()).default; const mergedMessages = cloneDeep(Object.assign({}, defaultMessages)); const chosenMessages = (await locales[messageLanguage]()).default; - const res = await fetch(getPlatformURL() + "customlocales"); let customMessages = {}; try { - customMessages = res.ok ? await res.json() : {}; + const res = await fetch(getPlatformURL() + "customlocales"); + if (res.ok) { + customMessages = await res.json(); + } } catch (err) { console.log(err); } @@ -110,12 +127,25 @@ export const App: React.FC = () => { } ); - setMessages(flattenMessages(mergedMessages)); + const newMessages = flattenMessages(mergedMessages) as Record< + string, + string + >; + + yup.setLocale({ + mixed: { + required: newMessages["validation.required"], + }, + }); + + setMessages(newMessages); }; setLocale(); }, [language]); + const location = useLocation(); + const history = useHistory(); const setupMatch = useRouteMatch(["/setup", "/migrate"]); // redirect to setup or migrate as needed @@ -124,27 +154,24 @@ export const App: React.FC = () => { return; } - const baseURL = getBaseURL(); + const { status } = systemStatusData.systemStatus; if ( - window.location.pathname !== baseURL + "setup" && - systemStatusData.systemStatus.status === GQL.SystemStatusEnum.Setup + location.pathname !== "/setup" && + status === GQL.SystemStatusEnum.Setup ) { // redirect to setup page - const newURL = new URL("setup", window.location.origin + baseURL); - window.location.href = newURL.toString(); + history.push("/setup"); } if ( - window.location.pathname !== baseURL + "migrate" && - systemStatusData.systemStatus.status === - GQL.SystemStatusEnum.NeedsMigration + location.pathname !== "/migrate" && + status === GQL.SystemStatusEnum.NeedsMigration ) { - // redirect to setup page - const newURL = new URL("migrate", window.location.origin + baseURL); - window.location.href = newURL.toString(); + // redirect to migrate page + history.push("/migrate"); } - }, [systemStatusData]); + }, [systemStatusData, setupMatch, history, location]); function maybeRenderNavbar() { // don't render navbar for setup views @@ -190,7 +217,7 @@ export const App: React.FC = () => { } function maybeRenderReleaseNotes() { - if (setupMatch || config.loading || config.error) { + if (setupMatch || !systemStatusData || config.loading || config.error) { return; } @@ -204,7 +231,7 @@ export const App: React.FC = () => { return ( n.content)} + notes={notes} onClose={() => { saveUI({ variables: { diff --git a/ui/v2.5/src/components/Changelog/Changelog.tsx b/ui/v2.5/src/components/Changelog/Changelog.tsx index 22d6ed640..d959019a7 100644 --- a/ui/v2.5/src/components/Changelog/Changelog.tsx +++ b/ui/v2.5/src/components/Changelog/Changelog.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { useChangelogStorage } from "src/hooks"; +import { useChangelogStorage } from "src/hooks/LocalForage"; import Version from "./Version"; import V010 from "src/docs/en/Changelog/v010.md"; import V011 from "src/docs/en/Changelog/v011.md"; @@ -23,11 +23,10 @@ import V0160 from "src/docs/en/Changelog/v0160.md"; import V0161 from "src/docs/en/Changelog/v0161.md"; import V0170 from "src/docs/en/Changelog/v0170.md"; import V0180 from "src/docs/en/Changelog/v0180.md"; +import V0190 from "src/docs/en/Changelog/v0190.md"; +import V0200 from "src/docs/en/Changelog/v0200.md"; import { MarkdownPage } from "../Shared/MarkdownPage"; -// to avoid use of explicit any -type Module = typeof V010; - const Changelog: React.FC = () => { const [{ data, loading }, setOpenState] = useChangelogStorage(); @@ -54,16 +53,16 @@ const Changelog: React.FC = () => { interface IStashRelease { version: string; date?: string; - page: Module; + page: string; defaultOpen?: boolean; } // after new release: // add entry to releases, using the current* fields // then update the current fields. - const currentVersion = stashVersion || "v0.18.0"; + const currentVersion = stashVersion || "v0.20.0"; const currentDate = buildDate; - const currentPage = V0180; + const currentPage = V0200; const releases: IStashRelease[] = [ { @@ -72,6 +71,16 @@ const Changelog: React.FC = () => { page: currentPage, defaultOpen: true, }, + { + version: "v0.19.1", + date: "2023-02-21", + page: V0190, + }, + { + version: "v0.18.0", + date: "2022-11-30", + page: V0180, + }, { version: "v0.17.2", date: "2022-10-25", diff --git a/ui/v2.5/src/components/Changelog/Version.tsx b/ui/v2.5/src/components/Changelog/Version.tsx index cd5b99442..bc2f75138 100644 --- a/ui/v2.5/src/components/Changelog/Version.tsx +++ b/ui/v2.5/src/components/Changelog/Version.tsx @@ -2,7 +2,7 @@ import { faAngleDown, faAngleUp } from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Card, Collapse } from "react-bootstrap"; import { FormattedDate, FormattedMessage } from "react-intl"; -import { Icon } from "src/components/Shared"; +import { Icon } from "src/components/Shared/Icon"; interface IVersionProps { version: string; diff --git a/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx index f9eb805ce..4a0657efe 100644 --- a/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx @@ -1,13 +1,14 @@ import React, { useState, useEffect, useMemo } from "react"; import { Form, Button } from "react-bootstrap"; import { mutateMetadataGenerate } from "src/core/StashService"; -import { Modal, Icon } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { ModalComponent } from "../Shared/Modal"; +import { Icon } from "src/components/Shared/Icon"; +import { useToast } from "src/hooks/Toast"; import * as GQL from "src/core/generated-graphql"; import { FormattedMessage, useIntl } from "react-intl"; import { ConfigurationContext } from "src/hooks/Config"; import { Manual } from "../Help/Manual"; -import { withoutTypename } from "src/utils"; +import { withoutTypename } from "src/utils/data"; import { GenerateOptions } from "../Settings/Tasks/GenerateOptions"; import { SettingSection } from "../Settings/SettingSection"; import { faCogs, faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; @@ -169,7 +170,7 @@ export const GenerateDialog: React.FC = ({ } return ( - = ({ /> - + ); }; diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx index f8c146fbb..ba027cd5c 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback } from "react"; import { Form, Button, Table } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; +import { Icon } from "src/components/Shared/Icon"; import * as GQL from "src/core/generated-graphql"; import { FormattedMessage, useIntl } from "react-intl"; import { @@ -261,9 +261,8 @@ export const FieldOptionsList: React.FC = ({ allowSetDefault = true, defaultOptions, }) => { - const [localFieldOptions, setLocalFieldOptions] = useState< - GQL.IdentifyFieldOptions[] - >(); + const [localFieldOptions, setLocalFieldOptions] = + useState(); const [editField, setEditField] = useState(); useEffect(() => { diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx index f5964ee97..7c5207f44 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx @@ -6,11 +6,13 @@ import { useConfigureDefaults, useListSceneScrapers, } from "src/core/StashService"; -import { Icon, Modal, OperationButton } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { Icon } from "src/components/Shared/Icon"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { OperationButton } from "src/components/Shared/OperationButton"; +import { useToast } from "src/hooks/Toast"; import * as GQL from "src/core/generated-graphql"; import { FormattedMessage, useIntl } from "react-intl"; -import { withoutTypename } from "src/utils"; +import { withoutTypename } from "src/utils/data"; import { SCRAPER_PREFIX, STASH_BOX_PREFIX, @@ -202,9 +204,8 @@ export const IdentifyDialog: React.FC = ({ if (s.options) { const sourceOptions = withoutTypename(s.options); - sourceOptions.fieldOptions = sourceOptions.fieldOptions?.map( - withoutTypename - ); + sourceOptions.fieldOptions = + sourceOptions.fieldOptions?.map(withoutTypename); ret.options = sourceOptions; } @@ -215,9 +216,8 @@ export const IdentifyDialog: React.FC = ({ setSources(mappedSources); if (identifyDefaults.options) { const defaultOptions = withoutTypename(identifyDefaults.options); - defaultOptions.fieldOptions = defaultOptions.fieldOptions?.map( - withoutTypename - ); + defaultOptions.fieldOptions = + defaultOptions.fieldOptions?.map(withoutTypename); setOptions(defaultOptions); } } else { @@ -405,7 +405,7 @@ export const IdentifyDialog: React.FC = ({ } return ( - = ({ setEditingField={(v) => setEditingField(v)} /> - + ); }; diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx index 9cc0c6a51..b18e661bc 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import { Form, Button, ListGroup } from "react-bootstrap"; -import { Modal, Icon } from "src/components/Shared"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { Icon } from "src/components/Shared/Icon"; import { FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { IScraperSource } from "./constants"; @@ -53,7 +54,7 @@ export const SourcesEditor: React.FC = ({ } return ( - = ({ defaultOptions={defaultOptions} /> - + ); }; diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/constants.ts b/ui/v2.5/src/components/Dialogs/IdentifyDialog/constants.ts index 46ed88854..13889d037 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/constants.ts +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/constants.ts @@ -20,7 +20,7 @@ export const sceneFields = [ "tags", "stash_ids", ] as const; -export type SceneField = typeof sceneFields[number]; +export type SceneField = (typeof sceneFields)[number]; export const multiValueSceneFields: SceneField[] = [ "studio", diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/styles.scss b/ui/v2.5/src/components/Dialogs/IdentifyDialog/styles.scss index 450b31e86..dcaeb054c 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/styles.scss +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/styles.scss @@ -32,7 +32,3 @@ margin-right: 0.25em; } } - -.field-options-table td:first-child { - padding-left: 0.75rem; -} diff --git a/ui/v2.5/src/components/Dialogs/ReleaseNotesDialog.tsx b/ui/v2.5/src/components/Dialogs/ReleaseNotesDialog.tsx index fefc344be..4cfc35fdc 100644 --- a/ui/v2.5/src/components/Dialogs/ReleaseNotesDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/ReleaseNotesDialog.tsx @@ -1,13 +1,12 @@ import React from "react"; -import { Form } from "react-bootstrap"; -import { Modal } from "src/components/Shared"; +import { ModalComponent } from "../Shared/Modal"; import { faCogs } from "@fortawesome/free-solid-svg-icons"; import { useIntl } from "react-intl"; import { MarkdownPage } from "../Shared/MarkdownPage"; -import { Module } from "src/docs/en/ReleaseNotes"; +import { IReleaseNotes } from "src/docs/en/ReleaseNotes"; interface IReleaseNotesDialog { - notes: Module[]; + notes: IReleaseNotes[]; onClose: () => void; } @@ -18,7 +17,7 @@ export const ReleaseNotesDialog: React.FC = ({ const intl = useIntl(); return ( - = ({ text: intl.formatMessage({ id: "actions.close" }), }} > -
- {notes.map((n, i) => ( - - ))} - -
+
+ {notes + .map((n, i) => ( +
+

{n.version}

+ +
+ )) + .reduce((accu, curr) => ( + <> + {accu} +
+ {curr} + + ))} +
+ ); }; diff --git a/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx b/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx index dca73532f..7c0364f8d 100644 --- a/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx +++ b/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { useMutation, DocumentNode } from "@apollo/client"; import { Button, Form } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; -import { Modal } from "src/components/Shared"; -import { getStashboxBase } from "src/utils"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { getStashboxBase } from "src/utils/stashbox"; import { FormattedMessage, useIntl } from "react-intl"; import { faPaperPlane } from "@fortawesome/free-solid-svg-icons"; @@ -78,7 +78,7 @@ export const SubmitStashBoxDraft: React.FC = ({ undefined; return ( - = ({ {boxes.map((box, i) => ( + ); }; diff --git a/ui/v2.5/src/components/ErrorBoundary.tsx b/ui/v2.5/src/components/ErrorBoundary.tsx index 302abb2cc..a6e466efb 100644 --- a/ui/v2.5/src/components/ErrorBoundary.tsx +++ b/ui/v2.5/src/components/ErrorBoundary.tsx @@ -1,4 +1,6 @@ import React from "react"; +import { FormattedMessage } from "react-intl"; +import { isLazyComponentError } from "src/utils/lazyComponent"; interface IErrorBoundaryProps { children?: React.ReactNode; @@ -10,6 +12,7 @@ type ErrorInfo = { interface IErrorBoundaryState { error?: Error; + errorHelpId?: string; errorInfo?: ErrorInfo; } @@ -23,22 +26,35 @@ export class ErrorBoundary extends React.Component< } public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + let errorHelpId: string | undefined; + if (isLazyComponentError(error)) { + errorHelpId = "errors.lazy_component_error_help"; + } this.setState({ error, + errorHelpId, errorInfo, }); } public render() { - if (this.state.errorInfo) { + const { error, errorHelpId, errorInfo } = this.state; + if (errorInfo) { // Error path return (
-

Something went wrong.

+

+ +

+ {errorHelpId && ( +
+ +
+ )}
- {this.state.error && this.state.error.toString()} + {error?.toString()}
- {this.state.errorInfo.componentStack} + {errorInfo.componentStack.trim().replaceAll(/^\s*/gm, " ")}
); diff --git a/ui/v2.5/src/components/FrontPage/FrontPage.tsx b/ui/v2.5/src/components/FrontPage/FrontPage.tsx index d68e2ca00..856afb48b 100644 --- a/ui/v2.5/src/components/FrontPage/FrontPage.tsx +++ b/ui/v2.5/src/components/FrontPage/FrontPage.tsx @@ -1,10 +1,10 @@ import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useConfigureUI } from "src/core/StashService"; -import { LoadingIndicator } from "src/components/Shared"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { Button } from "react-bootstrap"; import { FrontPageConfig } from "./FrontPageConfig"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; import { Control } from "./Control"; import { ConfigurationContext } from "src/hooks/Config"; import { diff --git a/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx b/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx index 4bbf6a7c0..04464b655 100644 --- a/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx +++ b/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { FormattedMessage, IntlShape, useIntl } from "react-intl"; import { useFindSavedFilters } from "src/core/StashService"; -import { LoadingIndicator } from "src/components/Shared"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { Button, Form, Modal } from "react-bootstrap"; import { FilterMode, @@ -243,7 +243,8 @@ const ContentRow: React.FC = (props: IFilterRowProps) => { case "SavedFilter": const savedFilter = props.allSavedFilters.find( (f) => - f.id === (props.content as ISavedFilterRow).savedFilterId.toString() + f.id === + (props.content as ISavedFilterRow).savedFilterId?.toString() ); if (!savedFilter) return ""; return filterTitle(intl, savedFilter); @@ -337,7 +338,10 @@ export const FrontPageConfig: React.FC = ({ } const existingSavedFilterIDs = currentContent - .filter((f) => f.__typename === "SavedFilter") + .filter( + (f) => + f.__typename === "SavedFilter" && (f as ISavedFilterRow).savedFilterId + ) .map((f) => (f as ISavedFilterRow).savedFilterId.toString()); function addSavedFilter(content?: FrontPageContent) { diff --git a/ui/v2.5/src/components/FrontPage/styles.scss b/ui/v2.5/src/components/FrontPage/styles.scss index 34b58cc8a..a1661d032 100644 --- a/ui/v2.5/src/components/FrontPage/styles.scss +++ b/ui/v2.5/src/components/FrontPage/styles.scss @@ -69,11 +69,7 @@ white-space: normal; } -.recommendations-container .studio-card hr, -.recommendations-container .movie-card hr, -.recommendations-container .gallery-card hr, -.recommendations-container .image-card hr, -.recommendations-container .tag-card hr { +.card hr { margin-top: auto; } diff --git a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx index 4e128dd28..fe9de2d97 100644 --- a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx +++ b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { Form } from "react-bootstrap"; import { useGalleryDestroy } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; -import { Modal } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { ModalComponent } from "../Shared/Modal"; +import { useToast } from "src/hooks/Toast"; import { ConfigurationContext } from "src/hooks/Config"; import { FormattedMessage, useIntl } from "react-intl"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; @@ -119,7 +119,7 @@ export const DeleteGalleriesDialog: React.FC = ( } return ( - = ( onChange={() => setDeleteGenerated(!deleteGenerated)} /> - + ); }; diff --git a/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx b/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx index 24e153acd..68fb1f310 100644 --- a/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx +++ b/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx @@ -4,10 +4,11 @@ import { FormattedMessage, useIntl } from "react-intl"; import isEqual from "lodash-es/isEqual"; import { useBulkGalleryUpdate } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; -import { StudioSelect, Modal } from "src/components/Shared"; -import { useToast } from "src/hooks"; -import { FormUtils } from "src/utils"; -import MultiSet from "../Shared/MultiSet"; +import { StudioSelect } from "../Shared/Select"; +import { ModalComponent } from "../Shared/Modal"; +import { useToast } from "src/hooks/Toast"; +import FormUtils from "src/utils/form"; +import { MultiSet } from "../Shared/MultiSet"; import { RatingSystem } from "../Shared/Rating/RatingSystem"; import { getAggregateInputIDs, @@ -31,10 +32,8 @@ export const EditGalleriesDialog: React.FC = ( const Toast = useToast(); const [rating100, setRating] = useState(); const [studioId, setStudioId] = useState(); - const [ - performerMode, - setPerformerMode, - ] = React.useState(GQL.BulkUpdateIdMode.Add); + const [performerMode, setPerformerMode] = + React.useState(GQL.BulkUpdateIdMode.Add); const [performerIds, setPerformerIds] = useState(); const [existingPerformerIds, setExistingPerformerIds] = useState(); const [tagMode, setTagMode] = React.useState( @@ -228,7 +227,7 @@ export const EditGalleriesDialog: React.FC = ( function render() { return ( - = ( /> - + ); } diff --git a/ui/v2.5/src/components/Galleries/Galleries.tsx b/ui/v2.5/src/components/Galleries/Galleries.tsx index 7dd4d3b97..c2c6e9236 100644 --- a/ui/v2.5/src/components/Galleries/Galleries.tsx +++ b/ui/v2.5/src/components/Galleries/Galleries.tsx @@ -2,13 +2,13 @@ import React from "react"; import { Route, Switch } from "react-router-dom"; import { useIntl } from "react-intl"; import { Helmet } from "react-helmet"; -import { TITLE_SUFFIX } from "src/components/Shared"; -import { PersistanceLevel } from "src/hooks/ListHook"; +import { TITLE_SUFFIX } from "../Shared/constants"; +import { PersistanceLevel } from "../List/ItemList"; import Gallery from "./GalleryDetails/Gallery"; import GalleryCreate from "./GalleryDetails/GalleryCreate"; import { GalleryList } from "./GalleryList"; -const Galleries = () => { +const Galleries: React.FC = () => { const intl = useIntl(); const title_template = `${intl.formatMessage({ diff --git a/ui/v2.5/src/components/Galleries/GalleryCard.tsx b/ui/v2.5/src/components/Galleries/GalleryCard.tsx index 76673d74e..88fe37f2a 100644 --- a/ui/v2.5/src/components/Galleries/GalleryCard.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryCard.tsx @@ -2,17 +2,15 @@ import { Button, ButtonGroup } from "react-bootstrap"; import React from "react"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; -import { - GridCard, - HoverPopover, - Icon, - TagLink, - TruncatedText, -} from "src/components/Shared"; -import { PopoverCountButton } from "src/components/Shared/PopoverCountButton"; -import { NavUtils } from "src/utils"; -import { ConfigurationContext } from "src/hooks/Config"; +import { GridCard } from "../Shared/GridCard"; +import { HoverPopover } from "../Shared/HoverPopover"; +import { Icon } from "../Shared/Icon"; +import { TagLink } from "../Shared/TagLink"; +import { TruncatedText } from "../Shared/TruncatedText"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; +import { PopoverCountButton } from "../Shared/PopoverCountButton"; +import NavUtils from "src/utils/navigation"; +import { ConfigurationContext } from "src/hooks/Config"; import { RatingBanner } from "../Shared/RatingBanner"; import { faBox, faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons"; import { galleryTitle } from "src/core/galleries"; @@ -167,13 +165,11 @@ export const GalleryCard: React.FC = (props) => { details={
{props.gallery.date} -

- -

+
} popovers={maybeRenderPopoverButtonGroup()} diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/ChapterEntry.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/ChapterEntry.tsx new file mode 100644 index 000000000..632244a5b --- /dev/null +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/ChapterEntry.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { FormattedMessage } from "react-intl"; +import * as GQL from "src/core/generated-graphql"; +import { Button } from "react-bootstrap"; + +interface IChapterEntries { + galleryChapters: GQL.GalleryChapterDataFragment[]; + onClickChapter: (image_index: number) => void; + onEdit: (chapter: GQL.GalleryChapterDataFragment) => void; +} + +export const ChapterEntries: React.FC = ({ + galleryChapters, + onClickChapter, + onEdit, +}) => { + if (!galleryChapters?.length) return
; + + const chapterCards = galleryChapters.map((chapter) => { + return ( +
+
+
+ + +
+
+ ); + }); + + return
{chapterCards}
; +}; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx index 422e45d4e..1adedc799 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx @@ -1,4 +1,4 @@ -import { Tab, Nav, Dropdown } from "react-bootstrap"; +import { Button, Tab, Nav, Dropdown } from "react-bootstrap"; import React, { useEffect, useMemo, useState } from "react"; import { useParams, useHistory, Link } from "react-router-dom"; import { FormattedMessage, useIntl } from "react-intl"; @@ -9,14 +9,13 @@ import { useFindGallery, useGalleryUpdate, } from "src/core/StashService"; -import { - ErrorMessage, - LoadingIndicator, - Icon, - Counter, -} from "src/components/Shared"; +import { ErrorMessage } from "src/components/Shared/ErrorMessage"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { Icon } from "src/components/Shared/Icon"; +import { Counter } from "src/components/Shared/Counter"; import Mousetrap from "mousetrap"; -import { useToast } from "src/hooks"; +import { useGalleryLightbox } from "src/hooks/Lightbox/hooks"; +import { useToast } from "src/hooks/Toast"; import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton"; import { GalleryEditPanel } from "./GalleryEditPanel"; import { GalleryDetailPanel } from "./GalleryDetailPanel"; @@ -27,6 +26,7 @@ import { GalleryFileInfoPanel } from "./GalleryFileInfoPanel"; import { GalleryScenesPanel } from "./GalleryScenesPanel"; import { faEllipsisV } from "@fortawesome/free-solid-svg-icons"; import { galleryPath, galleryTitle } from "src/core/galleries"; +import { GalleryChapterPanel } from "./GalleryChaptersPanel"; interface IProps { gallery: GQL.GalleryDataFragment; @@ -41,6 +41,9 @@ export const GalleryPage: React.FC = ({ gallery }) => { const history = useHistory(); const Toast = useToast(); const intl = useIntl(); + const showLightbox = useGalleryLightbox(gallery.id, gallery.chapters); + + const [collapsed, setCollapsed] = useState(false); const [activeTabKey, setActiveTabKey] = useState("gallery-details-panel"); const activeRightTabKey = tab === "images" || tab === "add" ? tab : "images"; @@ -75,6 +78,10 @@ export const GalleryPage: React.FC = ({ gallery }) => { } }; + function getCollapseButtonText() { + return collapsed ? ">" : "<"; + } + async function onRescan() { if (!gallery || !path) { return; @@ -95,6 +102,10 @@ export const GalleryPage: React.FC = ({ gallery }) => { }); } + async function onClickChapter(imageindex: number) { + showLightbox(imageindex - 1); + } + const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); function onDeleteDialogClosed(deleted: boolean) { @@ -185,6 +196,11 @@ export const GalleryPage: React.FC = ({ gallery }) => { ) : undefined} + + + + + @@ -211,10 +227,16 @@ export const GalleryPage: React.FC = ({ gallery }) => { > + + + setIsDeleteAlertOpen(true)} /> @@ -257,10 +279,16 @@ export const GalleryPage: React.FC = ({ gallery }) => { - + - + @@ -270,13 +298,17 @@ export const GalleryPage: React.FC = ({ gallery }) => { // set up hotkeys useEffect(() => { Mousetrap.bind("a", () => setActiveTabKey("gallery-details-panel")); + Mousetrap.bind("c", () => setActiveTabKey("gallery-chapter-panel")); Mousetrap.bind("e", () => setActiveTabKey("gallery-edit-panel")); Mousetrap.bind("f", () => setActiveTabKey("gallery-file-info-panel")); + Mousetrap.bind(",", () => setCollapsed(!collapsed)); return () => { Mousetrap.unbind("a"); + Mousetrap.unbind("c"); Mousetrap.unbind("e"); Mousetrap.unbind("f"); + Mousetrap.unbind(","); }; }); @@ -288,7 +320,7 @@ export const GalleryPage: React.FC = ({ gallery }) => { {title} {maybeRenderDeleteDialog()} -
+
{gallery.studio && (

@@ -305,7 +337,14 @@ export const GalleryPage: React.FC = ({ gallery }) => {

{renderTabs()}
-
{renderRightTabs()}
+
+ +
+
+ {renderRightTabs()} +
); }; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx index 04a9db305..b423b1104 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx @@ -3,18 +3,22 @@ import * as GQL from "src/core/generated-graphql"; import { GalleriesCriterion } from "src/models/list-filter/criteria/galleries"; import { ListFilterModel } from "src/models/list-filter/filter"; import { ImageList } from "src/components/Images/ImageList"; -import { showWhenSelected } from "src/hooks/ListHook"; +import { showWhenSelected } from "src/components/List/ItemList"; import { mutateAddGalleryImages } from "src/core/StashService"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; import { useIntl } from "react-intl"; import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { galleryTitle } from "src/core/galleries"; interface IGalleryAddProps { + active: boolean; gallery: GQL.GalleryDataFragment; } -export const GalleryAddPanel: React.FC = ({ gallery }) => { +export const GalleryAddPanel: React.FC = ({ + active, + gallery, +}) => { const Toast = useToast(); const intl = useIntl(); @@ -93,6 +97,10 @@ export const GalleryAddPanel: React.FC = ({ gallery }) => { ]; return ( - + ); }; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryChapterForm.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryChapterForm.tsx new file mode 100644 index 000000000..a8c7133d7 --- /dev/null +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryChapterForm.tsx @@ -0,0 +1,158 @@ +import React from "react"; +import { Button, Form } from "react-bootstrap"; +import { FormattedMessage, useIntl } from "react-intl"; +import { Form as FormikForm, Formik } from "formik"; +import * as yup from "yup"; +import * as GQL from "src/core/generated-graphql"; +import { + useGalleryChapterCreate, + useGalleryChapterUpdate, + useGalleryChapterDestroy, +} from "src/core/StashService"; +import { useToast } from "src/hooks/Toast"; +import isEqual from "lodash-es/isEqual"; + +interface IFormFields { + title: string; + imageIndex: number; +} + +interface IGalleryChapterForm { + galleryID: string; + editingChapter?: GQL.GalleryChapterDataFragment; + onClose: () => void; +} + +export const GalleryChapterForm: React.FC = ({ + galleryID, + editingChapter, + onClose, +}) => { + const intl = useIntl(); + + const [galleryChapterCreate] = useGalleryChapterCreate(); + const [galleryChapterUpdate] = useGalleryChapterUpdate(); + const [galleryChapterDestroy] = useGalleryChapterDestroy(); + const Toast = useToast(); + + const schema = yup.object({ + title: yup.string().ensure(), + imageIndex: yup + .number() + .required() + .label(intl.formatMessage({ id: "image_index" })) + .moreThan(0), + }); + + const onSubmit = (values: IFormFields) => { + const variables: + | GQL.GalleryChapterUpdateInput + | GQL.GalleryChapterCreateInput = { + title: values.title, + image_index: values.imageIndex, + gallery_id: galleryID, + }; + + if (!editingChapter) { + galleryChapterCreate({ variables }) + .then(onClose) + .catch((err) => Toast.error(err)); + } else { + const updateVariables = variables as GQL.GalleryChapterUpdateInput; + updateVariables.id = editingChapter!.id; + galleryChapterUpdate({ variables: updateVariables }) + .then(onClose) + .catch((err) => Toast.error(err)); + } + }; + + const onDelete = () => { + if (!editingChapter) return; + + galleryChapterDestroy({ variables: { id: editingChapter.id } }) + .then(onClose) + .catch((err) => Toast.error(err)); + }; + + const values: IFormFields = { + title: editingChapter?.title ?? "", + imageIndex: editingChapter?.image_index ?? 1, + }; + + return ( + + {(formik) => ( + +
+ + + + + + + + {formik.getFieldMeta("title").error} + + + + + + + + + + + {formik.getFieldMeta("imageIndex").error} + + +
+
+
+ + + {editingChapter && ( + + )} +
+
+
+ )} +
+ ); +}; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryChaptersPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryChaptersPanel.tsx new file mode 100644 index 000000000..ae5021c39 --- /dev/null +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryChaptersPanel.tsx @@ -0,0 +1,72 @@ +import React, { useState, useEffect } from "react"; +import { Button } from "react-bootstrap"; +import { FormattedMessage } from "react-intl"; +import Mousetrap from "mousetrap"; +import * as GQL from "src/core/generated-graphql"; +import { ChapterEntries } from "./ChapterEntry"; +import { GalleryChapterForm } from "./GalleryChapterForm"; + +interface IGalleryChapterPanelProps { + gallery: GQL.GalleryDataFragment; + isVisible: boolean; + onClickChapter: (index: number) => void; +} + +export const GalleryChapterPanel: React.FC = ( + props: IGalleryChapterPanelProps +) => { + const [isEditorOpen, setIsEditorOpen] = useState(false); + const [editingChapter, setEditingChapter] = + useState(); + + // set up hotkeys + useEffect(() => { + if (props.isVisible) { + Mousetrap.bind("n", () => onOpenEditor()); + + return () => { + Mousetrap.unbind("n"); + }; + } + }); + + function onOpenEditor(chapter?: GQL.GalleryChapterDataFragment) { + setIsEditorOpen(true); + setEditingChapter(chapter ?? undefined); + } + + function onClickChapter(image_index: number) { + props.onClickChapter(image_index); + } + + const closeEditor = () => { + setEditingChapter(undefined); + setIsEditorOpen(false); + }; + + if (isEditorOpen) + return ( + + ); + + return ( +
+ +
+ +
+
+ ); +}; + +export default GalleryChapterPanel; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx index 09ad8d17f..62e80e23e 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx @@ -1,18 +1,15 @@ -import React from "react"; +import React, { useMemo } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useLocation } from "react-router-dom"; import { GalleryEditPanel } from "./GalleryEditPanel"; const GalleryCreate: React.FC = () => { const intl = useIntl(); - - function useQuery() { - const { search } = useLocation(); - return React.useMemo(() => new URLSearchParams(search), [search]); - } - - const query = useQuery(); - const nameQuery = query.get("name"); + const location = useLocation(); + const query = useMemo(() => new URLSearchParams(location.search), [location]); + const gallery = { + title: query.get("q") ?? undefined, + }; return (
@@ -23,12 +20,7 @@ const GalleryCreate: React.FC = () => { values={{ entityType: intl.formatMessage({ id: "gallery" }) }} /> - {}} - /> + {}} />
); diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx index c2cfece58..463ced506 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx @@ -2,8 +2,9 @@ import React from "react"; import { Link } from "react-router-dom"; import { FormattedDate, FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; -import { TextUtils } from "src/utils"; -import { TagLink, TruncatedText } from "src/components/Shared"; +import TextUtils from "src/utils/text"; +import { TagLink } from "src/components/Shared/TagLink"; +import { TruncatedText } from "src/components/Shared/TruncatedText"; import { PerformerCard } from "src/components/Performers/PerformerCard"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { sortPerformers } from "src/core/performers"; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx index ed85d9309..d560127dc 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx @@ -25,36 +25,33 @@ import { TagSelect, SceneSelect, StudioSelect, - Icon, - LoadingIndicator, - URLField, -} from "src/components/Shared"; -import { useToast } from "src/hooks"; +} from "src/components/Shared/Select"; +import { Icon } from "src/components/Shared/Icon"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { URLField } from "src/components/Shared/URLField"; +import { useToast } from "src/hooks/Toast"; import { useFormik } from "formik"; -import { FormUtils } from "src/utils"; +import FormUtils from "src/utils/form"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { GalleryScrapeDialog } from "./GalleryScrapeDialog"; import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; import { galleryTitle } from "src/core/galleries"; +import { useRatingKeybinds } from "src/hooks/keybinds"; +import { ConfigurationContext } from "src/hooks/Config"; +import isEqual from "lodash-es/isEqual"; +import { DateInput } from "src/components/Shared/DateInput"; interface IProps { + gallery: Partial; isVisible: boolean; onDelete: () => void; } -interface INewProps { - isNew: true; - gallery?: Partial; -} - -interface IExistingProps { - isNew: false; - gallery: GQL.GalleryDataFragment; -} - -export const GalleryEditPanel: React.FC< - IProps & (INewProps | IExistingProps) -> = ({ gallery, isNew, isVisible, onDelete }) => { +export const GalleryEditPanel: React.FC = ({ + gallery, + isVisible, + onDelete, +}) => { const intl = useIntl(); const Toast = useToast(); const history = useHistory(); @@ -65,13 +62,14 @@ export const GalleryEditPanel: React.FC< })) ); + const isNew = gallery.id === undefined; + const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const Scrapers = useListGalleryScrapers(); const [queryableScrapers, setQueryableScrapers] = useState([]); - const [ - scrapedGallery, - setScrapedGallery, - ] = useState(); + const [scrapedGallery, setScrapedGallery] = + useState(); // Network state const [isLoading, setIsLoading] = useState(false); @@ -83,37 +81,48 @@ export const GalleryEditPanel: React.FC< isNew || (gallery?.files?.length === 0 && !gallery?.folder); const schema = yup.object({ - title: titleRequired - ? yup.string().required() - : yup.string().optional().nullable(), - details: yup.string().optional().nullable(), - url: yup.string().optional().nullable(), - date: yup.string().optional().nullable(), - rating100: yup.number().optional().nullable(), - studio_id: yup.string().optional().nullable(), - performer_ids: yup.array(yup.string().required()).optional().nullable(), - tag_ids: yup.array(yup.string().required()).optional().nullable(), - scene_ids: yup.array(yup.string().required()).optional().nullable(), + title: titleRequired ? yup.string().required() : yup.string().ensure(), + url: yup.string().ensure(), + date: yup + .string() + .ensure() + .test({ + 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; + return true; + }, + message: intl.formatMessage({ id: "validation.date_invalid_form" }), + }), + rating100: yup.number().nullable().defined(), + studio_id: yup.string().required().nullable(), + performer_ids: yup.array(yup.string().required()).defined(), + tag_ids: yup.array(yup.string().required()).defined(), + scene_ids: yup.array(yup.string().required()).defined(), + details: yup.string().ensure(), }); const initialValues = { title: gallery?.title ?? "", - details: gallery?.details ?? "", url: gallery?.url ?? "", date: gallery?.date ?? "", rating100: gallery?.rating100 ?? null, - studio_id: gallery?.studio?.id, + studio_id: gallery?.studio?.id ?? null, performer_ids: (gallery?.performers ?? []).map((p) => p.id), tag_ids: (gallery?.tags ?? []).map((t) => t.id), scene_ids: (gallery?.scenes ?? []).map((s) => s.id), + details: gallery?.details ?? "", }; - type InputValues = typeof initialValues; + type InputValues = yup.InferType; - const formik = useFormik({ + const formik = useFormik({ initialValues, + enableReinitialize: true, validationSchema: schema, - onSubmit: (values) => onSave(getGalleryInput(values)), + onSubmit: (values) => onSave(values), }); function setRating(v: number) { @@ -133,6 +142,12 @@ export const GalleryEditPanel: React.FC< ); } + useRatingKeybinds( + isVisible, + stashConfig?.ui?.ratingSystemOptions?.type, + setRating + ); + useEffect(() => { if (isVisible) { Mousetrap.bind("s s", () => { @@ -142,35 +157,9 @@ export const GalleryEditPanel: React.FC< onDelete(); }); - // numeric keypresses get caught by jwplayer, so blur the element - // if the rating sequence is started - Mousetrap.bind("r", () => { - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); - } - - Mousetrap.bind("0", () => setRating(NaN)); - Mousetrap.bind("1", () => setRating(20)); - Mousetrap.bind("2", () => setRating(40)); - Mousetrap.bind("3", () => setRating(60)); - Mousetrap.bind("4", () => setRating(80)); - Mousetrap.bind("5", () => setRating(100)); - - setTimeout(() => { - Mousetrap.unbind("0"); - Mousetrap.unbind("1"); - Mousetrap.unbind("2"); - Mousetrap.unbind("3"); - Mousetrap.unbind("4"); - Mousetrap.unbind("5"); - }, 1000); - }); - return () => { Mousetrap.unbind("s s"); Mousetrap.unbind("d d"); - - Mousetrap.unbind("r"); }; } }); @@ -185,24 +174,13 @@ export const GalleryEditPanel: React.FC< setQueryableScrapers(newQueryableScrapers); }, [Scrapers]); - function getGalleryInput( - input: InputValues - ): GQL.GalleryCreateInput | GQL.GalleryUpdateInput { - return { - id: isNew ? undefined : gallery?.id ?? "", - ...input, - }; - } - - async function onSave( - input: GQL.GalleryCreateInput | GQL.GalleryUpdateInput - ) { + async function onSave(input: GQL.GalleryCreateInput) { setIsLoading(true); try { if (isNew) { const result = await createGallery({ variables: { - input: input as GQL.GalleryCreateInput, + input, }, }); if (result.data?.galleryCreate) { @@ -221,7 +199,10 @@ export const GalleryEditPanel: React.FC< } else { const result = await updateGallery({ variables: { - input: input as GQL.GalleryUpdateInput, + input: { + id: gallery.id!, + ...input, + }, }, }); if (result.data?.galleryUpdate) { @@ -235,7 +216,7 @@ export const GalleryEditPanel: React.FC< } ), }); - formik.resetForm({ values: formik.values }); + formik.resetForm(); } } } catch (e) { @@ -290,7 +271,10 @@ export const GalleryEditPanel: React.FC< return; } - const currentGallery = getGalleryInput(formik.values); + const currentGallery = { + id: gallery.id!, + ...formik.values, + }; return ( + {FormUtils.renderLabel({ title, })} @@ -438,7 +422,9 @@ export const GalleryEditPanel: React.FC<
+ {props.image.date ? ( +
+ +
+ ) : undefined} {props.image.rating100 ? (
:{" "} @@ -99,6 +112,7 @@ export const ImageDetailPanel: React.FC = (props) => { ) : ( "" )} + {renderGalleries()} {file?.width && file?.height ? (
diff --git a/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx b/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx index b16da38b5..96ace1609 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx @@ -9,13 +9,18 @@ import { PerformerSelect, TagSelect, StudioSelect, - LoadingIndicator, -} from "src/components/Shared"; -import { useToast } from "src/hooks"; -import { FormUtils } from "src/utils"; +} from "src/components/Shared/Select"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { URLField } from "src/components/Shared/URLField"; +import { useToast } from "src/hooks/Toast"; +import FormUtils from "src/utils/form"; import { useFormik } from "formik"; import { Prompt } from "react-router-dom"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; +import { useRatingKeybinds } from "src/hooks/keybinds"; +import { ConfigurationContext } from "src/hooks/Config"; +import isEqual from "lodash-es/isEqual"; +import { DateInput } from "src/components/Shared/DateInput"; interface IProps { image: GQL.ImageDataFragment; @@ -34,36 +39,61 @@ export const ImageEditPanel: React.FC = ({ // Network state const [isLoading, setIsLoading] = useState(false); + const { configuration } = React.useContext(ConfigurationContext); + const [updateImage] = useImageUpdate(); const schema = yup.object({ - title: yup.string().optional().nullable(), - rating100: yup.number().optional().nullable(), - studio_id: yup.string().optional().nullable(), - performer_ids: yup.array(yup.string().required()).optional().nullable(), - tag_ids: yup.array(yup.string().required()).optional().nullable(), + title: yup.string().ensure(), + url: yup.string().ensure(), + date: yup + .string() + .ensure() + .test({ + 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; + return true; + }, + message: intl.formatMessage({ id: "validation.date_invalid_form" }), + }), + rating100: yup.number().nullable().defined(), + studio_id: yup.string().required().nullable(), + performer_ids: yup.array(yup.string().required()).defined(), + tag_ids: yup.array(yup.string().required()).defined(), }); const initialValues = { title: image.title ?? "", + url: image?.url ?? "", + date: image?.date ?? "", rating100: image.rating100 ?? null, - studio_id: image.studio?.id, + studio_id: image.studio?.id ?? null, performer_ids: (image.performers ?? []).map((p) => p.id), tag_ids: (image.tags ?? []).map((t) => t.id), }; - type InputValues = typeof initialValues; + type InputValues = yup.InferType; - const formik = useFormik({ + const formik = useFormik({ initialValues, + enableReinitialize: true, validationSchema: schema, - onSubmit: (values) => onSave(getImageInput(values)), + onSubmit: (values) => onSave(values), }); function setRating(v: number) { formik.setFieldValue("rating100", v); } + useRatingKeybinds( + true, + configuration?.ui?.ratingSystemOptions?.type, + setRating + ); + useEffect(() => { if (isVisible) { Mousetrap.bind("s s", () => { @@ -73,52 +103,22 @@ export const ImageEditPanel: React.FC = ({ onDelete(); }); - // numeric keypresses get caught by jwplayer, so blur the element - // if the rating sequence is started - Mousetrap.bind("r", () => { - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); - } - - Mousetrap.bind("0", () => setRating(NaN)); - Mousetrap.bind("1", () => setRating(20)); - Mousetrap.bind("2", () => setRating(40)); - Mousetrap.bind("3", () => setRating(60)); - Mousetrap.bind("4", () => setRating(80)); - Mousetrap.bind("5", () => setRating(100)); - - setTimeout(() => { - Mousetrap.unbind("0"); - Mousetrap.unbind("1"); - Mousetrap.unbind("2"); - Mousetrap.unbind("3"); - Mousetrap.unbind("4"); - Mousetrap.unbind("5"); - }, 1000); - }); - return () => { Mousetrap.unbind("s s"); Mousetrap.unbind("d d"); - - Mousetrap.unbind("r"); }; } }); - function getImageInput(input: InputValues): GQL.ImageUpdateInput { - return { - id: image.id, - ...input, - }; - } - - async function onSave(input: GQL.ImageUpdateInput) { + async function onSave(input: InputValues) { setIsLoading(true); try { const result = await updateImage({ variables: { - input, + input: { + id: image.id, + ...input, + }, }, }); if (result.data?.imageUpdate) { @@ -128,7 +128,7 @@ export const ImageEditPanel: React.FC = ({ { entity: intl.formatMessage({ id: "image" }).toLocaleLowerCase() } ), }); - formik.resetForm({ values: formik.values }); + formik.resetForm(); } } catch (e) { Toast.error(e); @@ -138,7 +138,7 @@ export const ImageEditPanel: React.FC = ({ function renderTextField(field: string, title: string, placeholder?: string) { return ( - + {FormUtils.renderLabel({ title, })} @@ -172,7 +172,7 @@ export const ImageEditPanel: React.FC = ({ - - - - ); -}; diff --git a/ui/v2.5/src/components/List/CriterionEditor.tsx b/ui/v2.5/src/components/List/CriterionEditor.tsx new file mode 100644 index 000000000..7e7e5c636 --- /dev/null +++ b/ui/v2.5/src/components/List/CriterionEditor.tsx @@ -0,0 +1,226 @@ +import cloneDeep from "lodash-es/cloneDeep"; +import React, { useCallback, useMemo } from "react"; +import { Form } from "react-bootstrap"; +import { CriterionModifier } from "src/core/generated-graphql"; +import { + DurationCriterion, + CriterionValue, + Criterion, + IHierarchicalLabeledIdCriterion, + NumberCriterion, + ILabeledIdCriterion, + DateCriterion, + TimestampCriterion, + BooleanCriterion, + PathCriterionOption, +} from "src/models/list-filter/criteria/criterion"; +import { useIntl } from "react-intl"; +import { + criterionIsHierarchicalLabelValue, + criterionIsNumberValue, + criterionIsStashIDValue, + criterionIsDateValue, + criterionIsTimestampValue, +} from "src/models/list-filter/types"; +import { DurationFilter } from "./Filters/DurationFilter"; +import { NumberFilter } from "./Filters/NumberFilter"; +import { LabeledIdFilter } from "./Filters/LabeledIdFilter"; +import { HierarchicalLabelValueFilter } from "./Filters/HierarchicalLabelValueFilter"; +import { InputFilter } from "./Filters/InputFilter"; +import { DateFilter } from "./Filters/DateFilter"; +import { TimestampFilter } from "./Filters/TimestampFilter"; +import { CountryCriterion } from "src/models/list-filter/criteria/country"; +import { CountrySelect } from "../Shared/CountrySelect"; +import { StashIDCriterion } from "src/models/list-filter/criteria/stash-ids"; +import { StashIDFilter } from "./Filters/StashIDFilter"; +import { RatingCriterion } from "../../models/list-filter/criteria/rating"; +import { RatingFilter } from "./Filters/RatingFilter"; +import { BooleanFilter } from "./Filters/BooleanFilter"; +import { OptionsListFilter } from "./Filters/OptionsListFilter"; +import { PathFilter } from "./Filters/PathFilter"; + +interface IGenericCriterionEditor { + criterion: Criterion; + setCriterion: (c: Criterion) => void; +} + +const GenericCriterionEditor: React.FC = ({ + criterion, + setCriterion, +}) => { + const intl = useIntl(); + + const { options, modifierOptions } = criterion.criterionOption; + + const onChangedModifierSelect = useCallback( + (event: React.ChangeEvent) => { + const newCriterion = cloneDeep(criterion); + newCriterion.modifier = event.target.value as CriterionModifier; + setCriterion(newCriterion); + }, + [criterion, setCriterion] + ); + + const modifierSelector = useMemo(() => { + if (!modifierOptions || modifierOptions.length === 0) { + return; + } + + return ( + + {modifierOptions.map((c) => ( + + ))} + + ); + }, [modifierOptions, onChangedModifierSelect, criterion.modifier, intl]); + + const valueControl = useMemo(() => { + function onValueChanged(value: CriterionValue) { + const newCriterion = cloneDeep(criterion); + newCriterion.value = value; + setCriterion(newCriterion); + } + + // always show stashID filter + if (criterion instanceof StashIDCriterion) { + return ( + + ); + } + + // Hide the value select if the modifier is "IsNull" or "NotNull" + if ( + criterion.modifier === CriterionModifier.IsNull || + criterion.modifier === CriterionModifier.NotNull + ) { + return; + } + + if (criterion instanceof ILabeledIdCriterion) { + return ( + + ); + } + if (criterion instanceof IHierarchicalLabeledIdCriterion) { + return ( + + ); + } + if ( + options && + !criterionIsHierarchicalLabelValue(criterion.value) && + !criterionIsNumberValue(criterion.value) && + !criterionIsStashIDValue(criterion.value) && + !criterionIsDateValue(criterion.value) && + !criterionIsTimestampValue(criterion.value) && + !Array.isArray(criterion.value) + ) { + // if (!modifierOptions || modifierOptions.length === 0) { + return ( + + ); + // } + + // return ( + // + // ); + } + if (criterion.criterionOption instanceof PathCriterionOption) { + return ( + + ); + } + if (criterion instanceof DurationCriterion) { + return ( + + ); + } + if (criterion instanceof DateCriterion) { + return ( + + ); + } + if (criterion instanceof TimestampCriterion) { + return ( + + ); + } + if (criterion instanceof NumberCriterion) { + return ( + + ); + } + if (criterion instanceof RatingCriterion) { + return ( + + ); + } + if ( + criterion instanceof CountryCriterion && + (criterion.modifier === CriterionModifier.Equals || + criterion.modifier === CriterionModifier.NotEquals) + ) { + return ( + onValueChanged(v)} + menuPortalTarget={document.body} + /> + ); + } + return ( + + ); + }, [criterion, setCriterion, options]); + + return ( +
+ {modifierSelector} + {valueControl} +
+ ); +}; + +interface ICriterionEditor { + criterion: Criterion; + setCriterion: (c: Criterion) => void; +} + +export const CriterionEditor: React.FC = ({ + criterion, + setCriterion, +}) => { + const filterControl = useMemo(() => { + if (criterion instanceof BooleanCriterion) { + return ( + + ); + } + + return ( + + ); + }, [criterion, setCriterion]); + + return
{filterControl}
; +}; diff --git a/ui/v2.5/src/components/List/EditFilterDialog.tsx b/ui/v2.5/src/components/List/EditFilterDialog.tsx new file mode 100644 index 000000000..7097bc1d8 --- /dev/null +++ b/ui/v2.5/src/components/List/EditFilterDialog.tsx @@ -0,0 +1,340 @@ +import cloneDeep from "lodash-es/cloneDeep"; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { Accordion, Button, Card, Modal } from "react-bootstrap"; +import cx from "classnames"; +import { + CriterionValue, + Criterion, + CriterionOption, +} from "src/models/list-filter/criteria/criterion"; +import { makeCriteria } from "src/models/list-filter/criteria/factory"; +import { FormattedMessage, useIntl } from "react-intl"; +import { ConfigurationContext } from "src/hooks/Config"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { getFilterOptions } from "src/models/list-filter/factory"; +import { FilterTags } from "./FilterTags"; +import { CriterionEditor } from "./CriterionEditor"; +import { Icon } from "../Shared/Icon"; +import { + faChevronDown, + faChevronRight, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; +import { useCompare, usePrevious } from "src/hooks/state"; +import { CriterionType } from "src/models/list-filter/types"; + +interface ICriterionList { + criteria: string[]; + currentCriterion?: Criterion; + setCriterion: (c: Criterion) => void; + criterionOptions: CriterionOption[]; + selected?: CriterionOption; + optionSelected: (o?: CriterionOption) => void; + onRemoveCriterion: (c: string) => void; +} + +const CriterionOptionList: React.FC = ({ + criteria, + currentCriterion, + setCriterion, + criterionOptions, + selected, + optionSelected, + onRemoveCriterion, +}) => { + const prevCriterion = usePrevious(currentCriterion); + + const scrolled = useRef(false); + + const type = currentCriterion?.criterionOption.type; + const prevType = prevCriterion?.criterionOption.type; + + const criteriaRefs = useMemo(() => { + const refs: Record> = {}; + criterionOptions.forEach((c) => { + refs[c.type] = React.createRef(); + }); + return refs; + }, [criterionOptions]); + + function onSelect(k: string | null) { + if (!k) { + optionSelected(undefined); + return; + } + const option = criterionOptions.find((c) => c.type === k); + + if (option) { + optionSelected(option); + } + } + + useEffect(() => { + // 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) { + criteriaRefs[type].current!.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + scrolled.current = true; + } + }, [currentCriterion, criteriaRefs, type]); + + function getReleventCriterion(t: CriterionType) { + if (currentCriterion?.criterionOption.type === t) { + return currentCriterion; + } + + return prevCriterion; + } + + function removeClicked(ev: React.MouseEvent, t: string) { + // needed to prevent the nav item from being selected + ev.stopPropagation(); + ev.preventDefault(); + onRemoveCriterion(t); + } + + return ( + + {criterionOptions.map((c) => ( + + + + + + + {criteria.some((cc) => c.type === cc) && ( + + )} + + + {(type === c.type && currentCriterion) || + (prevType === c.type && prevCriterion) ? ( + + + + ) : ( + + )} + + + ))} + + ); +}; + +interface IEditFilterProps { + filter: ListFilterModel; + editingCriterion?: string; + onApply: (filter: ListFilterModel) => void; + onCancel: () => void; +} + +export const EditFilterDialog: React.FC = ({ + filter, + editingCriterion, + onApply, + onCancel, +}) => { + const intl = useIntl(); + + const { configuration: config } = useContext(ConfigurationContext); + + const [currentFilter, setCurrentFilter] = useState( + cloneDeep(filter) + ); + const [criterion, setCriterion] = useState>(); + + const { criteria } = currentFilter; + + const criteriaList = useMemo(() => { + return criteria.map((c) => c.criterionOption.type); + }, [criteria]); + + const filterOptions = useMemo(() => { + return getFilterOptions(currentFilter.mode); + }, [currentFilter.mode]); + + const criterionOptions = useMemo(() => { + const filteredOptions = filterOptions.criterionOptions.filter((o) => { + return o.type !== "none"; + }); + + filteredOptions.sort((a, b) => { + return intl + .formatMessage({ id: a.messageID }) + .localeCompare(intl.formatMessage({ id: b.messageID })); + }); + + return filteredOptions; + }, [intl, filterOptions.criterionOptions]); + + const optionSelected = useCallback( + (option?: CriterionOption) => { + if (!option) { + setCriterion(undefined); + return; + } + + // find the existing criterion if present + const existing = criteria.find( + (c) => c.criterionOption.type === option.type + ); + if (existing) { + setCriterion(existing); + } else { + const newCriterion = makeCriteria(config, option.type); + setCriterion(newCriterion); + } + }, + [criteria, config] + ); + + const editingCriterionChanged = useCompare(editingCriterion); + + useEffect(() => { + if (editingCriterionChanged && editingCriterion) { + const option = criterionOptions.find((c) => c.type === editingCriterion); + if (option) { + optionSelected(option); + } + } + }, [ + editingCriterion, + criterionOptions, + optionSelected, + editingCriterionChanged, + ]); + + function replaceCriterion(c: Criterion) { + const newFilter = cloneDeep(currentFilter); + + if (!c.isValid()) { + // remove from the filter if present + const newCriteria = criteria.filter((cc) => { + return cc.criterionOption.type !== c.criterionOption.type; + }); + + newFilter.criteria = newCriteria; + } else { + let found = false; + + const newCriteria = criteria.map((cc) => { + if (cc.criterionOption.type === c.criterionOption.type) { + found = true; + return c; + } + + return cc; + }); + + if (!found) { + newCriteria.push(c); + } + + newFilter.criteria = newCriteria; + } + + setCurrentFilter(newFilter); + setCriterion(c); + } + + function removeCriterion(c: Criterion) { + const newFilter = cloneDeep(currentFilter); + + const newCriteria = criteria.filter((cc) => { + return cc.getId() !== c.getId(); + }); + + newFilter.criteria = newCriteria; + + setCurrentFilter(newFilter); + if (criterion?.getId() === c.getId()) { + optionSelected(undefined); + } + } + + function removeCriterionString(c: string) { + const cc = criteria.find((ccc) => ccc.criterionOption.type === c); + if (cc) { + removeCriterion(cc); + } + } + + function onClearAll() { + const newFilter = cloneDeep(currentFilter); + newFilter.criteria = []; + setCurrentFilter(newFilter); + } + + return ( + <> + onCancel()} className="edit-filter-dialog"> + + + + +
+ removeCriterionString(c)} + /> + {criteria.length > 0 && ( +
+ optionSelected(c.criterionOption)} + onRemoveCriterion={(c) => removeCriterion(c)} + onRemoveAll={() => onClearAll()} + /> +
+ )} +
+
+ + + + +
+ + ); +}; diff --git a/ui/v2.5/src/components/List/FilterTags.tsx b/ui/v2.5/src/components/List/FilterTags.tsx index 48e79da98..779fa26be 100644 --- a/ui/v2.5/src/components/List/FilterTags.tsx +++ b/ui/v2.5/src/components/List/FilterTags.tsx @@ -4,20 +4,22 @@ import { Criterion, CriterionValue, } from "src/models/list-filter/criteria/criterion"; -import { useIntl } from "react-intl"; -import { Icon } from "../Shared"; +import { FormattedMessage, useIntl } from "react-intl"; +import { Icon } from "../Shared/Icon"; import { faTimes } from "@fortawesome/free-solid-svg-icons"; interface IFilterTagsProps { criteria: Criterion[]; onEditCriterion: (c: Criterion) => void; onRemoveCriterion: (c: Criterion) => void; + onRemoveAll: () => void; } export const FilterTags: React.FC = ({ criteria, onEditCriterion, onRemoveCriterion, + onRemoveAll, }) => { const intl = useIntl(); @@ -55,9 +57,26 @@ export const FilterTags: React.FC = ({ )); } + function maybeRenderClearAll() { + if (criteria.length < 3) { + return; + } + + return ( + + ); + } + return ( -
+
{renderFilterTags()} + {maybeRenderClearAll()}
); }; diff --git a/ui/v2.5/src/components/List/Filters/BooleanFilter.tsx b/ui/v2.5/src/components/List/Filters/BooleanFilter.tsx new file mode 100644 index 000000000..0a04a4fc6 --- /dev/null +++ b/ui/v2.5/src/components/List/Filters/BooleanFilter.tsx @@ -0,0 +1,45 @@ +import cloneDeep from "lodash-es/cloneDeep"; +import React from "react"; +import { Form } from "react-bootstrap"; +import { BooleanCriterion } from "src/models/list-filter/criteria/criterion"; +import { FormattedMessage } from "react-intl"; + +interface IBooleanFilter { + criterion: BooleanCriterion; + setCriterion: (c: BooleanCriterion) => void; +} + +export const BooleanFilter: React.FC = ({ + criterion, + setCriterion, +}) => { + function onSelect(v: boolean) { + const c = cloneDeep(criterion); + if ((v && c.value === "true") || (!v && c.value === "false")) { + c.value = ""; + } else { + c.value = v ? "true" : "false"; + } + + setCriterion(c); + } + + return ( +
+ onSelect(true)} + checked={criterion.value === "true"} + type="checkbox" + label={} + /> + onSelect(false)} + checked={criterion.value === "false"} + type="checkbox" + label={} + /> +
+ ); +}; diff --git a/ui/v2.5/src/components/List/Filters/DateFilter.tsx b/ui/v2.5/src/components/List/Filters/DateFilter.tsx index 9235b8f04..bbedfdfee 100644 --- a/ui/v2.5/src/components/List/Filters/DateFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/DateFilter.tsx @@ -1,9 +1,10 @@ -import React, { useRef } from "react"; +import React from "react"; import { Form } from "react-bootstrap"; import { useIntl } from "react-intl"; import { CriterionModifier } from "../../../core/generated-graphql"; import { IDateValue } from "../../../models/list-filter/types"; import { Criterion } from "../../../models/list-filter/criteria/criterion"; +import { DateInput } from "src/components/Shared/DateInput"; interface IDateFilterProps { criterion: Criterion; @@ -16,18 +17,13 @@ export const DateFilter: React.FC = ({ }) => { const intl = useIntl(); - const valueStage = useRef(criterion.value); + const { value } = criterion; - function onChanged( - event: React.ChangeEvent, - property: "value" | "value2" - ) { - const { value } = event.target; - valueStage.current[property] = value; - } + function onChanged(newValue: string, property: "value" | "value2") { + const valueCopy = { ...value }; - function onBlurInput() { - onValueChanged(valueStage.current); + valueCopy[property] = newValue; + onValueChanged(valueCopy); } let equalsControl: JSX.Element | null = null; @@ -37,17 +33,10 @@ export const DateFilter: React.FC = ({ ) { equalsControl = ( - ) => - onChanged(e, "value") - } - onBlur={onBlurInput} - defaultValue={criterion.value?.value ?? ""} - placeholder={ - intl.formatMessage({ id: "criterion.value" }) + " (YYYY-MM-DD)" - } + onChanged(v, "value")} + placeholder={intl.formatMessage({ id: "criterion.value" })} /> ); @@ -61,18 +50,10 @@ export const DateFilter: React.FC = ({ ) { lowerControl = ( - ) => - onChanged(e, "value") - } - onBlur={onBlurInput} - defaultValue={criterion.value?.value ?? ""} - placeholder={ - intl.formatMessage({ id: "criterion.greater_than" }) + - " (YYYY-MM-DD)" - } + onChanged(v, "value")} + placeholder={intl.formatMessage({ id: "criterion.greater_than" })} /> ); @@ -86,26 +67,21 @@ export const DateFilter: React.FC = ({ ) { upperControl = ( - ) => + onChanged( - e, + v, criterion.modifier === CriterionModifier.LessThan ? "value" : "value2" ) } - onBlur={onBlurInput} - defaultValue={ - (criterion.modifier === CriterionModifier.LessThan - ? criterion.value?.value - : criterion.value?.value2) ?? "" - } - placeholder={ - intl.formatMessage({ id: "criterion.less_than" }) + " (YYYY-MM-DD)" - } + placeholder={intl.formatMessage({ id: "criterion.less_than" })} /> ); diff --git a/ui/v2.5/src/components/List/Filters/DurationFilter.tsx b/ui/v2.5/src/components/List/Filters/DurationFilter.tsx index 3fffa954a..772bb0137 100644 --- a/ui/v2.5/src/components/List/Filters/DurationFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/DurationFilter.tsx @@ -1,10 +1,10 @@ import React from "react"; import { Form } from "react-bootstrap"; import { useIntl } from "react-intl"; -import { CriterionModifier } from "../../../core/generated-graphql"; -import { DurationInput } from "../../Shared"; -import { INumberValue } from "../../../models/list-filter/types"; -import { Criterion } from "../../../models/list-filter/criteria/criterion"; +import { CriterionModifier } from "src/core/generated-graphql"; +import { DurationInput } from "src/components/Shared/DurationInput"; +import { INumberValue } from "src/models/list-filter/types"; +import { Criterion } from "src/models/list-filter/criteria/criterion"; interface IDurationFilterProps { criterion: Criterion; diff --git a/ui/v2.5/src/components/List/Filters/FilterButton.tsx b/ui/v2.5/src/components/List/Filters/FilterButton.tsx new file mode 100644 index 000000000..0b4d4453d --- /dev/null +++ b/ui/v2.5/src/components/List/Filters/FilterButton.tsx @@ -0,0 +1,28 @@ +import React, { useMemo } from "react"; +import { Badge, Button } from "react-bootstrap"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { faFilter } from "@fortawesome/free-solid-svg-icons"; +import { Icon } from "src/components/Shared/Icon"; + +interface IFilterButtonProps { + filter: ListFilterModel; + onClick: () => void; +} + +export const FilterButton: React.FC = ({ + filter, + onClick, +}) => { + const count = useMemo(() => filter.count(), [filter]); + + return ( + + ); +}; diff --git a/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx b/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx index 52a3bae7e..bb2625838 100644 --- a/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx @@ -1,19 +1,18 @@ import React from "react"; import { Form } from "react-bootstrap"; import { defineMessages, MessageDescriptor, useIntl } from "react-intl"; -import { FilterSelect, ValidTypes } from "../../Shared"; -import { Criterion } from "../../../models/list-filter/criteria/criterion"; -import { IHierarchicalLabelValue } from "../../../models/list-filter/types"; +import { FilterSelect, SelectObject } from "src/components/Shared/Select"; +import { Criterion } from "src/models/list-filter/criteria/criterion"; +import { IHierarchicalLabelValue } from "src/models/list-filter/types"; interface IHierarchicalLabelValueFilterProps { criterion: Criterion; onValueChanged: (value: IHierarchicalLabelValue) => void; } -export const HierarchicalLabelValueFilter: React.FC = ({ - criterion, - onValueChanged, -}) => { +export const HierarchicalLabelValueFilter: React.FC< + IHierarchicalLabelValueFilterProps +> = ({ criterion, onValueChanged }) => { const intl = useIntl(); if ( @@ -36,11 +35,11 @@ export const HierarchicalLabelValueFilter: React.FC ({ id: i.id, - label: i.name!, + label: i.name ?? i.title ?? "", })); onValueChanged(value); } @@ -81,6 +80,7 @@ export const HierarchicalLabelValueFilter: React.FC labeled.id)} + menuPortalTarget={document.body} /> diff --git a/ui/v2.5/src/components/List/Filters/InputFilter.tsx b/ui/v2.5/src/components/List/Filters/InputFilter.tsx index 95e6ce15d..46ee7b6b0 100644 --- a/ui/v2.5/src/components/List/Filters/InputFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/InputFilter.tsx @@ -19,13 +19,15 @@ export const InputFilter: React.FC = ({ } return ( - - - + <> + + + + ); }; diff --git a/ui/v2.5/src/components/List/Filters/LabeledIdFilter.tsx b/ui/v2.5/src/components/List/Filters/LabeledIdFilter.tsx index 09245adb9..f06e5c21b 100644 --- a/ui/v2.5/src/components/List/Filters/LabeledIdFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/LabeledIdFilter.tsx @@ -1,8 +1,8 @@ import React from "react"; import { Form } from "react-bootstrap"; -import { FilterSelect, ValidTypes } from "../../Shared"; -import { Criterion } from "../../../models/list-filter/criteria/criterion"; -import { ILabeledId } from "../../../models/list-filter/types"; +import { FilterSelect, SelectObject } from "src/components/Shared/Select"; +import { Criterion } from "src/models/list-filter/criteria/criterion"; +import { ILabeledId } from "src/models/list-filter/types"; interface ILabeledIdFilterProps { criterion: Criterion; @@ -26,11 +26,11 @@ export const LabeledIdFilter: React.FC = ({ ) return null; - function onSelectionChanged(items: ValidTypes[]) { + function onSelectionChanged(items: SelectObject[]) { onValueChanged( items.map((i) => ({ id: i.id, - label: i.name!, + label: i.name ?? i.title ?? "", })) ); } @@ -42,6 +42,7 @@ export const LabeledIdFilter: React.FC = ({ isMulti onSelect={onSelectionChanged} ids={criterion.value.map((labeled) => labeled.id)} + menuPortalTarget={document.body} /> ); diff --git a/ui/v2.5/src/components/List/Filters/NumberFilter.tsx b/ui/v2.5/src/components/List/Filters/NumberFilter.tsx index c25ff9814..7aa574a2e 100644 --- a/ui/v2.5/src/components/List/Filters/NumberFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/NumberFilter.tsx @@ -3,10 +3,10 @@ import { Form } from "react-bootstrap"; import { useIntl } from "react-intl"; import { CriterionModifier } from "../../../core/generated-graphql"; import { INumberValue } from "../../../models/list-filter/types"; -import { Criterion } from "../../../models/list-filter/criteria/criterion"; +import { NumberCriterion } from "../../../models/list-filter/criteria/criterion"; interface IDurationFilterProps { - criterion: Criterion; + criterion: NumberCriterion; onValueChanged: (value: INumberValue) => void; } @@ -16,12 +16,14 @@ export const NumberFilter: React.FC = ({ }) => { const intl = useIntl(); + const { value } = criterion; + function onChanged( event: React.ChangeEvent, property: "value" | "value2" ) { const numericValue = parseInt(event.target.value, 10); - const valueCopy = { ...criterion.value }; + const valueCopy = { ...value }; valueCopy[property] = !Number.isNaN(numericValue) ? numericValue : 0; onValueChanged(valueCopy); @@ -40,7 +42,7 @@ export const NumberFilter: React.FC = ({ onChange={(e: React.ChangeEvent) => onChanged(e, "value") } - value={criterion.value?.value ?? ""} + value={value?.value ?? ""} placeholder={intl.formatMessage({ id: "criterion.value" })} /> @@ -61,7 +63,7 @@ export const NumberFilter: React.FC = ({ onChange={(e: React.ChangeEvent) => onChanged(e, "value") } - value={criterion.value?.value ?? ""} + value={value?.value ?? ""} placeholder={intl.formatMessage({ id: "criterion.greater_than" })} /> @@ -89,8 +91,8 @@ export const NumberFilter: React.FC = ({ } value={ (criterion.modifier === CriterionModifier.LessThan - ? criterion.value?.value - : criterion.value?.value2) ?? "" + ? value?.value + : value?.value2) ?? "" } placeholder={intl.formatMessage({ id: "criterion.less_than" })} /> diff --git a/ui/v2.5/src/components/List/Filters/OptionsFilter.tsx b/ui/v2.5/src/components/List/Filters/OptionsFilter.tsx index c0d6baead..2f6f40bdc 100644 --- a/ui/v2.5/src/components/List/Filters/OptionsFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/OptionsFilter.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import { Form } from "react-bootstrap"; import { Criterion, @@ -18,16 +18,13 @@ export const OptionsFilter: React.FC = ({ onValueChanged(event.target.value); } - const options = criterion.criterionOption.options ?? []; + const options = useMemo(() => { + const ret = criterion.criterionOption.options?.slice() ?? []; - if ( - options && - (criterion.value === undefined || - criterion.value === "" || - typeof criterion.value === "number") - ) { - onValueChanged(options[0].toString()); - } + ret.unshift(""); + + return ret; + }, [criterion.criterionOption.options]); return ( @@ -39,7 +36,7 @@ export const OptionsFilter: React.FC = ({ > {options.map((c) => ( ))} diff --git a/ui/v2.5/src/components/List/Filters/OptionsListFilter.tsx b/ui/v2.5/src/components/List/Filters/OptionsListFilter.tsx new file mode 100644 index 000000000..b84cf8bd1 --- /dev/null +++ b/ui/v2.5/src/components/List/Filters/OptionsListFilter.tsx @@ -0,0 +1,45 @@ +import cloneDeep from "lodash-es/cloneDeep"; +import React from "react"; +import { Form } from "react-bootstrap"; +import { + CriterionValue, + Criterion, +} from "src/models/list-filter/criteria/criterion"; + +interface IOptionsListFilter { + criterion: Criterion; + setCriterion: (c: Criterion) => void; +} + +export const OptionsListFilter: React.FC = ({ + criterion, + setCriterion, +}) => { + function onSelect(v: string) { + const c = cloneDeep(criterion); + if (c.value === v) { + c.value = ""; + } else { + c.value = v; + } + + setCriterion(c); + } + + const { options } = criterion.criterionOption; + + return ( +
+ {options?.map((o) => ( + onSelect(o.toString())} + checked={criterion.value === o.toString()} + type="checkbox" + label={o.toString()} + /> + ))} +
+ ); +}; diff --git a/ui/v2.5/src/components/List/Filters/PathFilter.tsx b/ui/v2.5/src/components/List/Filters/PathFilter.tsx new file mode 100644 index 000000000..2ac345cc4 --- /dev/null +++ b/ui/v2.5/src/components/List/Filters/PathFilter.tsx @@ -0,0 +1,49 @@ +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 { + Criterion, + CriterionValue, +} from "../../../models/list-filter/criteria/criterion"; + +interface IInputFilterProps { + criterion: Criterion; + onValueChanged: (value: string) => void; +} + +export const PathFilter: React.FC = ({ + criterion, + onValueChanged, +}) => { + const { configuration } = React.useContext(ConfigurationContext); + const libraryPaths = configuration?.general.stashes.map((s) => s.path); + + // don't show folder select for regex + const regex = + criterion.modifier === CriterionModifier.MatchesRegex || + criterion.modifier === CriterionModifier.NotMatchesRegex; + + return ( + + {regex ? ( + onValueChanged(v.target.value)} + value={criterion.value ? criterion.value.toString() : ""} + /> + ) : ( + onValueChanged(v)} + collapsible + quoteSpaced + hideError + defaultDirectories={libraryPaths} + /> + )} + + ); +}; diff --git a/ui/v2.5/src/components/List/Filters/StashIDFilter.tsx b/ui/v2.5/src/components/List/Filters/StashIDFilter.tsx index 096f573b7..f28961bf5 100644 --- a/ui/v2.5/src/components/List/Filters/StashIDFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/StashIDFilter.tsx @@ -15,6 +15,7 @@ export const StashIDFilter: React.FC = ({ onValueChanged, }) => { const intl = useIntl(); + const { value } = criterion; function onEndpointChanged(event: React.ChangeEvent) { onValueChanged({ @@ -35,8 +36,8 @@ export const StashIDFilter: React.FC = ({ @@ -45,8 +46,8 @@ export const StashIDFilter: React.FC = ({ diff --git a/ui/v2.5/src/components/List/Filters/TimestampFilter.tsx b/ui/v2.5/src/components/List/Filters/TimestampFilter.tsx index de6eefb72..1cb25b7d5 100644 --- a/ui/v2.5/src/components/List/Filters/TimestampFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/TimestampFilter.tsx @@ -1,9 +1,10 @@ -import React, { useRef } from "react"; +import React from "react"; import { Form } from "react-bootstrap"; import { useIntl } from "react-intl"; import { CriterionModifier } from "../../../core/generated-graphql"; import { ITimestampValue } from "../../../models/list-filter/types"; import { Criterion } from "../../../models/list-filter/criteria/criterion"; +import { DateInput } from "src/components/Shared/DateInput"; interface ITimestampFilterProps { criterion: Criterion; @@ -16,18 +17,13 @@ export const TimestampFilter: React.FC = ({ }) => { const intl = useIntl(); - const valueStage = useRef(criterion.value); + const { value } = criterion; - function onChanged( - event: React.ChangeEvent, - property: "value" | "value2" - ) { - const { value } = event.target; - valueStage.current[property] = value; - } + function onChanged(newValue: string, property: "value" | "value2") { + const valueCopy = { ...value }; - function onBlurInput() { - onValueChanged(valueStage.current); + valueCopy[property] = newValue; + onValueChanged(valueCopy); } let equalsControl: JSX.Element | null = null; @@ -37,19 +33,24 @@ export const TimestampFilter: React.FC = ({ ) { equalsControl = ( - onChanged(v, "value")} + placeholder={intl.formatMessage({ id: "criterion.value" })} + isTime + /> + {/* ) => onChanged(e, "value") } - onBlur={onBlurInput} - defaultValue={criterion.value?.value ?? ""} + value={value?.value ?? ""} placeholder={ intl.formatMessage({ id: "criterion.value" }) + - " (YYYY-MM-DD HH-MM)" + " (YYYY-MM-DD HH:MM)" } - /> + /> */} ); } @@ -62,19 +63,24 @@ export const TimestampFilter: React.FC = ({ ) { lowerControl = ( - onChanged(v, "value")} + placeholder={intl.formatMessage({ id: "criterion.greater_than" })} + isTime + /> + {/* ) => onChanged(e, "value") } - onBlur={onBlurInput} - defaultValue={criterion.value?.value ?? ""} + value={value?.value ?? ""} placeholder={ intl.formatMessage({ id: "criterion.greater_than" }) + - " (YYYY-MM-DD HH-MM)" + " (YYYY-MM-DD HH:MM)" } - /> + /> */} ); } @@ -87,7 +93,24 @@ export const TimestampFilter: React.FC = ({ ) { upperControl = ( - + onChanged( + v, + criterion.modifier === CriterionModifier.LessThan + ? "value" + : "value2" + ) + } + placeholder={intl.formatMessage({ id: "criterion.less_than" })} + isTime + /> + {/* ) => @@ -98,17 +121,16 @@ export const TimestampFilter: React.FC = ({ : "value2" ) } - onBlur={onBlurInput} - defaultValue={ + value={ (criterion.modifier === CriterionModifier.LessThan - ? criterion.value?.value - : criterion.value?.value2) ?? "" + ? value?.value + : value?.value2) ?? "" } placeholder={ intl.formatMessage({ id: "criterion.less_than" }) + - " (YYYY-MM-DD HH-MM)" + " (YYYY-MM-DD HH:MM)" } - /> + /> */} ); } diff --git a/ui/v2.5/src/components/List/ItemList.tsx b/ui/v2.5/src/components/List/ItemList.tsx new file mode 100644 index 000000000..a4ef2a7ac --- /dev/null +++ b/ui/v2.5/src/components/List/ItemList.tsx @@ -0,0 +1,730 @@ +import React, { + useCallback, + useContext, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; +import clone from "lodash-es/clone"; +import cloneDeep from "lodash-es/cloneDeep"; +import isEqual from "lodash-es/isEqual"; +import Mousetrap from "mousetrap"; +import * as GQL from "src/core/generated-graphql"; +import { QueryResult } from "@apollo/client"; +import { + Criterion, + CriterionValue, +} from "src/models/list-filter/criteria/criterion"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import { useInterfaceLocalForage } from "src/hooks/LocalForage"; +import { useHistory, useLocation } from "react-router-dom"; +import { ConfigurationContext } from "src/hooks/Config"; +import { getFilterOptions } from "src/models/list-filter/factory"; +import { useFindDefaultFilter } from "src/core/StashService"; +import { Pagination, PaginationIndex } from "./Pagination"; +import { EditFilterDialog } from "src/components/List/EditFilterDialog"; +import { ListFilter } from "./ListFilter"; +import { FilterTags } from "./FilterTags"; +import { ListViewOptions } from "./ListViewOptions"; +import { ListOperationButtons } from "./ListOperationButtons"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; +import { DisplayMode } from "src/models/list-filter/types"; +import { ButtonToolbar } from "react-bootstrap"; + +export enum PersistanceLevel { + // do not load default query or persist display mode + NONE, + // load default query, don't load or persist display mode + ALL, + // load and persist display mode only + VIEW, +} + +interface IDataItem { + id: string; +} + +export interface IItemListOperation { + text: string; + onClick: ( + result: T, + filter: ListFilterModel, + selectedIds: Set + ) => Promise; + isDisplayed?: ( + result: T, + filter: ListFilterModel, + selectedIds: Set + ) => boolean; + postRefetch?: boolean; + icon?: IconDefinition; + buttonVariant?: string; +} + +interface IItemListOptions { + filterMode: GQL.FilterMode; + useResult: (filter: ListFilterModel) => T; + getCount: (data: T) => number; + renderMetadataByline?: (data: T) => React.ReactNode; + getItems: (data: T) => E[]; +} + +interface IRenderListProps { + filter: ListFilterModel; + onChangePage: (page: number) => void; + updateFilter: (filter: ListFilterModel) => void; +} + +interface IItemListProps { + persistState?: PersistanceLevel; + persistanceKey?: string; + defaultSort?: string; + filterHook?: (filter: ListFilterModel) => ListFilterModel; + filterDialog?: ( + criteria: Criterion[], + setCriteria: (v: Criterion[]) => void + ) => React.ReactNode; + zoomable?: boolean; + selectable?: boolean; + alterQuery?: boolean; + defaultZoomIndex?: number; + otherOperations?: IItemListOperation[]; + renderContent: ( + result: T, + filter: ListFilterModel, + selectedIds: Set, + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void, + onChangePage: (page: number) => void, + pageCount: number + ) => React.ReactNode; + renderEditDialog?: ( + selected: E[], + onClose: (applied: boolean) => void + ) => React.ReactNode; + renderDeleteDialog?: ( + selected: E[], + onClose: (confirmed: boolean) => void + ) => React.ReactNode; + addKeybinds?: ( + result: T, + filter: ListFilterModel, + selectedIds: Set + ) => () => void; +} + +const getSelectedData = ( + data: I[], + selectedIds: Set +) => data.filter((value) => selectedIds.has(value.id)); + +/** + * A factory function for ItemList components. + * IMPORTANT: as the component manipulates the URL query string, if there are + * ever multiple ItemLists rendered at once, all but one of them need to have + * `alterQuery` set to false to prevent conflicts. + */ +export function makeItemList({ + filterMode, + useResult, + getCount, + renderMetadataByline, + getItems, +}: IItemListOptions) { + const filterOptions = getFilterOptions(filterMode); + + const RenderList: React.FC & IRenderListProps> = ({ + filter, + onChangePage: _onChangePage, + updateFilter, + persistState, + zoomable, + selectable, + otherOperations, + renderContent, + renderEditDialog, + renderDeleteDialog, + addKeybinds, + }) => { + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [selectedIds, setSelectedIds] = useState>(new Set()); + const [lastClickedId, setLastClickedId] = useState(); + + const [editingCriterion, setEditingCriterion] = useState< + string | undefined + >(); + const [showEditFilter, setShowEditFilter] = useState(false); + + const result = useResult(filter); + const [totalCount, setTotalCount] = useState(0); + const [metadataByline, setMetadataByline] = useState(); + const items = useMemo(() => getItems(result), [result]); + + const [arePaging, setArePaging] = useState(false); + const hidePagination = !arePaging && result.loading; + + // useLayoutEffect to set total count before paint, avoiding a 0 being displayed + useLayoutEffect(() => { + if (result.loading) return; + setArePaging(false); + + setTotalCount(getCount(result)); + setMetadataByline(renderMetadataByline?.(result)); + }, [result]); + + const onChangePage = useCallback( + (page: number) => { + setArePaging(true); + _onChangePage(page); + }, + [_onChangePage] + ); + + // handle case where page is more than there are pages + useEffect(() => { + const pages = Math.ceil(totalCount / filter.itemsPerPage); + if (pages > 0 && filter.currentPage > pages) { + onChangePage(pages); + } + }, [filter, onChangePage, totalCount]); + + // set up hotkeys + useEffect(() => { + Mousetrap.bind("f", () => setShowEditFilter(true)); + + return () => { + Mousetrap.unbind("f"); + }; + }, []); + useEffect(() => { + const pages = Math.ceil(totalCount / filter.itemsPerPage); + Mousetrap.bind("right", () => { + if (filter.currentPage < pages) { + onChangePage(filter.currentPage + 1); + } + }); + Mousetrap.bind("left", () => { + if (filter.currentPage > 1) { + onChangePage(filter.currentPage - 1); + } + }); + Mousetrap.bind("shift+right", () => { + onChangePage(Math.min(pages, filter.currentPage + 10)); + }); + Mousetrap.bind("shift+left", () => { + onChangePage(Math.max(1, filter.currentPage - 10)); + }); + Mousetrap.bind("ctrl+end", () => { + onChangePage(pages); + }); + Mousetrap.bind("ctrl+home", () => { + onChangePage(1); + }); + + return () => { + Mousetrap.unbind("right"); + Mousetrap.unbind("left"); + Mousetrap.unbind("shift+right"); + Mousetrap.unbind("shift+left"); + Mousetrap.unbind("ctrl+end"); + Mousetrap.unbind("ctrl+home"); + }; + }, [filter, onChangePage, totalCount]); + useEffect(() => { + if (addKeybinds) { + const unbindExtras = addKeybinds(result, filter, selectedIds); + return () => { + unbindExtras(); + }; + } + }, [addKeybinds, result, filter, selectedIds]); + + function singleSelect(id: string, selected: boolean) { + setLastClickedId(id); + + const newSelectedIds = clone(selectedIds); + if (selected) { + newSelectedIds.add(id); + } else { + newSelectedIds.delete(id); + } + + setSelectedIds(newSelectedIds); + } + + function selectRange(startIndex: number, endIndex: number) { + let start = startIndex; + let end = endIndex; + if (start > end) { + const tmp = start; + start = end; + end = tmp; + } + + const subset = items.slice(start, end + 1); + const newSelectedIds = new Set(); + + subset.forEach((item) => { + newSelectedIds.add(item.id); + }); + + setSelectedIds(newSelectedIds); + } + + function multiSelect(id: string) { + let startIndex = 0; + let thisIndex = -1; + + if (lastClickedId) { + startIndex = items.findIndex((item) => { + return item.id === lastClickedId; + }); + } + + thisIndex = items.findIndex((item) => { + return item.id === id; + }); + + selectRange(startIndex, thisIndex); + } + + function onSelectChange(id: string, selected: boolean, shiftKey: boolean) { + if (shiftKey) { + multiSelect(id); + } else { + singleSelect(id, selected); + } + } + + function onSelectAll() { + const newSelectedIds = new Set(); + items.forEach((item) => { + newSelectedIds.add(item.id); + }); + + setSelectedIds(newSelectedIds); + setLastClickedId(undefined); + } + + function onSelectNone() { + const newSelectedIds = new Set(); + setSelectedIds(newSelectedIds); + setLastClickedId(undefined); + } + + function onChangeZoom(newZoomIndex: number) { + const newFilter = cloneDeep(filter); + newFilter.zoomIndex = newZoomIndex; + updateFilter(newFilter); + } + + async function onOperationClicked(o: IItemListOperation) { + await o.onClick(result, filter, selectedIds); + if (o.postRefetch) { + result.refetch(); + } + } + + const operations = otherOperations?.map((o) => ({ + text: o.text, + onClick: () => { + onOperationClicked(o); + }, + isDisplayed: () => { + if (o.isDisplayed) { + return o.isDisplayed(result, filter, selectedIds); + } + + return true; + }, + icon: o.icon, + buttonVariant: o.buttonVariant, + })); + + function onEdit() { + setIsEditDialogOpen(true); + } + + function onEditDialogClosed(applied: boolean) { + if (applied) { + onSelectNone(); + } + setIsEditDialogOpen(false); + + // refetch + result.refetch(); + } + + function onDelete() { + setIsDeleteDialogOpen(true); + } + + function onDeleteDialogClosed(deleted: boolean) { + if (deleted) { + onSelectNone(); + } + setIsDeleteDialogOpen(false); + + // refetch + result.refetch(); + } + + function renderPagination() { + if (hidePagination) return; + return ( + + ); + } + + function renderPaginationIndex() { + if (hidePagination) return; + return ( + + ); + } + + function maybeRenderContent() { + if (result.loading) { + return ; + } + if (result.error) { + return

{result.error.message}

; + } + + const pages = Math.ceil(totalCount / filter.itemsPerPage); + return ( + <> + {renderContent( + result, + filter, + selectedIds, + onSelectChange, + onChangePage, + pages + )} + {!!pages && ( + <> + {renderPaginationIndex()} + {renderPagination()} + + )} + + ); + } + + function onChangeDisplayMode(displayMode: DisplayMode) { + const newFilter = cloneDeep(filter); + newFilter.displayMode = displayMode; + updateFilter(newFilter); + } + + function onRemoveCriterion(removedCriterion: Criterion) { + const newFilter = cloneDeep(filter); + newFilter.criteria = newFilter.criteria.filter( + (criterion) => criterion.getId() !== removedCriterion.getId() + ); + newFilter.currentPage = 1; + updateFilter(newFilter); + } + + function onClearAllCriteria() { + const newFilter = cloneDeep(filter); + newFilter.criteria = []; + newFilter.currentPage = 1; + updateFilter(newFilter); + } + + function onApplyEditFilter(f: ListFilterModel) { + setShowEditFilter(false); + setEditingCriterion(undefined); + updateFilter(f); + } + + function onCancelEditFilter() { + setShowEditFilter(false); + setEditingCriterion(undefined); + } + + return ( +
+ + setShowEditFilter(true)} + persistState={persistState} + /> + 0} + onEdit={renderEditDialog ? onEdit : undefined} + onDelete={renderDeleteDialog ? onDelete : undefined} + /> + + + setEditingCriterion(c.criterionOption.type)} + onRemoveCriterion={onRemoveCriterion} + onRemoveAll={() => onClearAllCriteria()} + /> + {(showEditFilter || editingCriterion) && ( + + )} + {isEditDialogOpen && + renderEditDialog && + renderEditDialog(getSelectedData(items, selectedIds), (applied) => + onEditDialogClosed(applied) + )} + {isDeleteDialogOpen && + renderDeleteDialog && + renderDeleteDialog(getSelectedData(items, selectedIds), (deleted) => + onDeleteDialogClosed(deleted) + )} + {renderPagination()} + {renderPaginationIndex()} + {maybeRenderContent()} +
+ ); + }; + + const ItemList: React.FC> = (props) => { + const { + persistState, + persistanceKey = filterMode, + defaultSort = filterOptions.defaultSortBy, + filterHook, + defaultZoomIndex, + alterQuery = true, + } = props; + + const history = useHistory(); + const location = useLocation(); + const [interfaceState, setInterfaceState] = useInterfaceLocalForage(); + const [filterInitialised, setFilterInitialised] = useState(false); + const { configuration: config } = useContext(ConfigurationContext); + + const lastPathname = useRef(location.pathname); + const defaultDisplayMode = filterOptions.displayModeOptions[0]; + const [filter, setFilter] = useState( + () => new ListFilterModel(filterMode) + ); + + const updateSavedFilter = useCallback( + (updatedFilter: ListFilterModel) => { + setInterfaceState((prevState) => { + if (!prevState.queryConfig) { + prevState.queryConfig = {}; + } + + const oldFilter = prevState.queryConfig[persistanceKey]?.filter ?? ""; + const newFilter = new URLSearchParams(oldFilter); + newFilter.set("disp", String(updatedFilter.displayMode)); + + return { + ...prevState, + queryConfig: { + ...prevState.queryConfig, + [persistanceKey]: { + ...prevState.queryConfig[persistanceKey], + filter: newFilter.toString(), + }, + }, + }; + }); + }, + [persistanceKey, setInterfaceState] + ); + + const { data: defaultFilter, loading: defaultFilterLoading } = + useFindDefaultFilter(filterMode); + + const updateQueryParams = useCallback( + (newFilter: ListFilterModel) => { + if (!alterQuery) return; + + const newParams = newFilter.makeQueryParameters(); + history.replace({ ...history.location, search: newParams }); + }, + [alterQuery, history] + ); + + const updateFilter = useCallback( + (newFilter: ListFilterModel) => { + setFilter(newFilter); + updateQueryParams(newFilter); + if (persistState === PersistanceLevel.VIEW) { + updateSavedFilter(newFilter); + } + }, + [persistState, updateSavedFilter, updateQueryParams] + ); + + // 'Startup' hook, initialises the filters + useEffect(() => { + // Only run once + if (filterInitialised) return; + + let newFilter = new ListFilterModel( + filterMode, + config, + defaultSort, + defaultDisplayMode, + defaultZoomIndex + ); + let loadDefault = true; + if (alterQuery && location.search) { + loadDefault = false; + newFilter.configureFromQueryString(location.search); + } + + if (persistState === PersistanceLevel.ALL) { + // only set default filter if uninitialised + if (loadDefault) { + // wait until default filter is loaded + if (defaultFilterLoading) return; + + if (defaultFilter?.findDefaultFilter) { + newFilter.currentPage = 1; + try { + newFilter.configureFromJSON( + defaultFilter.findDefaultFilter.filter + ); + } catch (err) { + console.log(err); + // ignore + } + // #1507 - reset random seed when loaded + newFilter.randomSeed = -1; + } + } + } else if (persistState === PersistanceLevel.VIEW) { + // wait until forage is initialised + if (interfaceState.loading) return; + + const storedQuery = interfaceState.data?.queryConfig?.[persistanceKey]; + if (persistState === PersistanceLevel.VIEW && storedQuery) { + const displayMode = new URLSearchParams(storedQuery.filter).get( + "disp" + ); + if (displayMode) { + newFilter.displayMode = Number.parseInt(displayMode, 10); + } + } + } + setFilter(newFilter); + updateQueryParams(newFilter); + + setFilterInitialised(true); + }, [ + filterInitialised, + location, + config, + defaultSort, + defaultDisplayMode, + defaultZoomIndex, + alterQuery, + persistState, + updateQueryParams, + defaultFilter, + defaultFilterLoading, + interfaceState, + persistanceKey, + ]); + + // This hook runs on every page location change (ie navigation), + // and updates the filter accordingly. + useEffect(() => { + if (!filterInitialised || !alterQuery) return; + + // re-init if the pathname has changed + if (location.pathname !== lastPathname.current) { + lastPathname.current = location.pathname; + setFilterInitialised(false); + return; + } + + // re-init to load default filter on empty new query params + if (!location.search) { + setFilterInitialised(false); + return; + } + + // the query has changed, update filter if necessary + setFilter((prevFilter) => { + let newFilter = prevFilter.clone(); + newFilter.configureFromQueryString(location.search); + if (!isEqual(newFilter, prevFilter)) { + return newFilter; + } else { + return prevFilter; + } + }); + }, [filterInitialised, alterQuery, location]); + + const onChangePage = useCallback( + (page: number) => { + const newFilter = cloneDeep(filter); + newFilter.currentPage = page; + updateFilter(newFilter); + window.scrollTo(0, 0); + }, + [filter, updateFilter] + ); + + const renderFilter = useMemo(() => { + if (filterInitialised) { + return filterHook ? filterHook(cloneDeep(filter)) : filter; + } + }, [filterInitialised, filter, filterHook]); + + if (!renderFilter) return null; + + return ( + + ); + }; + + return ItemList; +} + +export const showWhenSelected = ( + result: T, + filter: ListFilterModel, + selectedIds: Set +) => { + return selectedIds.size > 0; +}; diff --git a/ui/v2.5/src/components/List/ListFilter.tsx b/ui/v2.5/src/components/List/ListFilter.tsx index 9236a5561..93b227828 100644 --- a/ui/v2.5/src/components/List/ListFilter.tsx +++ b/ui/v2.5/src/components/List/ListFilter.tsx @@ -1,6 +1,11 @@ -import debounce from "lodash-es/debounce"; import cloneDeep from "lodash-es/cloneDeep"; -import React, { HTMLAttributes, useEffect, useRef, useState } from "react"; +import React, { + HTMLAttributes, + useCallback, + useEffect, + useRef, + useState, +} from "react"; import cx from "classnames"; import Mousetrap from "mousetrap"; import { SortDirectionEnum } from "src/core/generated-graphql"; @@ -17,29 +22,28 @@ import { Overlay, } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; +import { Icon } from "../Shared/Icon"; import { ListFilterModel } from "src/models/list-filter/filter"; -import { useFocus } from "src/utils"; +import useFocus from "src/utils/focus"; import { ListFilterOptions } from "src/models/list-filter/filter-options"; import { FormattedMessage, useIntl } from "react-intl"; -import { PersistanceLevel } from "src/hooks/ListHook"; +import { PersistanceLevel } from "./ItemList"; import { SavedFilterList } from "./SavedFilterList"; import { faBookmark, faCaretDown, faCaretUp, faCheck, - faFilter, faRandom, faTimes, } from "@fortawesome/free-solid-svg-icons"; +import { FilterButton } from "./Filters/FilterButton"; +import { useDebounce } from "src/hooks/debounce"; -const maxPageSize = 1000; interface IListFilterProps { onFilterUpdate: (newFilter: ListFilterModel) => void; filter: ListFilterModel; filterOptions: ListFilterOptions; - filterDialogOpen?: boolean; persistState?: PersistanceLevel; openFilterDialog: () => void; } @@ -50,7 +54,6 @@ export const ListFilter: React.FC = ({ onFilterUpdate, filter, filterOptions, - filterDialogOpen, openFilterDialog, persistState, }) => { @@ -62,12 +65,26 @@ export const ListFilter: React.FC = ({ const perPageSelect = useRef(null); const [perPageInput, perPageFocus] = useFocus(); - const searchCallback = debounce((value: string) => { - const newFilter = cloneDeep(filter); - newFilter.searchTerm = value; - newFilter.currentPage = 1; - onFilterUpdate(newFilter); - }, 500); + const searchQueryUpdated = useCallback( + (value: string) => { + const newFilter = cloneDeep(filter); + newFilter.searchTerm = value; + newFilter.currentPage = 1; + onFilterUpdate(newFilter); + }, + [filter, onFilterUpdate] + ); + + const searchCallback = useDebounce( + (value: string) => { + const newFilter = cloneDeep(filter); + newFilter.searchTerm = value; + newFilter.currentPage = 1; + onFilterUpdate(newFilter); + }, + [filter, onFilterUpdate], + 500 + ); const intl = useIntl(); @@ -91,6 +108,14 @@ export const ListFilter: React.FC = ({ } }, [customPageSizeShowing, perPageFocus]); + // clear search input when filter is cleared + useEffect(() => { + if (!filter.searchTerm) { + queryRef.current.value = ""; + setQueryClearShowing(false); + } + }, [filter.searchTerm, queryRef]); + function onChangePageSize(val: string) { if (val === "custom") { // added timeout since Firefox seems to trigger the rootClose immediately @@ -106,11 +131,6 @@ export const ListFilter: React.FC = ({ return; } - // don't allow page sizes over 1000 - if (pp > maxPageSize) { - pp = maxPageSize; - } - const newFilter = cloneDeep(filter); newFilter.itemsPerPage = pp; newFilter.currentPage = 1; @@ -124,7 +144,7 @@ export const ListFilter: React.FC = ({ function onClearQuery() { queryRef.current.value = ""; - searchCallback(""); + searchQueryUpdated(""); setQueryFocus(); setQueryClearShowing(false); } @@ -267,13 +287,7 @@ export const ListFilter: React.FC = ({ } > - + openFilterDialog()} filter={filter} /> @@ -349,7 +363,6 @@ export const ListFilter: React.FC = ({ ) => { diff --git a/ui/v2.5/src/components/List/ListOperationButtons.tsx b/ui/v2.5/src/components/List/ListOperationButtons.tsx index 15a94b75e..c279020e9 100644 --- a/ui/v2.5/src/components/List/ListOperationButtons.tsx +++ b/ui/v2.5/src/components/List/ListOperationButtons.tsx @@ -9,7 +9,7 @@ import { import Mousetrap from "mousetrap"; import { FormattedMessage, useIntl } from "react-intl"; import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; -import { Icon } from "../Shared"; +import { Icon } from "../Shared/Icon"; import { faEllipsisH, faPencilAlt, diff --git a/ui/v2.5/src/components/List/ListViewOptions.tsx b/ui/v2.5/src/components/List/ListViewOptions.tsx index ec31cf452..2dc84d09a 100644 --- a/ui/v2.5/src/components/List/ListViewOptions.tsx +++ b/ui/v2.5/src/components/List/ListViewOptions.tsx @@ -9,7 +9,7 @@ import { } from "react-bootstrap"; import { DisplayMode } from "src/models/list-filter/types"; import { useIntl } from "react-intl"; -import { Icon } from "../Shared"; +import { Icon } from "../Shared/Icon"; import { faList, faSquare, diff --git a/ui/v2.5/src/components/List/SavedFilterList.tsx b/ui/v2.5/src/components/List/SavedFilterList.tsx index 87be25365..8a5da0473 100644 --- a/ui/v2.5/src/components/List/SavedFilterList.tsx +++ b/ui/v2.5/src/components/List/SavedFilterList.tsx @@ -15,13 +15,13 @@ import { useSaveFilter, useSetDefaultFilter, } from "src/core/StashService"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; import { ListFilterModel } from "src/models/list-filter/filter"; import { SavedFilterDataFragment } from "src/core/generated-graphql"; -import { LoadingIndicator } from "src/components/Shared"; -import { PersistanceLevel } from "src/hooks/ListHook"; +import { PersistanceLevel } from "./ItemList"; import { FormattedMessage, useIntl } from "react-intl"; -import { Icon } from "../Shared"; +import { Icon } from "../Shared/Icon"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { faSave, faTimes } from "@fortawesome/free-solid-svg-icons"; interface ISavedFilterListProps { @@ -162,7 +162,10 @@ export const SavedFilterList: React.FC = ({ function filterClicked(f: SavedFilterDataFragment) { const newFilter = filter.clone(); + newFilter.currentPage = 1; + // #1795 - reset search term if not present in saved filter + newFilter.searchTerm = ""; newFilter.configureFromJSON(f.filter); // #1507 - reset random seed when loaded newFilter.randomSeed = -1; diff --git a/ui/v2.5/src/components/List/styles.scss b/ui/v2.5/src/components/List/styles.scss index 99933638b..c8fcb4bc4 100644 --- a/ui/v2.5/src/components/List/styles.scss +++ b/ui/v2.5/src/components/List/styles.scss @@ -116,3 +116,83 @@ input[type="range"].zoom-slider { .rating-filter .and-divider { margin-left: 0.5em; } + +.edit-filter-dialog { + .modal-body { + padding-left: 0; + padding-right: 0; + } + + .filter-tags { + border-top: 1px solid rgb(16 22 26 / 40%); + padding: 1rem 1rem 0 1rem; + } + + .criterion-list { + flex-direction: column; + flex-wrap: nowrap; + max-height: 550px; + overflow-y: auto; + + .card { + border: 1px solid rgb(16 22 26 / 40%); + box-shadow: none; + margin: 0 0 -1px; + padding: 0; + + .collapse-icon { + margin-left: 0; + } + + .card-header { + cursor: pointer; + display: flex; + justify-content: space-between; + } + } + + .remove-criterion-button { + border: 0; + color: $danger; + padding-bottom: 0; + padding-top: 0; + } + } + + .edit-filter-right { + display: flex; + flex-direction: column; + justify-content: space-between; + padding-left: 1rem; + padding-right: 1rem; + width: 100%; + } +} + +.modifier-selector { + margin-bottom: 1rem; + + // to accommodate for caret + padding-right: 2rem; +} + +.filter-tags .clear-all-button { + color: $text-color; + // to match filter pills + line-height: 16px; + padding: 0; +} + +.filter-button { + .fa-icon { + margin: 0; + } + + .badge { + position: absolute; + right: -3px; + + // button group has a z-index of 1 + z-index: 2; + } +} diff --git a/ui/v2.5/src/components/MainNavbar.tsx b/ui/v2.5/src/components/MainNavbar.tsx index d48fb015a..85eea19c0 100644 --- a/ui/v2.5/src/components/MainNavbar.tsx +++ b/ui/v2.5/src/components/MainNavbar.tsx @@ -11,14 +11,14 @@ import { LinkContainer } from "react-router-bootstrap"; import { Link, NavLink, useLocation, useHistory } from "react-router-dom"; import Mousetrap from "mousetrap"; -import { SessionUtils } from "src/utils"; -import Icon from "src/components/Shared/Icon"; +import SessionUtils from "src/utils/session"; +import { Icon } from "src/components/Shared/Icon"; import { ConfigurationContext } from "src/hooks/Config"; import { ManualStateContext } from "./Help/context"; import { SettingsButton } from "./SettingsButton"; import { faBars, - faChartBar, + faChartColumn, faFilm, faHeart, faImage, @@ -220,10 +220,10 @@ export const MainNavbar: React.FC = () => { const pathname = location.pathname.replace(/\/$/, ""); let newPath = newPathsList.includes(pathname) ? `${pathname}/new` : null; - if (newPath != null) { + if (newPath !== null) { let queryParam = new URLSearchParams(location.search).get("q"); - if (queryParam != null) { - newPath += "?name=" + encodeURIComponent(queryParam); + if (queryParam) { + newPath += "?q=" + encodeURIComponent(queryParam); } } @@ -296,7 +296,7 @@ export const MainNavbar: React.FC = () => { className="minimal d-flex align-items-center h-100" title={intl.formatMessage({ id: "statistics" })} > - + = ( function render() { return ( - = ( />
- + ); } diff --git a/ui/v2.5/src/components/Movies/MovieCard.tsx b/ui/v2.5/src/components/Movies/MovieCard.tsx index 5c766c97b..dc1087264 100644 --- a/ui/v2.5/src/components/Movies/MovieCard.tsx +++ b/ui/v2.5/src/components/Movies/MovieCard.tsx @@ -1,13 +1,11 @@ -import React, { FunctionComponent } from "react"; +import React from "react"; import { Button, ButtonGroup } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; -import { - GridCard, - HoverPopover, - Icon, - TagLink, - TruncatedText, -} from "src/components/Shared"; +import { GridCard } from "../Shared/GridCard"; +import { HoverPopover } from "../Shared/HoverPopover"; +import { Icon } from "../Shared/Icon"; +import { TagLink } from "../Shared/TagLink"; +import { TruncatedText } from "../Shared/TruncatedText"; import { FormattedMessage } from "react-intl"; import { RatingBanner } from "../Shared/RatingBanner"; import { faPlayCircle } from "@fortawesome/free-solid-svg-icons"; @@ -20,7 +18,7 @@ interface IProps { onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void; } -export const MovieCard: FunctionComponent = (props: IProps) => { +export const MovieCard: React.FC = (props: IProps) => { function maybeRenderSceneNumber() { if (!props.sceneIndex) return; diff --git a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx index 3984d4c33..03aa12b6e 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx @@ -9,13 +9,11 @@ import { useMovieDestroy, } from "src/core/StashService"; import { useParams, useHistory } from "react-router-dom"; -import { - DetailsEditNavbar, - ErrorMessage, - LoadingIndicator, - Modal, -} from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; +import { ErrorMessage } from "src/components/Shared/ErrorMessage"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { useToast } from "src/hooks/Toast"; import { MovieScenesPanel } from "./MovieScenesPanel"; import { MovieDetailsPanel } from "./MovieDetailsPanel"; import { MovieEditPanel } from "./MovieEditPanel"; @@ -35,12 +33,8 @@ const MoviePage: React.FC = ({ movie }) => { const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); // Editing movie state - const [frontImage, setFrontImage] = useState( - undefined - ); - const [backImage, setBackImage] = useState( - undefined - ); + const [frontImage, setFrontImage] = useState(); + const [backImage, setBackImage] = useState(); const [encodingImage, setEncodingImage] = useState(false); const [updateMovie, { loading: updating }] = useMovieUpdate(); @@ -51,7 +45,9 @@ const MoviePage: React.FC = ({ movie }) => { // set up hotkeys useEffect(() => { Mousetrap.bind("e", () => setIsEditing(true)); - Mousetrap.bind("d d", () => onDelete()); + Mousetrap.bind("d d", () => { + onDelete(); + }); return () => { Mousetrap.unbind("e"); @@ -59,26 +55,14 @@ const MoviePage: React.FC = ({ movie }) => { }; }); - const onImageEncoding = (isEncoding = false) => setEncodingImage(isEncoding); - - function getMovieInput( - input: Partial - ) { - const ret: Partial = { - ...input, - id: movie.id, - }; - - return ret; - } - - async function onSave( - input: Partial - ) { + async function onSave(input: GQL.MovieCreateInput) { try { const result = await updateMovie({ variables: { - input: getMovieInput(input) as GQL.MovieUpdateInput, + input: { + id: movie.id, + ...input, + }, }, }); if (result.data?.movieUpdate) { @@ -109,7 +93,7 @@ const MoviePage: React.FC = ({ movie }) => { function renderDeleteAlert() { return ( - = ({ movie }) => { }} />

-
+ ); } @@ -214,13 +198,13 @@ const MoviePage: React.FC = ({ movie }) => { onDelete={onDelete} setFrontImage={setFrontImage} setBackImage={setBackImage} - onImageEncoding={onImageEncoding} + setEncodingImage={setEncodingImage} /> )}
- +
{renderDeleteAlert()} diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx index faef1c20c..36b6ea5bd 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx @@ -1,44 +1,32 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import * as GQL from "src/core/generated-graphql"; import { useMovieCreate } from "src/core/StashService"; -import { useHistory } from "react-router-dom"; -import { LoadingIndicator } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { useHistory, useLocation } from "react-router-dom"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { useToast } from "src/hooks/Toast"; import { MovieEditPanel } from "./MovieEditPanel"; const MovieCreate: React.FC = () => { const history = useHistory(); + const location = useLocation(); const Toast = useToast(); + const query = useMemo(() => new URLSearchParams(location.search), [location]); + const movie = { + name: query.get("q") ?? undefined, + }; + // Editing movie state - const [frontImage, setFrontImage] = useState( - undefined - ); - const [backImage, setBackImage] = useState( - undefined - ); + const [frontImage, setFrontImage] = useState(); + const [backImage, setBackImage] = useState(); const [encodingImage, setEncodingImage] = useState(false); const [createMovie] = useMovieCreate(); - const onImageEncoding = (isEncoding = false) => setEncodingImage(isEncoding); - - function getMovieInput( - input: Partial - ) { - const ret: Partial = { - ...input, - }; - - return ret; - } - - async function onSave( - input: Partial - ) { + async function onSave(input: GQL.MovieCreateInput) { try { const result = await createMovie({ - variables: getMovieInput(input) as GQL.MovieCreateInput, + variables: input, }); if (result.data?.movieCreate?.id) { history.push(`/movies/${result.data.movieCreate.id}`); @@ -84,12 +72,13 @@ const MovieCreate: React.FC = () => { history.push("/movies")} onDelete={() => {}} setFrontImage={setFrontImage} setBackImage={setBackImage} - onImageEncoding={onImageEncoding} + setEncodingImage={setEncodingImage} /> diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx index 2c4851bf6..bccbe36b6 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx @@ -1,7 +1,8 @@ import React from "react"; import { useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; -import { DurationUtils, TextUtils } from "src/utils"; +import DurationUtils from "src/utils/duration"; +import TextUtils from "src/utils/text"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { TextField, URLField } from "src/utils/field"; diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx index 07eb87648..c2c51794c 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx @@ -7,31 +7,33 @@ import { queryScrapeMovieURL, useListMovieScrapers, } from "src/core/StashService"; -import { - LoadingIndicator, - StudioSelect, - DetailsEditNavbar, - DurationInput, - URLField, -} from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { StudioSelect } from "src/components/Shared/Select"; +import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; +import { DurationInput } from "src/components/Shared/DurationInput"; +import { URLField } from "src/components/Shared/URLField"; +import { useToast } from "src/hooks/Toast"; import { Modal as BSModal, Form, Button, Col, Row } from "react-bootstrap"; -import { DurationUtils, FormUtils, ImageUtils } from "src/utils"; +import DurationUtils from "src/utils/duration"; +import FormUtils from "src/utils/form"; +import ImageUtils from "src/utils/image"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { useFormik } from "formik"; import { Prompt } from "react-router-dom"; import { MovieScrapeDialog } from "./MovieScrapeDialog"; +import { useRatingKeybinds } from "src/hooks/keybinds"; +import { ConfigurationContext } from "src/hooks/Config"; +import isEqual from "lodash-es/isEqual"; +import { DateInput } from "src/components/Shared/DateInput"; interface IMovieEditPanel { - movie?: Partial; - onSubmit: ( - movie: Partial - ) => void; + movie: Partial; + onSubmit: (movie: GQL.MovieCreateInput) => void; onCancel: () => void; onDelete: () => void; setFrontImage: (image?: string | null) => void; setBackImage: (image?: string | null) => void; - onImageEncoding: (loading?: boolean) => void; + setEncodingImage: (loading: boolean) => void; } export const MovieEditPanel: React.FC = ({ @@ -41,92 +43,87 @@ export const MovieEditPanel: React.FC = ({ onDelete, setFrontImage, setBackImage, - onImageEncoding, + setEncodingImage, }) => { const intl = useIntl(); const Toast = useToast(); + const { configuration: stashConfig } = React.useContext(ConfigurationContext); - const isNew = movie === undefined; + const isNew = movie.id === undefined; const [isLoading, setIsLoading] = useState(false); const [isImageAlertOpen, setIsImageAlertOpen] = useState(false); - const [imageClipboard, setImageClipboard] = useState( - undefined - ); + const [imageClipboard, setImageClipboard] = useState(); const Scrapers = useListMovieScrapers(); - const [scrapedMovie, setScrapedMovie] = useState< - GQL.ScrapedMovie | undefined - >(); + const [scrapedMovie, setScrapedMovie] = useState(); const schema = yup.object({ name: yup.string().required(), - aliases: yup.string().optional().nullable(), - duration: yup.string().optional().nullable(), + aliases: yup.string().ensure(), + duration: yup.number().nullable().defined(), date: yup .string() - .optional() - .nullable() - .matches(/^\d{4}-\d{2}-\d{2}$/), - rating100: yup.number().optional().nullable(), - studio_id: yup.string().optional().nullable(), - director: yup.string().optional().nullable(), - synopsis: yup.string().optional().nullable(), - url: yup.string().optional().nullable(), - front_image: yup.string().optional().nullable(), - back_image: yup.string().optional().nullable(), + .ensure() + .test({ + 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; + return true; + }, + message: intl.formatMessage({ id: "validation.date_invalid_form" }), + }), + studio_id: yup.string().required().nullable(), + director: yup.string().ensure(), + rating100: yup.number().nullable().defined(), + url: yup.string().ensure(), + synopsis: yup.string().ensure(), + front_image: yup.string().nullable().optional(), + back_image: yup.string().nullable().optional(), }); const initialValues = { - name: movie?.name, - aliases: movie?.aliases, - duration: movie?.duration, - date: movie?.date, + name: movie?.name ?? "", + aliases: movie?.aliases ?? "", + duration: movie?.duration ?? null, + date: movie?.date ?? "", + studio_id: movie?.studio?.id ?? null, + director: movie?.director ?? "", rating100: movie?.rating100 ?? null, - studio_id: movie?.studio?.id, - director: movie?.director, - synopsis: movie?.synopsis, - url: movie?.url, - front_image: undefined, - back_image: undefined, + url: movie?.url ?? "", + synopsis: movie?.synopsis ?? "", }; - type InputValues = typeof initialValues; + type InputValues = yup.InferType; - const formik = useFormik({ + const formik = useFormik({ initialValues, + enableReinitialize: true, validationSchema: schema, - onSubmit: (values) => onSubmit(getMovieInput(values)), + onSubmit: (values) => onSubmit(values), }); - const encodingImage = ImageUtils.usePasteImage(showImageAlert); - - useEffect(() => { - setFrontImage(formik.values.front_image); - }, [formik.values.front_image, setFrontImage]); - - useEffect(() => { - setBackImage(formik.values.back_image); - }, [formik.values.back_image, setBackImage]); - - useEffect(() => onImageEncoding(encodingImage), [ - onImageEncoding, - encodingImage, - ]); - function setRating(v: number) { formik.setFieldValue("rating100", v); } + useRatingKeybinds( + true, + stashConfig?.ui?.ratingSystemOptions?.type, + setRating + ); + + function onCancelEditing() { + setFrontImage(undefined); + setBackImage(undefined); + onCancel?.(); + } + // set up hotkeys useEffect(() => { - Mousetrap.bind("r 0", () => setRating(NaN)); - Mousetrap.bind("r 1", () => setRating(20)); - Mousetrap.bind("r 2", () => setRating(40)); - Mousetrap.bind("r 3", () => setRating(60)); - Mousetrap.bind("r 4", () => setRating(80)); - Mousetrap.bind("r 5", () => setRating(100)); // Mousetrap.bind("u", (e) => { // setStudioFocus() // e.preventDefault(); @@ -134,46 +131,11 @@ export const MovieEditPanel: React.FC = ({ Mousetrap.bind("s s", () => formik.handleSubmit()); return () => { - Mousetrap.unbind("r 0"); - Mousetrap.unbind("r 1"); - Mousetrap.unbind("r 2"); - Mousetrap.unbind("r 3"); - Mousetrap.unbind("r 4"); - Mousetrap.unbind("r 5"); // Mousetrap.unbind("u"); Mousetrap.unbind("s s"); }; }); - function showImageAlert(imageData: string) { - setImageClipboard(imageData); - setIsImageAlertOpen(true); - } - - function setImageFromClipboard(isFrontImage: boolean) { - if (isFrontImage) { - formik.setFieldValue("front_image", imageClipboard); - } else { - formik.setFieldValue("back_image", imageClipboard); - } - - setImageClipboard(undefined); - setIsImageAlertOpen(false); - } - - function getMovieInput(values: InputValues) { - const input: Partial = { - ...values, - rating100: values.rating100 ?? null, - studio_id: values.studio_id ?? null, - }; - - if (movie && movie.id) { - (input as GQL.MovieUpdateInput).id = movie.id; - } - return input; - } - function updateMovieEditStateFromScraper( state: Partial ) { @@ -182,39 +144,42 @@ export const MovieEditPanel: React.FC = ({ } if (state.aliases) { - formik.setFieldValue("aliases", state.aliases ?? undefined); + formik.setFieldValue("aliases", state.aliases); } if (state.duration) { formik.setFieldValue( "duration", - DurationUtils.stringToSeconds(state.duration) ?? undefined + DurationUtils.stringToSeconds(state.duration) ); } if (state.date) { - formik.setFieldValue("date", state.date ?? undefined); + formik.setFieldValue("date", state.date); } if (state.studio && state.studio.stored_id) { - formik.setFieldValue("studio_id", state.studio.stored_id ?? undefined); + formik.setFieldValue("studio_id", state.studio.stored_id); } if (state.director) { - formik.setFieldValue("director", state.director ?? undefined); + formik.setFieldValue("director", state.director); } if (state.synopsis) { - formik.setFieldValue("synopsis", state.synopsis ?? undefined); + formik.setFieldValue("synopsis", state.synopsis); } if (state.url) { - formik.setFieldValue("url", state.url ?? undefined); + formik.setFieldValue("url", state.url); } - const imageStr = (state as GQL.ScrapedMovieDataFragment).front_image; - formik.setFieldValue("front_image", imageStr ?? undefined); - - const backImageStr = (state as GQL.ScrapedMovieDataFragment).back_image; - formik.setFieldValue("back_image", backImageStr ?? undefined); + if (state.front_image) { + // image is a base64 string + formik.setFieldValue("front_image", state.front_image); + } + if (state.back_image) { + // image is a base64 string + formik.setFieldValue("back_image", state.back_image); + } } async function onScrapeMovieURL() { @@ -255,7 +220,10 @@ export const MovieEditPanel: React.FC = ({ return; } - const currentMovie = getMovieInput(formik.values); + const currentMovie = { + id: movie.id!, + ...formik.values, + }; // Get image paths for scrape gui currentMovie.front_image = movie?.front_image_path; @@ -279,16 +247,50 @@ export const MovieEditPanel: React.FC = ({ setScrapedMovie(undefined); } + const encodingImage = ImageUtils.usePasteImage(showImageAlert); + + useEffect(() => { + setFrontImage(formik.values.front_image); + }, [formik.values.front_image, setFrontImage]); + + useEffect(() => { + setBackImage(formik.values.back_image); + }, [formik.values.back_image, setBackImage]); + + useEffect(() => { + setEncodingImage(encodingImage); + }, [setEncodingImage, encodingImage]); + + function onFrontImageLoad(imageData: string | null) { + formik.setFieldValue("front_image", imageData); + } + function onFrontImageChange(event: React.FormEvent) { - ImageUtils.onImageChange(event, (data) => - formik.setFieldValue("front_image", data) - ); + ImageUtils.onImageChange(event, onFrontImageLoad); + } + + function onBackImageLoad(imageData: string | null) { + formik.setFieldValue("back_image", imageData); } function onBackImageChange(event: React.FormEvent) { - ImageUtils.onImageChange(event, (data) => - formik.setFieldValue("back_image", data) - ); + ImageUtils.onImageChange(event, onBackImageLoad); + } + + function showImageAlert(imageData: string) { + setImageClipboard(imageData); + setIsImageAlertOpen(true); + } + + function setImageFromClipboard(isFrontImage: boolean) { + if (isFrontImage) { + formik.setFieldValue("front_image", imageClipboard); + } else { + formik.setFieldValue("back_image", imageClipboard); + } + + setImageClipboard(undefined); + setIsImageAlertOpen(false); } function renderImageAlert() { @@ -332,7 +334,7 @@ export const MovieEditPanel: React.FC = ({ const isEditing = true; - function renderTextField(field: string, title: string) { + function renderTextField(field: string, title: string, placeholder?: string) { return ( {FormUtils.renderLabel({ @@ -341,10 +343,13 @@ export const MovieEditPanel: React.FC = ({ + + {formik.getFieldMeta(field).error} + ); @@ -399,14 +404,25 @@ export const MovieEditPanel: React.FC = ({ { - formik.setFieldValue("duration", valueAsNumber); + onValueChange={(valueAsNumber) => { + formik.setFieldValue("duration", valueAsNumber ?? null); }} />
- {renderTextField("date", intl.formatMessage({ id: "date" }))} + + {FormUtils.renderLabel({ + title: intl.formatMessage({ id: "date" }), + })} + + formik.setFieldValue("date", value)} + error={formik.errors.date} + /> + + {FormUtils.renderLabel({ @@ -417,7 +433,7 @@ export const MovieEditPanel: React.FC = ({ onSelect={(items) => formik.setFieldValue( "studio_id", - items.length > 0 ? items[0]?.id : undefined + items.length > 0 ? items[0]?.id : null ) } ids={formik.values.studio_id ? [formik.values.studio_id] : []} @@ -472,19 +488,15 @@ export const MovieEditPanel: React.FC = ({ objectName={movie?.name ?? intl.formatMessage({ id: "movie" })} isNew={isNew} isEditing={isEditing} - onToggleEdit={onCancel} - onSave={() => formik.handleSubmit()} - saveDisabled={!formik.dirty} + onToggleEdit={onCancelEditing} + onSave={formik.handleSubmit} + saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})} onImageChange={onFrontImageChange} - onImageChangeURL={(i) => formik.setFieldValue("front_image", i)} - onClearImage={() => { - formik.setFieldValue("front_image", null); - }} + onImageChangeURL={onFrontImageLoad} + onClearImage={() => onFrontImageLoad(null)} onBackImageChange={onBackImageChange} - onBackImageChangeURL={(i) => formik.setFieldValue("back_image", i)} - onClearBackImage={() => { - formik.setFieldValue("back_image", null); - }} + onBackImageChangeURL={onBackImageLoad} + onClearBackImage={() => onBackImageLoad(null)} onDelete={onDelete} /> diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx index 1750bc5f9..22215de5a 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx @@ -5,10 +5,14 @@ import { ListFilterModel } from "src/models/list-filter/filter"; import { SceneList } from "src/components/Scenes/SceneList"; interface IMovieScenesPanel { + active: boolean; movie: GQL.MovieDataFragment; } -export const MovieScenesPanel: React.FC = ({ movie }) => { +export const MovieScenesPanel: React.FC = ({ + active, + movie, +}) => { function filterHook(filter: ListFilterModel) { const movieValue = { id: movie.id, label: movie.name }; // if movie is already present, then we modify it, otherwise add @@ -43,7 +47,11 @@ export const MovieScenesPanel: React.FC = ({ movie }) => { if (movie && movie.id) { return ( - + ); } return <>; diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieScrapeDialog.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieScrapeDialog.tsx index 08c545e04..a6f53f179 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieScrapeDialog.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieScrapeDialog.tsx @@ -9,10 +9,10 @@ import { ScrapeDialogRow, ScrapedTextAreaRow, } from "src/components/Shared/ScrapeDialog"; -import { StudioSelect } from "src/components/Shared"; -import { DurationUtils } from "src/utils"; +import { StudioSelect } from "src/components/Shared/Select"; +import DurationUtils from "src/utils/duration"; import { useStudioCreate } from "src/core/StashService"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; function renderScrapedStudio( result: ScrapeResult, diff --git a/ui/v2.5/src/components/Movies/MovieList.tsx b/ui/v2.5/src/components/Movies/MovieList.tsx index 51a6bd2b0..c5b6c8ded 100644 --- a/ui/v2.5/src/components/Movies/MovieList.tsx +++ b/ui/v2.5/src/components/Movies/MovieList.tsx @@ -3,28 +3,41 @@ import { useIntl } from "react-intl"; import cloneDeep from "lodash-es/cloneDeep"; import Mousetrap from "mousetrap"; import { useHistory } from "react-router-dom"; -import { - FindMoviesQueryResult, - SlimMovieDataFragment, - MovieDataFragment, -} from "src/core/generated-graphql"; import { ListFilterModel } from "src/models/list-filter/filter"; import { DisplayMode } from "src/models/list-filter/types"; -import { queryFindMovies, useMoviesDestroy } from "src/core/StashService"; +import * as GQL from "src/core/generated-graphql"; import { - showWhenSelected, - useMoviesList, + queryFindMovies, + useFindMovies, + useMoviesDestroy, +} from "src/core/StashService"; +import { + makeItemList, PersistanceLevel, -} from "src/hooks/ListHook"; -import { ExportDialog, DeleteEntityDialog } from "src/components/Shared"; + showWhenSelected, +} from "../List/ItemList"; +import { ExportDialog } from "../Shared/ExportDialog"; +import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog"; import { MovieCard } from "./MovieCard"; import { EditMoviesDialog } from "./EditMoviesDialog"; +const MovieItemList = makeItemList({ + filterMode: GQL.FilterMode.Movies, + useResult: useFindMovies, + getItems(result: GQL.FindMoviesQueryResult) { + return result?.data?.findMovies?.movies ?? []; + }, + getCount(result: GQL.FindMoviesQueryResult) { + return result?.data?.findMovies?.count ?? 0; + }, +}); + interface IMovieList { filterHook?: (filter: ListFilterModel) => ListFilterModel; + alterQuery?: boolean; } -export const MovieList: React.FC = ({ filterHook }) => { +export const MovieList: React.FC = ({ filterHook, alterQuery }) => { const intl = useIntl(); const history = useHistory(); const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); @@ -46,10 +59,10 @@ export const MovieList: React.FC = ({ filterHook }) => { }, ]; - const addKeybinds = ( - result: FindMoviesQueryResult, + function addKeybinds( + result: GQL.FindMoviesQueryResult, filter: ListFilterModel - ) => { + ) { Mousetrap.bind("p r", () => { viewRandom(result, filter); }); @@ -57,49 +70,14 @@ export const MovieList: React.FC = ({ filterHook }) => { return () => { Mousetrap.unbind("p r"); }; - }; - - function renderEditDialog( - selectedMovies: MovieDataFragment[], - onClose: (applied: boolean) => void - ) { - return ( - <> - - - ); } - const renderDeleteDialog = ( - selectedMovies: SlimMovieDataFragment[], - onClose: (confirmed: boolean) => void - ) => ( - - ); - - const listData = useMoviesList({ - renderContent, - addKeybinds, - otherOperations, - selectable: true, - persistState: PersistanceLevel.ALL, - renderEditDialog, - renderDeleteDialog, - filterHook, - }); - async function viewRandom( - result: FindMoviesQueryResult, + result: GQL.FindMoviesQueryResult, filter: ListFilterModel ) { // query for a random image - if (result.data && result.data.findMovies) { + if (result.data?.findMovies) { const { count } = result.data.findMovies; const index = Math.floor(Math.random() * count); @@ -107,13 +85,8 @@ export const MovieList: React.FC = ({ filterHook }) => { filterCopy.itemsPerPage = 1; filterCopy.currentPage = index + 1; const singleResult = await queryFindMovies(filterCopy); - if ( - singleResult && - singleResult.data && - singleResult.data.findMovies && - singleResult.data.findMovies.movies.length === 1 - ) { - const { id } = singleResult!.data!.findMovies!.movies[0]; + if (singleResult.data.findMovies.movies.length === 1) { + const { id } = singleResult.data.findMovies.movies[0]; // navigate to the movie page history.push(`/movies/${id}`); } @@ -130,10 +103,15 @@ export const MovieList: React.FC = ({ filterHook }) => { setIsExportDialogOpen(true); } - function maybeRenderMovieExportDialog(selectedIds: Set) { - if (isExportDialogOpen) { - return ( - <> + function renderContent( + result: GQL.FindMoviesQueryResult, + filter: ListFilterModel, + selectedIds: Set, + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void + ) { + function maybeRenderMovieExportDialog() { + if (isExportDialogOpen) { + return ( = ({ filterHook }) => { all: isExportAll, }, }} - onClose={() => { - setIsExportDialogOpen(false); - }} + onClose={() => setIsExportDialogOpen(false)} /> - - ); + ); + } } - } - function renderContent( - result: FindMoviesQueryResult, - filter: ListFilterModel, - selectedIds: Set - ) { - if (!result.data?.findMovies) { - return; - } - if (filter.displayMode === DisplayMode.Grid) { - return ( - <> - {maybeRenderMovieExportDialog(selectedIds)} + function renderMovies() { + if (!result.data?.findMovies) return; + + if (filter.displayMode === DisplayMode.Grid) { + return (
{result.data.findMovies.movies.map((p) => ( = ({ filterHook }) => { selecting={selectedIds.size > 0} selected={selectedIds.has(p.id)} onSelectedChanged={(selected: boolean, shiftKey: boolean) => - listData.onSelectChange(p.id, selected, shiftKey) + onSelectChange(p.id, selected, shiftKey) } /> ))}
- - ); - } - if (filter.displayMode === DisplayMode.List) { - return

TODO

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

TODO

; + } } + return ( + <> + {maybeRenderMovieExportDialog()} + {renderMovies()} + + ); } - return listData.template; + function renderEditDialog( + selectedMovies: GQL.MovieDataFragment[], + onClose: (applied: boolean) => void + ) { + return ; + } + + function renderDeleteDialog( + selectedMovies: GQL.SlimMovieDataFragment[], + onClose: (confirmed: boolean) => void + ) { + return ( + + ); + } + + return ( + + ); }; diff --git a/ui/v2.5/src/components/Movies/MovieRecommendationRow.tsx b/ui/v2.5/src/components/Movies/MovieRecommendationRow.tsx index 710b54b59..34d5119ff 100644 --- a/ui/v2.5/src/components/Movies/MovieRecommendationRow.tsx +++ b/ui/v2.5/src/components/Movies/MovieRecommendationRow.tsx @@ -1,6 +1,6 @@ import React from "react"; import { useFindMovies } from "src/core/StashService"; -import Slider from "react-slick"; +import Slider from "@ant-design/react-slick"; import { MovieCard } from "./MovieCard"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getSlickSliderSettings } from "src/core/recommendations"; diff --git a/ui/v2.5/src/components/Movies/Movies.tsx b/ui/v2.5/src/components/Movies/Movies.tsx index af9b501b3..2c35759fb 100644 --- a/ui/v2.5/src/components/Movies/Movies.tsx +++ b/ui/v2.5/src/components/Movies/Movies.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Route, Switch } from "react-router-dom"; import { useIntl } from "react-intl"; import { Helmet } from "react-helmet"; -import { TITLE_SUFFIX } from "src/components/Shared"; +import { TITLE_SUFFIX } from "src/components/Shared/constants"; import Movie from "./MovieDetails/Movie"; import MovieCreate from "./MovieDetails/MovieCreate"; import { MovieList } from "./MovieList"; diff --git a/ui/v2.5/src/components/PageNotFound.tsx b/ui/v2.5/src/components/PageNotFound.tsx index 645709fcf..67cb6b38c 100644 --- a/ui/v2.5/src/components/PageNotFound.tsx +++ b/ui/v2.5/src/components/PageNotFound.tsx @@ -1,5 +1,5 @@ -import React, { FunctionComponent } from "react"; +import React from "react"; -export const PageNotFound: FunctionComponent = () => { +export const PageNotFound: React.FC = () => { return

Page not found.

; }; diff --git a/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx b/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx index 42453a1a6..aff7fa268 100644 --- a/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx +++ b/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx @@ -3,9 +3,9 @@ import { Col, Form, Row } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; import { useBulkPerformerUpdate } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; -import { Modal } from "src/components/Shared"; -import { useToast } from "src/hooks"; -import MultiSet from "../Shared/MultiSet"; +import { ModalComponent } from "../Shared/Modal"; +import { useToast } from "src/hooks/Toast"; +import { MultiSet } from "../Shared/MultiSet"; import { RatingSystem } from "../Shared/Rating/RatingSystem"; import { getAggregateInputValue, @@ -20,7 +20,7 @@ import { import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox"; import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput"; import { faPencilAlt } from "@fortawesome/free-solid-svg-icons"; -import { FormUtils } from "../../utils"; +import FormUtils from "src/utils/form"; interface IListOperationProps { selected: GQL.SlimPerformerDataFragment[]; @@ -29,6 +29,7 @@ interface IListOperationProps { const performerFields = [ "favorite", + "disambiguation", "url", "instagram", "twitter", @@ -59,10 +60,8 @@ export const EditPerformersDialog: React.FC = ( mode: GQL.BulkUpdateIdMode.Add, }); const [existingTagIds, setExistingTagIds] = useState(); - const [ - aggregateState, - setAggregateState, - ] = useState({}); + const [aggregateState, setAggregateState] = + useState({}); // weight needs conversion to/from number const [weight, setWeight] = useState(); const [updateInput, setUpdateInput] = useState( @@ -182,7 +181,7 @@ export const EditPerformersDialog: React.FC = ( function render() { return ( - = ( setUpdateField({ gender: stringToGender(event.currentTarget.value), @@ -243,6 +242,9 @@ export const EditPerformersDialog: React.FC = (
+ {renderTextField("disambiguation", updateInput.disambiguation, (v) => + setUpdateField({ disambiguation: v }) + )} {renderTextField("birthdate", updateInput.birthdate, (v) => setUpdateField({ birthdate: v }) )} @@ -315,7 +317,7 @@ export const EditPerformersDialog: React.FC = ( />
- + ); } diff --git a/ui/v2.5/src/components/Performers/PerformerCard.tsx b/ui/v2.5/src/components/Performers/PerformerCard.tsx index 26baa8f7a..f11f96dbe 100644 --- a/ui/v2.5/src/components/Performers/PerformerCard.tsx +++ b/ui/v2.5/src/components/Performers/PerformerCard.tsx @@ -2,14 +2,13 @@ import React from "react"; import { Link } from "react-router-dom"; import { useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; -import { NavUtils, TextUtils } from "src/utils"; -import { - GridCard, - CountryFlag, - HoverPopover, - Icon, - TagLink, -} from "src/components/Shared"; +import NavUtils from "src/utils/navigation"; +import TextUtils from "src/utils/text"; +import { GridCard } from "../Shared/GridCard"; +import { CountryFlag } from "../Shared/CountryFlag"; +import { HoverPopover } from "../Shared/HoverPopover"; +import { Icon } from "../Shared/Icon"; +import { TagLink } from "../Shared/TagLink"; import { Button, ButtonGroup } from "react-bootstrap"; import { Criterion, @@ -19,6 +18,8 @@ import { PopoverCountButton } from "../Shared/PopoverCountButton"; import GenderIcon from "./GenderIcon"; import { faHeart, faTag } from "@fortawesome/free-solid-svg-icons"; import { RatingBanner } from "../Shared/RatingBanner"; +import cx from "classnames"; +import { usePerformerUpdate } from "src/core/StashService"; export interface IPerformerCardExtraCriteria { scenes: Criterion[]; @@ -61,17 +62,39 @@ export const PerformerCard: React.FC = ({ { age, years_old: ageL10String } ); - function maybeRenderFavoriteIcon() { - if (performer.favorite === false) { - return; - } + const [updatePerformer] = usePerformerUpdate(); + + function renderFavoriteIcon() { return ( -
- -
+ e.preventDefault()}> + + ); } + function onToggleFavorite(v: boolean) { + if (performer.id) { + updatePerformer({ + variables: { + input: { + id: performer.id, + favorite: v, + }, + }, + }); + } + } + function maybeRenderScenesPopoverButton() { if (!performer.scene_count) return; @@ -197,7 +220,16 @@ export const PerformerCard: React.FC = ({ pretitleIcon={ } - title={performer.name ?? ""} + title={ +
+ {performer.name} + {performer.disambiguation && ( + + {` (${performer.disambiguation})`} + + )} +
+ } image={ <> = ({ alt={performer.name ?? ""} src={performer.image_path ?? ""} /> - {maybeRenderFavoriteIcon()} + + {renderFavoriteIcon()} {maybeRenderRatingBanner()} {maybeRenderFlag()} @@ -217,9 +250,9 @@ export const PerformerCard: React.FC = ({ ) : ( "" )} - {maybeRenderPopoverButtonGroup()} } + popovers={maybeRenderPopoverButtonGroup()} selected={selected} selecting={selecting} onSelectedChanged={onSelectedChanged} diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index d3032762f..36b288e9a 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -12,17 +12,16 @@ import { usePerformerDestroy, mutateMetadataAutoTag, } from "src/core/StashService"; -import { - Counter, - CountryFlag, - DetailsEditNavbar, - ErrorMessage, - Icon, - LoadingIndicator, -} from "src/components/Shared"; -import { useLightbox, useToast } from "src/hooks"; +import { Counter } from "src/components/Shared/Counter"; +import { CountryFlag } from "src/components/Shared/CountryFlag"; +import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; +import { ErrorMessage } from "src/components/Shared/ErrorMessage"; +import { Icon } from "src/components/Shared/Icon"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { useLightbox } from "src/hooks/Lightbox/hooks"; +import { useToast } from "src/hooks/Toast"; import { ConfigurationContext } from "src/hooks/Config"; -import { TextUtils } from "src/utils"; +import TextUtils from "src/utils/text"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { PerformerDetailsPanel } from "./PerformerDetailsPanel"; import { PerformerScenesPanel } from "./PerformerScenesPanel"; @@ -32,13 +31,10 @@ import { PerformerImagesPanel } from "./PerformerImagesPanel"; import { PerformerEditPanel } from "./PerformerEditPanel"; import { PerformerSubmitButton } from "./PerformerSubmitButton"; import GenderIcon from "../GenderIcon"; -import { - faCamera, - faDove, - faHeart, - faLink, -} from "@fortawesome/free-solid-svg-icons"; +import { faHeart, faLink } from "@fortawesome/free-solid-svg-icons"; +import { faInstagram, faTwitter } from "@fortawesome/free-brands-svg-icons"; import { IUIConfig } from "src/core/config"; +import { useRatingKeybinds } from "src/hooks/keybinds"; interface IProps { performer: GQL.PerformerDataFragment; @@ -53,22 +49,24 @@ const PerformerPage: React.FC = ({ performer }) => { const intl = useIntl(); const { tab = "details" } = useParams(); + const [collapsed, setCollapsed] = useState(false); + // Configuration settings const { configuration } = React.useContext(ConfigurationContext); const abbreviateCounter = (configuration?.ui as IUIConfig)?.abbreviateCounters ?? false; - const [imagePreview, setImagePreview] = useState(); - const [imageEncoding, setImageEncoding] = useState(false); const [isEditing, setIsEditing] = useState(false); + const [image, setImage] = useState(); + const [encodingImage, setEncodingImage] = useState(false); // if undefined then get the existing image // if null then get the default (no) image // otherwise get the set image const activeImage = - imagePreview === undefined + image === undefined ? performer.image_path ?? "" - : imagePreview ?? `${performer.image_path}&default=true`; + : image ?? `${performer.image_path}&default=true`; const lightboxImages = useMemo( () => [{ paths: { thumbnail: activeImage, image: activeImage } }], [activeImage] @@ -95,10 +93,6 @@ const PerformerPage: React.FC = ({ performer }) => { } }; - const onImageChange = (image?: string | null) => setImagePreview(image); - - const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding); - async function onAutoTag() { try { await mutateMetadataAutoTag({ performers: [performer.id] }); @@ -110,6 +104,12 @@ const PerformerPage: React.FC = ({ performer }) => { } } + useRatingKeybinds( + true, + configuration?.ui?.ratingSystemOptions?.type, + setRating + ); + // set up hotkeys useEffect(() => { Mousetrap.bind("a", () => setActiveTabKey("details")); @@ -118,30 +118,7 @@ const PerformerPage: React.FC = ({ performer }) => { Mousetrap.bind("g", () => setActiveTabKey("galleries")); Mousetrap.bind("m", () => setActiveTabKey("movies")); Mousetrap.bind("f", () => setFavorite(!performer.favorite)); - - // numeric keypresses get caught by jwplayer, so blur the element - // if the rating sequence is started - Mousetrap.bind("r", () => { - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); - } - - Mousetrap.bind("0", () => setRating(NaN)); - Mousetrap.bind("1", () => setRating(20)); - Mousetrap.bind("2", () => setRating(40)); - Mousetrap.bind("3", () => setRating(60)); - Mousetrap.bind("4", () => setRating(80)); - Mousetrap.bind("5", () => setRating(100)); - - setTimeout(() => { - Mousetrap.unbind("0"); - Mousetrap.unbind("1"); - Mousetrap.unbind("2"); - Mousetrap.unbind("3"); - Mousetrap.unbind("4"); - Mousetrap.unbind("5"); - }, 1000); - }); + Mousetrap.bind(",", () => setCollapsed(!collapsed)); return () => { Mousetrap.unbind("a"); @@ -149,6 +126,7 @@ const PerformerPage: React.FC = ({ performer }) => { Mousetrap.unbind("c"); Mousetrap.unbind("f"); Mousetrap.unbind("o"); + Mousetrap.unbind(","); }; }); @@ -210,7 +188,10 @@ const PerformerPage: React.FC = ({ performer }) => { } > - + = ({ performer }) => { } > - + = ({ performer }) => { } > - + = ({ performer }) => { } > - + @@ -264,12 +254,9 @@ const PerformerPage: React.FC = ({ performer }) => { { - setIsEditing(false); - }} + onCancel={() => setIsEditing(false)} + setImage={setImage} + setEncodingImage={setEncodingImage} /> ); } else { @@ -296,13 +283,13 @@ const PerformerPage: React.FC = ({ performer }) => { } function maybeRenderAliases() { - if (performer?.aliases) { + if (performer?.alias_list?.length) { return (
{" "} - {performer.aliases} + {performer.alias_list?.join(", ")}
); } @@ -368,7 +355,7 @@ const PerformerPage: React.FC = ({ performer }) => { target="_blank" rel="noopener noreferrer" > - + )} @@ -383,7 +370,7 @@ const PerformerPage: React.FC = ({ performer }) => { target="_blank" rel="noopener noreferrer" > - + )} @@ -397,14 +384,22 @@ const PerformerPage: React.FC = ({ performer }) => { /> ); + function getCollapseButtonText() { + return collapsed ? ">" : "<"; + } + return (
{performer.name} -
- {imageEncoding ? ( +
+ {encodingImage ? ( ) : ( )}
-
+
+ +
+

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

{ - const [imagePreview, setImagePreview] = useState(); - const [imageEncoding, setImageEncoding] = useState(false); + const [image, setImage] = useState(); + const [encodingImage, setEncodingImage] = useState(false); - function useQuery() { - const { search } = useLocation(); - return React.useMemo(() => new URLSearchParams(search), [search]); - } + const location = useLocation(); + const query = useMemo(() => new URLSearchParams(location.search), [location]); + const performer = { + name: query.get("q") ?? undefined, + }; - const query = useQuery(); - const nameQuery = query.get("name"); - - const activeImage = imagePreview ?? ""; const intl = useIntl(); - const onImageChange = (image?: string | null) => setImagePreview(image); - const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding); - function renderPerformerImage() { - if (imageEncoding) { + if (encodingImage) { return ; } - if (activeImage) { + if (image) { return ( {intl.formatMessage({ ); @@ -50,11 +44,10 @@ const PerformerCreate: React.FC = () => { />
diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx index e8c4ffc3f..9a0aa9f07 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -1,8 +1,10 @@ import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { TagLink } from "src/components/Shared"; +import { TagLink } from "src/components/Shared/TagLink"; import * as GQL from "src/core/generated-graphql"; -import { TextUtils, getStashboxBase, getCountryByISO } from "src/utils"; +import TextUtils from "src/utils/text"; +import { getStashboxBase } from "src/utils/stashbox"; +import { getCountryByISO } from "src/utils/country"; import { TextField, URLField } from "src/utils/field"; import { cmToImperial, kgToLbs } from "src/utils/units"; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index d9370d9ad..b92330fad 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -13,26 +13,25 @@ import { useTagCreate, queryScrapePerformerURL, } from "src/core/StashService"; -import { - Icon, - ImageInput, - LoadingIndicator, - CollapseButton, - TagSelect, - URLField, - CountrySelect, -} from "src/components/Shared"; -import { ImageUtils, getStashIDs } from "src/utils"; -import { useToast } from "src/hooks"; +import { Icon } from "src/components/Shared/Icon"; +import { ImageInput } from "src/components/Shared/ImageInput"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { CollapseButton } from "src/components/Shared/CollapseButton"; +import { TagSelect } from "src/components/Shared/Select"; +import { CountrySelect } from "src/components/Shared/CountrySelect"; +import { URLField } from "src/components/Shared/URLField"; +import ImageUtils from "src/utils/image"; +import { getStashIDs } from "src/utils/stashIds"; +import { stashboxDisplayName } from "src/utils/stashbox"; +import { useToast } from "src/hooks/Toast"; import { Prompt, useHistory } from "react-router-dom"; import { useFormik } from "formik"; import { - genderStrings, genderToString, + stringGenderMap, stringToGender, } from "src/utils/gender"; import { ConfigurationContext } from "src/hooks/Config"; -import { stashboxDisplayName } from "src/utils/stashbox"; import { PerformerScrapeDialog } from "./PerformerScrapeDialog"; import PerformerScrapeModal from "./PerformerScrapeModal"; import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal"; @@ -42,6 +41,9 @@ import { faSyncAlt, faTrashAlt, } from "@fortawesome/free-solid-svg-icons"; +import { StringListInput } from "src/components/Shared/StringListInput"; +import isEqual from "lodash-es/isEqual"; +import { DateInput } from "src/components/Shared/DateInput"; const isScraper = ( scraper: GQL.Scraper | GQL.StashBox @@ -49,26 +51,26 @@ const isScraper = ( interface IPerformerDetails { performer: Partial; - isNew?: boolean; isVisible: boolean; - onImageChange?: (image?: string | null) => void; - onImageEncoding?: (loading?: boolean) => void; - onCancelEditing?: () => void; + onCancel?: () => void; + setImage: (image?: string | null) => void; + setEncodingImage: (loading: boolean) => void; } export const PerformerEditPanel: React.FC = ({ performer, - isNew, isVisible, - onImageChange, - onImageEncoding, - onCancelEditing, + onCancel, + setImage, + setEncodingImage, }) => { const Toast = useToast(); const history = useHistory(); + const isNew = performer.id === undefined; + // Editing state - const [scraper, setScraper] = useState(); + const [scraper, setScraper] = useState(); const [newTags, setNewTags] = useState(); const [isScraperModalOpen, setIsScraperModalOpen] = useState(false); @@ -81,18 +83,13 @@ export const PerformerEditPanel: React.FC = ({ const Scrapers = useListPerformerScrapers(); const [queryableScrapers, setQueryableScrapers] = useState([]); - const [scrapedPerformer, setScrapedPerformer] = useState< - GQL.ScrapedPerformer | undefined - >(); + const [scrapedPerformer, setScrapedPerformer] = + useState(); const { configuration: stashConfig } = React.useContext(ConfigurationContext); - const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true); - const [createTag] = useTagCreate(); const intl = useIntl(); - const genderOptions = [""].concat(genderStrings); - const labelXS = 3; const labelXL = 2; const fieldXS = 9; @@ -100,62 +97,102 @@ export const PerformerEditPanel: React.FC = ({ const schema = yup.object({ name: yup.string().required(), - aliases: yup.string().optional(), - gender: yup.string().optional().oneOf(genderOptions), - birthdate: yup.string().optional(), - ethnicity: yup.string().optional(), - eye_color: yup.string().optional(), - country: yup.string().optional(), - height_cm: yup.number().optional(), - measurements: yup.string().optional(), - fake_tits: yup.string().optional(), - career_length: yup.string().optional(), - tattoos: yup.string().optional(), - piercings: yup.string().optional(), - url: yup.string().optional(), - twitter: yup.string().optional(), - instagram: yup.string().optional(), - tag_ids: yup.array(yup.string().required()).optional(), - stash_ids: yup.mixed().optional(), - image: yup.string().optional().nullable(), - details: yup.string().optional(), - death_date: yup.string().optional(), - hair_color: yup.string().optional(), - weight: yup.number().optional(), - ignore_auto_tag: yup.boolean().optional(), + disambiguation: yup.string().ensure(), + alias_list: yup + .array(yup.string().required()) + .defined() + .test({ + name: "unique", + test: (value, context) => { + if (!value) return true; + const aliases = new Set(value); + aliases.add(context.parent.name); + return value.length + 1 === aliases.size; + }, + message: intl.formatMessage({ + id: "validation.aliases_must_be_unique", + }), + }), + gender: yup.string().ensure(), + birthdate: yup + .string() + .ensure() + .test({ + 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; + return true; + }, + message: intl.formatMessage({ id: "validation.date_invalid_form" }), + }), + death_date: yup + .string() + .ensure() + .test({ + 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; + return true; + }, + message: intl.formatMessage({ id: "validation.date_invalid_form" }), + }), + country: yup.string().ensure(), + ethnicity: yup.string().ensure(), + hair_color: yup.string().ensure(), + eye_color: yup.string().ensure(), + height_cm: yup.number().nullable().defined().default(null), + weight: yup.number().nullable().defined().default(null), + measurements: yup.string().ensure(), + fake_tits: yup.string().ensure(), + tattoos: yup.string().ensure(), + piercings: yup.string().ensure(), + career_length: yup.string().ensure(), + url: yup.string().ensure(), + twitter: yup.string().ensure(), + instagram: yup.string().ensure(), + details: yup.string().ensure(), + tag_ids: yup.array(yup.string().required()).defined(), + ignore_auto_tag: yup.boolean().defined(), + stash_ids: yup.mixed().defined(), + image: yup.string().nullable().optional(), }); const initialValues = { name: performer.name ?? "", - aliases: performer.aliases ?? "", - gender: genderToString(performer.gender ?? undefined), + disambiguation: performer.disambiguation ?? "", + alias_list: performer.alias_list ?? [], + gender: (performer.gender as GQL.GenderEnum) ?? "", birthdate: performer.birthdate ?? "", - ethnicity: performer.ethnicity ?? "", - eye_color: performer.eye_color ?? "", + death_date: performer.death_date ?? "", country: performer.country ?? "", - height_cm: performer.height_cm ?? undefined, + ethnicity: performer.ethnicity ?? "", + hair_color: performer.hair_color ?? "", + eye_color: performer.eye_color ?? "", + height_cm: performer.height_cm ?? null, + weight: performer.weight ?? null, measurements: performer.measurements ?? "", fake_tits: performer.fake_tits ?? "", - career_length: performer.career_length ?? "", tattoos: performer.tattoos ?? "", piercings: performer.piercings ?? "", + career_length: performer.career_length ?? "", url: performer.url ?? "", twitter: performer.twitter ?? "", instagram: performer.instagram ?? "", - tag_ids: (performer.tags ?? []).map((t) => t.id), - stash_ids: performer.stash_ids ?? undefined, - image: undefined, details: performer.details ?? "", - death_date: performer.death_date ?? "", - hair_color: performer.hair_color ?? "", - weight: performer.weight ?? undefined, + tag_ids: (performer.tags ?? []).map((t) => t.id), ignore_auto_tag: performer.ignore_auto_tag ?? false, + stash_ids: getStashIDs(performer.stash_ids), }; - type InputValues = typeof initialValues; + type InputValues = yup.InferType; - const formik = useFormik({ + const formik = useFormik({ initialValues, + enableReinitialize: true, validationSchema: schema, onSubmit: (values) => onSave(values), }); @@ -165,20 +202,16 @@ export const PerformerEditPanel: React.FC = ({ return; } - let retEnum: GQL.GenderEnum | undefined; - // try to translate from enum values first - const upperGender = scrapedGender?.toUpperCase(); + const upperGender = scrapedGender.toUpperCase(); const asEnum = genderToString(upperGender); if (asEnum) { - retEnum = stringToGender(asEnum); + return stringToGender(asEnum); } else { // try to match against gender strings const caseInsensitive = true; - retEnum = stringToGender(scrapedGender, caseInsensitive); + return stringToGender(scrapedGender, caseInsensitive); } - - return genderToString(retEnum); } function renderNewTags() { @@ -262,9 +295,14 @@ export const PerformerEditPanel: React.FC = ({ if (state.name) { formik.setFieldValue("name", state.name); } - + if (state.disambiguation) { + formik.setFieldValue("disambiguation", state.disambiguation); + } if (state.aliases) { - formik.setFieldValue("aliases", state.aliases); + formik.setFieldValue( + "alias_list", + state.aliases.split(",").map((a) => a.trim()) + ); } if (state.birthdate) { formik.setFieldValue("birthdate", state.birthdate); @@ -307,10 +345,10 @@ export const PerformerEditPanel: React.FC = ({ } if (state.gender) { // gender is a string in the scraper data - formik.setFieldValue( - "gender", - translateScrapedGender(state.gender ?? undefined) - ); + const newGender = translateScrapedGender(state.gender); + if (newGender) { + formik.setFieldValue("gender", newGender); + } } if (state.tags) { // map tags to their ids and filter out those not found @@ -323,15 +361,14 @@ export const PerformerEditPanel: React.FC = ({ // image is a base64 string // #404: don't overwrite image if it has been modified by the user // overwrite if not new since it came from a dialog - // overwrite if image was cleared (`null`) - // otherwise follow existing behaviour (`undefined`) + // overwrite if image is unset if ( - (!isNew || [null, undefined].includes(formik.values.image)) && + (!isNew || !formik.values.image) && state.images && state.images.length > 0 ) { const imageStr = state.images[0]; - formik.setFieldValue("image", imageStr ?? undefined); + formik.setFieldValue("image", imageStr); } if (state.details) { formik.setFieldValue("details", state.details); @@ -360,31 +397,50 @@ export const PerformerEditPanel: React.FC = ({ } } - function onImageLoad(imageData: string) { + const encodingImage = ImageUtils.usePasteImage(onImageLoad); + + useEffect(() => { + setImage(formik.values.image); + }, [formik.values.image, setImage]); + + useEffect(() => { + setEncodingImage(encodingImage); + }, [setEncodingImage, encodingImage]); + + function onImageLoad(imageData: string | null) { formik.setFieldValue("image", imageData); } - async function onSave(performerInput: InputValues) { + function onImageChange(event: React.FormEvent) { + ImageUtils.onImageChange(event, onImageLoad); + } + + async function onSave(input: InputValues) { setIsLoading(true); try { if (isNew) { - const input = getCreateValues(performerInput); const result = await createPerformer({ variables: { - input, + input: { + ...input, + gender: input.gender || null, + height_cm: input.height_cm || null, + weight: input.weight || null, + }, }, }); if (result.data?.performerCreate) { history.push(`/performers/${result.data.performerCreate.id}`); } } else { - const input = getUpdateValues(performerInput); - await updatePerformer({ variables: { input: { + id: performer.id!, ...input, - stash_ids: getStashIDs(performerInput?.stash_ids), + gender: input.gender || null, + height_cm: input.height_cm || null, + weight: input.weight || null, }, }, }); @@ -394,12 +450,17 @@ export const PerformerEditPanel: React.FC = ({ setIsLoading(false); return; } - if (!isNew && onCancelEditing) { - onCancelEditing(); + if (!isNew && onCancel) { + onCancel(); } setIsLoading(false); } + function onCancelEditing() { + setImage(undefined); + onCancel?.(); + } + // set up hotkeys useEffect(() => { if (isVisible) { @@ -417,18 +478,6 @@ export const PerformerEditPanel: React.FC = ({ } }); - useEffect(() => { - if (onImageChange) { - onImageChange(formik.values.image); - } - return () => onImageChange?.(); - }, [formik.values.image, onImageChange]); - - useEffect(() => onImageEncoding?.(imageEncoding), [ - onImageEncoding, - imageEncoding, - ]); - useEffect(() => { const newQueryableScrapers = ( Scrapers?.data?.listPerformerScrapers ?? [] @@ -441,33 +490,6 @@ export const PerformerEditPanel: React.FC = ({ if (isLoading) return ; - function getUpdateValues(values: InputValues): GQL.PerformerUpdateInput { - return { - ...values, - gender: stringToGender(values.gender) ?? null, - height_cm: values.height_cm ? Number(values.height_cm) : null, - weight: values.weight ? Number(values.weight) : null, - id: performer.id ?? "", - }; - } - - function getCreateValues(values: InputValues): GQL.PerformerCreateInput { - return { - ...values, - gender: stringToGender(values.gender), - height_cm: values.height_cm ? Number(values.height_cm) : null, - weight: values.weight ? Number(values.weight) : null, - }; - } - - function onImageChangeHandler(event: React.FormEvent) { - ImageUtils.onImageChange(event, onImageLoad); - } - - function onImageChangeURL(url: string) { - formik.setFieldValue("image", url); - } - async function onReloadScrapers() { setIsLoading(true); try { @@ -633,9 +655,9 @@ export const PerformerEditPanel: React.FC = ({ return; } - const currentPerformer: Partial = { + const currentPerformer = { ...formik.values, - gender: stringToGender(formik.values.gender), + gender: formik.values.gender || null, image: formik.values.image ?? performer.image_path, }; @@ -662,22 +684,16 @@ export const PerformerEditPanel: React.FC = ({ function renderButtons(classNames: string) { return (
- {!isNew && onCancelEditing ? ( - - ) : ( - "" - )} + ) : null} {renderScraperMenu()}
))} )}
- + ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx index b9c7f4316..5b508ea1f 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx @@ -1,11 +1,12 @@ import React, { useEffect, useRef, useState } from "react"; -import debounce from "lodash-es/debounce"; import { Button, Form } from "react-bootstrap"; import { useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; -import { Modal, LoadingIndicator } from "src/components/Shared"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { stashboxDisplayName } from "src/utils/stashbox"; +import { useDebouncedSetState } from "src/hooks/debounce"; const CLASSNAME = "PerformerScrapeModal"; const CLASSNAME_LIST = `${CLASSNAME}-list`; @@ -43,14 +44,12 @@ const PerformerStashBoxModal: React.FC = ({ const performers = data?.scrapeSinglePerformer ?? []; - const onInputChange = debounce((input: string) => { - setQuery(input); - }, 500); + const onInputChange = useDebouncedSetState(setQuery, 500); useEffect(() => inputRef.current?.focus(), []); return ( - = ({
  • ))} @@ -89,7 +89,7 @@ const PerformerStashBoxModal: React.FC = ({ query !== "" &&
    No results found.
    )} -
    + ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerList.tsx b/ui/v2.5/src/components/Performers/PerformerList.tsx index 0cd8902ae..cdfc37f09 100644 --- a/ui/v2.5/src/components/Performers/PerformerList.tsx +++ b/ui/v2.5/src/components/Performers/PerformerList.tsx @@ -3,33 +3,48 @@ import React, { useState } from "react"; import { useIntl } from "react-intl"; import { useHistory } from "react-router-dom"; import Mousetrap from "mousetrap"; -import { - FindPerformersQueryResult, - SlimPerformerDataFragment, -} from "src/core/generated-graphql"; +import * as GQL from "src/core/generated-graphql"; import { queryFindPerformers, + useFindPerformers, usePerformersDestroy, } from "src/core/StashService"; -import { usePerformersList } from "src/hooks"; -import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook"; +import { + makeItemList, + PersistanceLevel, + showWhenSelected, +} from "../List/ItemList"; import { ListFilterModel } from "src/models/list-filter/filter"; import { DisplayMode } from "src/models/list-filter/types"; -import { PerformerTagger } from "src/components/Tagger"; -import { ExportDialog, DeleteEntityDialog } from "src/components/Shared"; +import { PerformerTagger } from "../Tagger/performers/PerformerTagger"; +import { ExportDialog } from "../Shared/ExportDialog"; +import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog"; import { IPerformerCardExtraCriteria, PerformerCard } from "./PerformerCard"; import { PerformerListTable } from "./PerformerListTable"; import { EditPerformersDialog } from "./EditPerformersDialog"; +const PerformerItemList = makeItemList({ + filterMode: GQL.FilterMode.Performers, + useResult: useFindPerformers, + getItems(result: GQL.FindPerformersQueryResult) { + return result?.data?.findPerformers?.performers ?? []; + }, + getCount(result: GQL.FindPerformersQueryResult) { + return result?.data?.findPerformers?.count ?? 0; + }, +}); + interface IPerformerList { filterHook?: (filter: ListFilterModel) => ListFilterModel; persistState?: PersistanceLevel; + alterQuery?: boolean; extraCriteria?: IPerformerCardExtraCriteria; } export const PerformerList: React.FC = ({ filterHook, persistState, + alterQuery, extraCriteria, }) => { const intl = useIntl(); @@ -40,7 +55,7 @@ export const PerformerList: React.FC = ({ const otherOperations = [ { text: intl.formatMessage({ id: "actions.open_random" }), - onClick: getRandom, + onClick: openRandom, }, { text: intl.formatMessage({ id: "actions.export" }), @@ -53,18 +68,36 @@ export const PerformerList: React.FC = ({ }, ]; - const addKeybinds = ( - result: FindPerformersQueryResult, + function addKeybinds( + result: GQL.FindPerformersQueryResult, filter: ListFilterModel - ) => { + ) { Mousetrap.bind("p r", () => { - getRandom(result, filter); + openRandom(result, filter); }); 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 onExport() { setIsExportAll(false); @@ -76,96 +109,35 @@ export const PerformerList: React.FC = ({ setIsExportDialogOpen(true); } - function maybeRenderPerformerExportDialog(selectedIds: Set) { - if (isExportDialogOpen) { - return ( - <> - { - setIsExportDialogOpen(false); - }} - /> - - ); - } - } - - function renderEditPerformersDialog( - selectedPerformers: SlimPerformerDataFragment[], - onClose: (applied: boolean) => void + function renderContent( + result: GQL.FindPerformersQueryResult, + filter: ListFilterModel, + selectedIds: Set, + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void ) { - return ( - <> - - - ); - } - - const renderDeleteDialog = ( - selectedPerformers: SlimPerformerDataFragment[], - onClose: (confirmed: boolean) => void - ) => ( - - ); - - const listData = usePerformersList({ - otherOperations, - renderContent, - renderEditDialog: renderEditPerformersDialog, - filterHook, - addKeybinds, - selectable: true, - persistState, - renderDeleteDialog, - }); - - async function getRandom( - result: 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 && - singleResult.data && - singleResult.data.findPerformers && - singleResult.data.findPerformers.performers.length === 1 - ) { - const { id } = singleResult!.data!.findPerformers!.performers[0]!; - history.push(`/performers/${id}`); + function maybeRenderPerformerExportDialog() { + if (isExportDialogOpen) { + return ( + <> + setIsExportDialogOpen(false)} + /> + + ); } } - } - function renderContent( - result: FindPerformersQueryResult, - filter: ListFilterModel, - selectedIds: Set - ) { - if (!result.data?.findPerformers) { - return; - } - if (filter.displayMode === DisplayMode.Grid) { - return ( - <> - {maybeRenderPerformerExportDialog(selectedIds)} + function renderPerformers() { + if (!result.data?.findPerformers) return; + + if (filter.displayMode === DisplayMode.Grid) { + return (
    {result.data.findPerformers.performers.map((p) => ( = ({ selecting={selectedIds.size > 0} selected={selectedIds.has(p.id)} onSelectedChanged={(selected: boolean, shiftKey: boolean) => - listData.onSelectChange(p.id, selected, shiftKey) + onSelectChange(p.id, selected, shiftKey) } extraCriteria={extraCriteria} /> ))}
    - - ); - } - if (filter.displayMode === DisplayMode.List) { - return ( - - ); - } - if (filter.displayMode === DisplayMode.Tagger) { - return ( - - ); + ); + } + if (filter.displayMode === DisplayMode.List) { + return ( + + ); + } + if (filter.displayMode === DisplayMode.Tagger) { + return ( + + ); + } } + + return ( + <> + {maybeRenderPerformerExportDialog()} + {renderPerformers()} + + ); } - return listData.template; + function renderEditDialog( + selectedPerformers: GQL.SlimPerformerDataFragment[], + onClose: (applied: boolean) => void + ) { + return ( + + ); + } + + function renderDeleteDialog( + selectedPerformers: GQL.SlimPerformerDataFragment[], + onClose: (confirmed: boolean) => void + ) { + return ( + + ); + } + + return ( + + ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerListTable.tsx b/ui/v2.5/src/components/Performers/PerformerListTable.tsx index 74a5fabc1..1b2f858fd 100644 --- a/ui/v2.5/src/components/Performers/PerformerListTable.tsx +++ b/ui/v2.5/src/components/Performers/PerformerListTable.tsx @@ -5,8 +5,8 @@ import { useIntl } from "react-intl"; import { Button, Table } from "react-bootstrap"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; -import { Icon } from "src/components/Shared"; -import { NavUtils } from "src/utils"; +import { Icon } from "../Shared/Icon"; +import NavUtils from "src/utils/navigation"; import { faHeart } from "@fortawesome/free-solid-svg-icons"; import { cmToImperial } from "src/utils/units"; @@ -64,10 +64,17 @@ export const PerformerListTable: React.FC = ( -
    {performer.name}
    +
    + {performer.name} + {performer.disambiguation && ( + + {` (${performer.disambiguation})`} + + )} +
    - {performer.aliases ? performer.aliases : ""} + {performer.alias_list ? performer.alias_list.join(", ") : ""} {performer.favorite && (
    +
    diff --git a/ui/v2.5/src/components/ScenePlayer/index.ts b/ui/v2.5/src/components/ScenePlayer/index.ts deleted file mode 100644 index 04a525412..000000000 --- a/ui/v2.5/src/components/ScenePlayer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ScenePlayer"; diff --git a/ui/v2.5/src/components/ScenePlayer/styles.scss b/ui/v2.5/src/components/ScenePlayer/styles.scss index 76a8ba73e..8e717892f 100644 --- a/ui/v2.5/src/components/ScenePlayer/styles.scss +++ b/ui/v2.5/src/components/ScenePlayer/styles.scss @@ -382,7 +382,7 @@ $sceneTabWidth: 450px; } #scrubber-position-indicator { - background-color: #ccc; + background-color: rgba(255, 255, 255, 0.7); height: 20px; left: -100%; position: absolute; @@ -426,6 +426,14 @@ $sceneTabWidth: 450px; } } +.scrubber-heatmap { + background-size: 100% 100%; + height: 20px; + left: 0; + position: absolute; + right: 0; +} + .scrubber-tag { background-color: #000; cursor: pointer; diff --git a/ui/v2.5/src/components/ScenePlayer/track-activity.ts b/ui/v2.5/src/components/ScenePlayer/track-activity.ts index f4ed2eb49..6b5cbd051 100644 --- a/ui/v2.5/src/components/ScenePlayer/track-activity.ts +++ b/ui/v2.5/src/components/ScenePlayer/track-activity.ts @@ -10,12 +10,10 @@ class TrackActivityPlugin extends videojs.getPlugin("plugin") { incrementPlayCount: () => Promise = () => { return Promise.resolve(); }; - saveActivity: ( - resumeTime: number, - playDuration: number - ) => Promise = () => { - return Promise.resolve(); - }; + saveActivity: (resumeTime: number, playDuration: number) => Promise = + () => { + return Promise.resolve(); + }; private enabled = false; private playCountIncremented = false; diff --git a/ui/v2.5/src/components/Scenes/DeleteScenesDialog.tsx b/ui/v2.5/src/components/Scenes/DeleteScenesDialog.tsx index c98785e1c..6a17ff5bf 100644 --- a/ui/v2.5/src/components/Scenes/DeleteScenesDialog.tsx +++ b/ui/v2.5/src/components/Scenes/DeleteScenesDialog.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { Form } from "react-bootstrap"; import { useScenesDestroy } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; -import { Modal } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { useToast } from "src/hooks/Toast"; import { ConfigurationContext } from "src/hooks/Config"; import { FormattedMessage, useIntl } from "react-intl"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; @@ -126,7 +126,7 @@ export const DeleteScenesDialog: React.FC = ( } return ( - = ( onChange={() => setDeleteGenerated(!deleteGenerated)} /> - + ); }; diff --git a/ui/v2.5/src/components/Scenes/EditScenesDialog.tsx b/ui/v2.5/src/components/Scenes/EditScenesDialog.tsx index 34947060f..9fed7f54b 100644 --- a/ui/v2.5/src/components/Scenes/EditScenesDialog.tsx +++ b/ui/v2.5/src/components/Scenes/EditScenesDialog.tsx @@ -4,10 +4,11 @@ import { FormattedMessage, useIntl } from "react-intl"; import isEqual from "lodash-es/isEqual"; import { useBulkSceneUpdate } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; -import { StudioSelect, Modal } from "src/components/Shared"; -import { useToast } from "src/hooks"; -import { FormUtils } from "src/utils"; -import MultiSet from "../Shared/MultiSet"; +import { StudioSelect } from "../Shared/Select"; +import { ModalComponent } from "../Shared/Modal"; +import { MultiSet } from "../Shared/MultiSet"; +import { useToast } from "src/hooks/Toast"; +import FormUtils from "src/utils/form"; import { RatingSystem } from "../Shared/Rating/RatingSystem"; import { getAggregateInputIDs, @@ -32,10 +33,8 @@ export const EditScenesDialog: React.FC = ( const Toast = useToast(); const [rating100, setRating] = useState(); const [studioId, setStudioId] = useState(); - const [ - performerMode, - setPerformerMode, - ] = React.useState(GQL.BulkUpdateIdMode.Add); + const [performerMode, setPerformerMode] = + React.useState(GQL.BulkUpdateIdMode.Add); const [performerIds, setPerformerIds] = useState(); const [existingPerformerIds, setExistingPerformerIds] = useState(); const [tagMode, setTagMode] = React.useState( @@ -243,7 +242,7 @@ export const EditScenesDialog: React.FC = ( function render() { return ( - = ( /> - + ); } diff --git a/ui/v2.5/src/components/Scenes/SceneCard.tsx b/ui/v2.5/src/components/Scenes/SceneCard.tsx index b7b79c0e7..3205fdcb8 100644 --- a/ui/v2.5/src/components/Scenes/SceneCard.tsx +++ b/ui/v2.5/src/components/Scenes/SceneCard.tsx @@ -3,14 +3,13 @@ import { Button, ButtonGroup } from "react-bootstrap"; import { Link } from "react-router-dom"; import cx from "classnames"; import * as GQL from "src/core/generated-graphql"; -import { - Icon, - TagLink, - HoverPopover, - SweatDrops, - TruncatedText, -} from "src/components/Shared"; -import { NavUtils, TextUtils } from "src/utils"; +import { Icon } from "../Shared/Icon"; +import { TagLink } 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 { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; @@ -99,9 +98,8 @@ export const SceneCard: React.FC = ( ); // studio image is missing if it uses the default - const missingStudioImage = props.scene.studio?.image_path?.endsWith( - "?default=true" - ); + const missingStudioImage = + props.scene.studio?.image_path?.endsWith("?default=true"); const showStudioAsText = missingStudioImage || (configuration?.interface.showStudioAsText ?? false); diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx index 8ee4d992c..eef8db914 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx @@ -2,7 +2,7 @@ import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button } from "react-bootstrap"; import { useIntl } from "react-intl"; -import Icon from "src/components/Shared/Icon"; +import { Icon } from "src/components/Shared/Icon"; import { objectTitle } from "src/core/files"; import { SceneDataFragment } from "src/core/generated-graphql"; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx index 19d9e337d..8fdb7dfd7 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx @@ -2,7 +2,9 @@ import { faBan, faMinus } 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, LoadingIndicator, SweatDrops } from "src/components/Shared"; +import { Icon } from "src/components/Shared/Icon"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { SweatDrops } from "src/components/Shared/SweatDrops"; export interface IOCounterButtonProps { value: number; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/OrganizedButton.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/OrganizedButton.tsx index 63e82f9d2..4d0d7dd51 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/OrganizedButton.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/OrganizedButton.tsx @@ -1,7 +1,7 @@ import React from "react"; import cx from "classnames"; import { Button, Spinner } from "react-bootstrap"; -import Icon from "src/components/Shared/Icon"; +import { Icon } from "src/components/Shared/Icon"; import { defineMessages, useIntl } from "react-intl"; import { faBox } from "@fortawesome/free-solid-svg-icons"; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/QueueViewer.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/QueueViewer.tsx index 09b575a68..1f4b8f740 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/QueueViewer.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/QueueViewer.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import cx from "classnames"; import { Button, Form, Spinner } from "react-bootstrap"; -import Icon from "src/components/Shared/Icon"; +import { Icon } from "src/components/Shared/Icon"; import { useIntl } from "react-intl"; import { faChevronDown, diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index 1e7a8163f..21ba9d5c4 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -1,13 +1,5 @@ import { Tab, Nav, Dropdown, Button, ButtonGroup } from "react-bootstrap"; -import queryString from "query-string"; -import React, { - useEffect, - useState, - useMemo, - useContext, - lazy, - useRef, -} from "react"; +import React, { useEffect, useState, useMemo, useContext, useRef } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useParams, useLocation, useHistory, Link } from "react-router-dom"; import { Helmet } from "react-helmet"; @@ -24,8 +16,11 @@ import { queryFindScenesByID, } from "src/core/StashService"; -import Icon from "src/components/Shared/Icon"; -import { useToast } from "src/hooks"; +import { ErrorMessage } from "src/components/Shared/ErrorMessage"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { Icon } from "src/components/Shared/Icon"; +import { Counter } from "src/components/Shared/Counter"; +import { useToast } from "src/hooks/Toast"; import SceneQueue, { QueuedScene } from "src/models/sceneQueue"; import { ListFilterModel } from "src/models/list-filter/filter"; import Mousetrap from "mousetrap"; @@ -34,31 +29,39 @@ import { OrganizedButton } from "./OrganizedButton"; import { ConfigurationContext } from "src/hooks/Config"; import { getPlayerPosition } from "src/components/ScenePlayer/util"; import { faEllipsisV } from "@fortawesome/free-solid-svg-icons"; +import { lazyComponent } from "src/utils/lazyComponent"; -const SubmitStashBoxDraft = lazy( +const SubmitStashBoxDraft = lazyComponent( () => import("src/components/Dialogs/SubmitDraft") ); -const ScenePlayer = lazy( +const ScenePlayer = lazyComponent( () => import("src/components/ScenePlayer/ScenePlayer") ); -const GalleryViewer = lazy( +const GalleryViewer = lazyComponent( () => import("src/components/Galleries/GalleryViewer") ); -const ExternalPlayerButton = lazy(() => import("./ExternalPlayerButton")); +const ExternalPlayerButton = lazyComponent( + () => import("./ExternalPlayerButton") +); -const QueueViewer = lazy(() => import("./QueueViewer")); -const SceneMarkersPanel = lazy(() => import("./SceneMarkersPanel")); -const SceneFileInfoPanel = lazy(() => import("./SceneFileInfoPanel")); -const SceneEditPanel = lazy(() => import("./SceneEditPanel")); -const SceneDetailPanel = lazy(() => import("./SceneDetailPanel")); -const SceneMoviePanel = lazy(() => import("./SceneMoviePanel")); -const SceneGalleriesPanel = lazy(() => import("./SceneGalleriesPanel")); -const DeleteScenesDialog = lazy(() => import("../DeleteScenesDialog")); -const GenerateDialog = lazy(() => import("../../Dialogs/GenerateDialog")); -const SceneVideoFilterPanel = lazy(() => import("./SceneVideoFilterPanel")); +const QueueViewer = lazyComponent(() => import("./QueueViewer")); +const SceneMarkersPanel = lazyComponent(() => import("./SceneMarkersPanel")); +const SceneFileInfoPanel = lazyComponent(() => import("./SceneFileInfoPanel")); +const SceneEditPanel = lazyComponent(() => import("./SceneEditPanel")); +const SceneDetailPanel = lazyComponent(() => import("./SceneDetailPanel")); +const SceneMoviePanel = lazyComponent(() => import("./SceneMoviePanel")); +const SceneGalleriesPanel = lazyComponent( + () => import("./SceneGalleriesPanel") +); +const DeleteScenesDialog = lazyComponent(() => import("../DeleteScenesDialog")); +const GenerateDialog = lazyComponent( + () => import("../../Dialogs/GenerateDialog") +); +const SceneVideoFilterPanel = lazyComponent( + () => import("./SceneVideoFilterPanel") +); import { objectPath, objectTitle } from "src/core/files"; -import { Counter } from "src/components/Shared"; interface IProps { scene: GQL.SceneDataFragment; @@ -140,7 +143,9 @@ const ScenePage: React.FC = ({ Mousetrap.bind("e", () => setActiveTabKey("scene-edit-panel")); Mousetrap.bind("k", () => setActiveTabKey("scene-markers-panel")); Mousetrap.bind("i", () => setActiveTabKey("scene-file-info-panel")); - Mousetrap.bind("o", () => onIncrementClick()); + Mousetrap.bind("o", () => { + onIncrementClick(); + }); Mousetrap.bind("p n", () => onQueueNext()); Mousetrap.bind("p p", () => onQueuePrevious()); Mousetrap.bind("p r", () => onQueueRandom()); @@ -514,10 +519,10 @@ const SceneLoader: React.FC = () => { const location = useLocation(); const history = useHistory(); const { configuration } = useContext(ConfigurationContext); - const { data, loading } = useFindScene(id ?? ""); + const { data, loading, error } = useFindScene(id ?? ""); const queryParams = useMemo( - () => queryString.parse(location.search, { decode: false }), + () => new URLSearchParams(location.search), [location.search] ); const sceneQueue = useMemo( @@ -525,13 +530,13 @@ const SceneLoader: React.FC = () => { [queryParams] ); const queryContinue = useMemo(() => { - let cont = queryParams.continue; - if (cont !== undefined) { + let cont = queryParams.get("continue"); + if (cont) { return cont === "true"; } else { return !!configuration?.interface.continuePlaylistDefault; } - }, [configuration?.interface.continuePlaylistDefault, queryParams.continue]); + }, [configuration?.interface.continuePlaylistDefault, queryParams]); const [queueScenes, setQueueScenes] = useState([]); @@ -543,14 +548,13 @@ const SceneLoader: React.FC = () => { const _setTimestamp = useRef<(value: number) => void>(); const initialTimestamp = useMemo(() => { - const t = Array.isArray(queryParams.t) ? queryParams.t[0] : queryParams.t; - return Number.parseInt(t ?? "0", 10); + return Number.parseInt(queryParams.get("t") ?? "0", 10); }, [queryParams]); const [queueTotal, setQueueTotal] = useState(0); const [queueStart, setQueueStart] = useState(1); - const autoplay = queryParams.autoplay === "true"; + const autoplay = queryParams.get("autoplay") === "true"; const currentQueueIndex = queueScenes ? queueScenes.findIndex((s) => s.id === id) : -1; @@ -728,32 +732,32 @@ const SceneLoader: React.FC = () => { } } + if (loading) return ; + if (error) return ; + const scene = data?.findScene; + if (!scene) return ; return (
    - {!loading && scene ? ( - - ) : ( -
    - )} +
    { const intl = useIntl(); - // create scene from provided scene id if applicable - const queryParams = queryString.parse(location.search); + const location = useLocation(); + const query = useMemo(() => new URLSearchParams(location.search), [location]); - const fromSceneID = (queryParams?.from_scene_id ?? "") as string; - const { data, loading } = useFindScene(fromSceneID ?? ""); + // create scene from provided scene id if applicable + const { data, loading } = useFindScene(query.get("from_scene_id") ?? ""); const [loadingCoverImage, setLoadingCoverImage] = useState(false); - const [coverImage, setCoverImage] = useState(undefined); + const [coverImage, setCoverImage] = useState(); const scene = useMemo(() => { if (data?.findScene) { @@ -26,8 +26,10 @@ const SceneCreate: React.FC = () => { }; } - return {}; - }, [data?.findScene]); + return { + title: query.get("q") ?? undefined, + }; + }, [data?.findScene, query]); useEffect(() => { async function fetchCoverImage() { @@ -62,6 +64,7 @@ const SceneCreate: React.FC = () => {
    import("./SceneScrapeDialog")); -const SceneQueryModal = lazy(() => import("./SceneQueryModal")); +const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog")); +const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal")); interface IProps { scene: Partial; + fileID?: string; initialCoverImage?: string; isNew?: boolean; isVisible: boolean; @@ -61,6 +68,7 @@ interface IProps { export const SceneEditPanel: React.FC = ({ scene, + fileID, initialCoverImage, isNew = false, isVisible, @@ -70,10 +78,6 @@ export const SceneEditPanel: React.FC = ({ const Toast = useToast(); const history = useHistory(); - const queryParams = queryString.parse(location.search); - - const fileID = (queryParams?.file_id ?? "") as string; - const [galleries, setGalleries] = useState<{ id: string; title: string }[]>( [] ); @@ -82,17 +86,13 @@ export const SceneEditPanel: React.FC = ({ const [fragmentScrapers, setFragmentScrapers] = useState([]); const [queryableScrapers, setQueryableScrapers] = useState([]); - const [scraper, setScraper] = useState(); - const [ - isScraperQueryModalOpen, - setIsScraperQueryModalOpen, - ] = useState(false); + const [scraper, setScraper] = useState(); + const [isScraperQueryModalOpen, setIsScraperQueryModalOpen] = + useState(false); const [scrapedScene, setScrapedScene] = useState(); - const [endpoint, setEndpoint] = useState(); + const [endpoint, setEndpoint] = useState(); - const [coverImagePreview, setCoverImagePreview] = useState< - string | undefined - >(); + const [coverImagePreview, setCoverImagePreview] = useState(); useEffect(() => { setCoverImagePreview( @@ -104,7 +104,7 @@ export const SceneEditPanel: React.FC = ({ setGalleries( scene.galleries?.map((g) => ({ id: g.id, - title: objectTitle(g), + title: galleryTitle(g), })) ?? [] ); }, [scene.galleries]); @@ -117,53 +117,66 @@ export const SceneEditPanel: React.FC = ({ const [updateScene] = useSceneUpdate(); const schema = yup.object({ - title: yup.string().optional().nullable(), - code: yup.string().optional().nullable(), - details: yup.string().optional().nullable(), - director: yup.string().optional().nullable(), - url: yup.string().optional().nullable(), - date: yup.string().optional().nullable(), - rating100: yup.number().optional().nullable(), - gallery_ids: yup.array(yup.string().required()).optional().nullable(), - studio_id: yup.string().optional().nullable(), - performer_ids: yup.array(yup.string().required()).optional().nullable(), + title: yup.string().ensure(), + code: yup.string().ensure(), + url: yup.string().ensure(), + date: yup + .string() + .ensure() + .test({ + 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; + return true; + }, + message: intl.formatMessage({ id: "validation.date_invalid_form" }), + }), + director: yup.string().ensure(), + rating100: yup.number().nullable().defined(), + gallery_ids: yup.array(yup.string().required()).defined(), + studio_id: yup.string().required().nullable(), + performer_ids: yup.array(yup.string().required()).defined(), movies: yup - .object({ - movie_id: yup.string().required(), - scene_index: yup.string().optional().nullable(), - }) - .optional() - .nullable(), - tag_ids: yup.array(yup.string().required()).optional().nullable(), - cover_image: yup.string().optional().nullable(), - stash_ids: yup.mixed().optional().nullable(), + .array( + yup.object({ + movie_id: yup.string().required(), + scene_index: yup.number().nullable().defined(), + }) + ) + .defined(), + tag_ids: yup.array(yup.string().required()).defined(), + stash_ids: yup.mixed().defined(), + details: yup.string().ensure(), + cover_image: yup.string().nullable().optional(), }); const initialValues = useMemo( () => ({ title: scene.title ?? "", code: scene.code ?? "", - details: scene.details ?? "", - director: scene.director ?? "", url: scene.url ?? "", date: scene.date ?? "", + director: scene.director ?? "", rating100: scene.rating100 ?? null, gallery_ids: (scene.galleries ?? []).map((g) => g.id), - studio_id: scene.studio?.id, + studio_id: scene.studio?.id ?? null, performer_ids: (scene.performers ?? []).map((p) => p.id), movies: (scene.movies ?? []).map((m) => { - return { movie_id: m.movie.id, scene_index: m.scene_index }; + return { movie_id: m.movie.id, scene_index: m.scene_index ?? null }; }), tag_ids: (scene.tags ?? []).map((t) => t.id), - cover_image: initialCoverImage, stash_ids: getStashIDs(scene.stash_ids), + details: scene.details ?? "", + cover_image: initialCoverImage, }), [scene, initialCoverImage] ); - type InputValues = typeof initialValues; + type InputValues = yup.InferType; - const formik = useFormik({ + const formik = useFormik({ initialValues, enableReinitialize: true, validationSchema: schema, @@ -187,6 +200,12 @@ export const SceneEditPanel: React.FC = ({ ); } + useRatingKeybinds( + isVisible, + stashConfig?.ui?.ratingSystemOptions?.type, + setRating + ); + useEffect(() => { if (isVisible) { Mousetrap.bind("s s", () => { @@ -198,35 +217,9 @@ export const SceneEditPanel: React.FC = ({ } }); - // numeric keypresses get caught by jwplayer, so blur the element - // if the rating sequence is started - Mousetrap.bind("r", () => { - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); - } - - Mousetrap.bind("0", () => setRating(NaN)); - Mousetrap.bind("1", () => setRating(20)); - Mousetrap.bind("2", () => setRating(40)); - Mousetrap.bind("3", () => setRating(60)); - Mousetrap.bind("4", () => setRating(80)); - Mousetrap.bind("5", () => setRating(100)); - - setTimeout(() => { - Mousetrap.unbind("0"); - Mousetrap.unbind("1"); - Mousetrap.unbind("2"); - Mousetrap.unbind("3"); - Mousetrap.unbind("4"); - Mousetrap.unbind("5"); - }, 1000); - }); - return () => { Mousetrap.unbind("s s"); Mousetrap.unbind("d d"); - - Mousetrap.unbind("r"); }; } }); @@ -245,15 +238,6 @@ export const SceneEditPanel: React.FC = ({ setQueryableScrapers(newQueryableScrapers); }, [Scrapers, stashConfig]); - const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true); - - function getSceneInput(input: InputValues): GQL.SceneUpdateInput { - return { - id: scene.id!, - ...input, - }; - } - function setMovieIds(movieIds: string[]) { const existingMovies = formik.values.movies; @@ -265,29 +249,22 @@ export const SceneEditPanel: React.FC = ({ return { movie_id: m, + scene_index: null, }; }); formik.setFieldValue("movies", newMovies); } - function getCreateValues(values: InputValues): GQL.SceneCreateInput { - return { - ...values, - }; - } - async function onSave(input: InputValues) { setIsLoading(true); try { if (!isNew) { - const updateValues = getSceneInput(input); const result = await updateScene({ variables: { input: { - ...updateValues, id: scene.id!, - rating100: input.rating100 ?? null, + ...input, }, }, }); @@ -300,20 +277,17 @@ export const SceneEditPanel: React.FC = ({ } ), }); + formik.resetForm(); } } else { - const createValues = getCreateValues(input); const result = await mutateCreateScene({ - ...createValues, - file_ids: fileID ? [fileID as string] : undefined, + ...input, + file_ids: fileID ? [fileID] : undefined, }); if (result.data?.sceneCreate?.id) { history.push(`/scenes/${result.data?.sceneCreate.id}`); } } - - // clear the cover image so that it doesn't appear dirty - formik.resetForm({ values: formik.values }); } catch (e) { Toast.error(e); } @@ -341,6 +315,8 @@ export const SceneEditPanel: React.FC = ({ ); } + const encodingImage = ImageUtils.usePasteImage(onImageLoad); + function onImageLoad(imageData: string) { setCoverImagePreview(imageData); formik.setFieldValue("cover_image", imageData); @@ -434,9 +410,13 @@ export const SceneEditPanel: React.FC = ({ return; } - const currentScene = getSceneInput(formik.values); + const currentScene = { + id: scene.id!, + ...formik.values, + }; + if (!currentScene.cover_image) { - currentScene.cover_image = scene.paths!.screenshot; + currentScene.cover_image = scene.paths?.screenshot; } return ( @@ -691,7 +671,7 @@ export const SceneEditPanel: React.FC = ({ function renderTextField(field: string, title: string, placeholder?: string) { return ( - + {FormUtils.renderLabel({ title, })} @@ -702,13 +682,16 @@ export const SceneEditPanel: React.FC = ({ {...formik.getFieldProps(field)} isInvalid={!!formik.getFieldMeta(field).error} /> + + {formik.getFieldMeta(field).error} + ); } const image = useMemo(() => { - if (imageEncoding) { + if (encodingImage) { return ; } @@ -723,7 +706,7 @@ export const SceneEditPanel: React.FC = ({ } return
    ; - }, [imageEncoding, coverImagePreview, intl]); + }, [encodingImage, coverImagePreview, intl]); if (isLoading) return ; @@ -742,7 +725,9 @@ export const SceneEditPanel: React.FC = ({ - )} - + {renderGalleries(scene)} ); }; @@ -144,7 +144,7 @@ export const SceneListTable: React.FC = ( - + diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx index 9958b0432..af6635940 100644 --- a/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx @@ -2,25 +2,41 @@ import cloneDeep from "lodash-es/cloneDeep"; import React from "react"; import { useHistory } from "react-router-dom"; import { useIntl } from "react-intl"; -import { Helmet } from "react-helmet"; -import { TITLE_SUFFIX } from "src/components/Shared"; import Mousetrap from "mousetrap"; -import { FindSceneMarkersQueryResult } from "src/core/generated-graphql"; -import { queryFindSceneMarkers } from "src/core/StashService"; -import { NavUtils } from "src/utils"; -import { useSceneMarkersList } from "src/hooks"; -import { PersistanceLevel } from "src/hooks/ListHook"; +import * as GQL from "src/core/generated-graphql"; +import { + queryFindSceneMarkers, + useFindSceneMarkers, +} from "src/core/StashService"; +import NavUtils from "src/utils/navigation"; +import { makeItemList, PersistanceLevel } from "../List/ItemList"; import { ListFilterModel } from "src/models/list-filter/filter"; import { DisplayMode } from "src/models/list-filter/types"; import { WallPanel } from "../Wall/WallPanel"; +const SceneMarkerItemList = makeItemList({ + filterMode: GQL.FilterMode.SceneMarkers, + useResult: useFindSceneMarkers, + getItems(result: GQL.FindSceneMarkersQueryResult) { + return result?.data?.findSceneMarkers?.scene_markers ?? []; + }, + getCount(result: GQL.FindSceneMarkersQueryResult) { + return result?.data?.findSceneMarkers?.count ?? 0; + }, +}); + interface ISceneMarkerList { filterHook?: (filter: ListFilterModel) => ListFilterModel; + alterQuery?: boolean; } -export const SceneMarkerList: React.FC = ({ filterHook }) => { +export const SceneMarkerList: React.FC = ({ + filterHook, + alterQuery, +}) => { const intl = useIntl(); const history = useHistory(); + const otherOperations = [ { text: intl.formatMessage({ id: "actions.play_random" }), @@ -28,10 +44,10 @@ export const SceneMarkerList: React.FC = ({ filterHook }) => { }, ]; - const addKeybinds = ( - result: FindSceneMarkersQueryResult, + function addKeybinds( + result: GQL.FindSceneMarkersQueryResult, filter: ListFilterModel - ) => { + ) { Mousetrap.bind("p r", () => { playRandom(result, filter); }); @@ -39,18 +55,10 @@ export const SceneMarkerList: React.FC = ({ filterHook }) => { return () => { Mousetrap.unbind("p r"); }; - }; - - const listData = useSceneMarkersList({ - otherOperations, - renderContent, - filterHook, - addKeybinds, - persistState: PersistanceLevel.ALL, - }); + } async function playRandom( - result: FindSceneMarkersQueryResult, + result: GQL.FindSceneMarkersQueryResult, filter: ListFilterModel ) { // query for a random scene @@ -62,7 +70,7 @@ export const SceneMarkerList: React.FC = ({ filterHook }) => { filterCopy.itemsPerPage = 1; filterCopy.currentPage = index + 1; const singleResult = await queryFindSceneMarkers(filterCopy); - if (singleResult?.data?.findSceneMarkers?.scene_markers?.length === 1) { + if (singleResult.data.findSceneMarkers.scene_markers.length === 1) { // navigate to the scene player page const url = NavUtils.makeSceneMarkerUrl( singleResult.data.findSceneMarkers.scene_markers[0] @@ -73,29 +81,27 @@ export const SceneMarkerList: React.FC = ({ filterHook }) => { } function renderContent( - result: FindSceneMarkersQueryResult, + result: GQL.FindSceneMarkersQueryResult, filter: ListFilterModel ) { - if (!result?.data?.findSceneMarkers) return; + if (!result.data?.findSceneMarkers) return; + if (filter.displayMode === DisplayMode.Wall) { return ( ); } } - const title_template = `${intl.formatMessage({ - id: "markers", - })} ${TITLE_SUFFIX}`; return ( - <> - - - {listData.template} - + ); }; diff --git a/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx b/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx index dfbeacb74..8f80ca1de 100644 --- a/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx @@ -1,18 +1,15 @@ import { Form, Col, Row, Button, FormControl } from "react-bootstrap"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import * as GQL from "src/core/generated-graphql"; -import { - GallerySelect, - Icon, - LoadingIndicator, - Modal, - SceneSelect, - StringListSelect, -} from "src/components/Shared"; -import { FormUtils, ImageUtils, TextUtils } from "src/utils"; +import { Icon } from "../Shared/Icon"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; +import { StringListSelect, GallerySelect, SceneSelect } from "../Shared/Select"; +import FormUtils from "src/utils/form"; +import ImageUtils from "src/utils/image"; +import TextUtils from "src/utils/text"; import { mutateSceneMerge, queryFindScenesByID } from "src/core/StashService"; import { FormattedMessage, useIntl } from "react-intl"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; import { faExchangeAlt, faSignInAlt } from "@fortawesome/free-solid-svg-icons"; import { hasScrapedValues, @@ -22,6 +19,7 @@ import { ScrapedInputGroupRow, ScrapedTextAreaRow, ScrapeResult, + ZeroableScrapeResult, } from "../Shared/ScrapeDialog"; import { clone, uniq } from "lodash-es"; import { @@ -32,6 +30,7 @@ import { } from "./SceneDetails/SceneScrapeDialog"; import { galleryTitle } from "src/core/galleries"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; +import { ModalComponent } from "../Shared/Modal"; interface IStashIDsField { values: GQL.StashId[]; @@ -67,8 +66,9 @@ const SceneMergeDetails: React.FC = ({ ); const [rating, setRating] = useState( - new ScrapeResult(dest.rating100) + new ZeroableScrapeResult(dest.rating100) ); + // zero values can be treated as missing for these fields const [oCounter, setOCounter] = useState( new ScrapeResult(dest.o_counter) ); @@ -119,6 +119,10 @@ const SceneMergeDetails: React.FC = ({ const [stashIDs, setStashIDs] = useState(new ScrapeResult([])); + const [organized, setOrganized] = useState( + new ZeroableScrapeResult(dest.organized) + ); + const [image, setImage] = useState>( new ScrapeResult(dest.paths.screenshot) ); @@ -133,7 +137,7 @@ const SceneMergeDetails: React.FC = ({ setLoading(true); const destData = await ImageUtils.imageToDataURL(dest.paths.screenshot); - const srcData = await ImageUtils.imageToDataURL(src.paths!.screenshot!); + const srcData = await ImageUtils.imageToDataURL(src.paths.screenshot!); // keep destination image by default const useNewValue = false; @@ -230,6 +234,13 @@ const SceneMergeDetails: React.FC = ({ ) ); + setOrganized( + new ScrapeResult( + dest.organized ?? false, + sources.every((s) => s.organized) + ) + ); + setStashIDs( new ScrapeResult( dest.stash_ids, @@ -289,6 +300,7 @@ const SceneMergeDetails: React.FC = ({ movies, tags, details, + organized, stashIDs, image, ]); @@ -304,6 +316,7 @@ const SceneMergeDetails: React.FC = ({ movies, tags, details, + organized, stashIDs, image, ]); @@ -325,6 +338,9 @@ const SceneMergeDetails: React.FC = ({ ); } + const trueString = intl.formatMessage({ id: "true" }); + const falseString = intl.formatMessage({ id: "false" }); + return ( <> = ({ result={details} onChange={(value) => setDetails(value)} /> + ( + {}} + className="bg-secondary text-white border-secondary" + /> + )} + renderNewField={() => ( + {}} + className="bg-secondary text-white border-secondary" + /> + )} + onChange={(value) => setOrganized(value)} + /> = ({ }), tag_ids: tags.getNewValue(), details: details.getNewValue(), + organized: organized.getNewValue(), stash_ids: stashIDs.getNewValue(), cover_image: coverImage, }; @@ -658,7 +696,7 @@ export const SceneMergeModal: React.FC = ({ } return ( - = ({
    - + ); }; diff --git a/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx b/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx index 2d9aa7371..86ae558f0 100644 --- a/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx +++ b/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx @@ -1,6 +1,6 @@ -import React, { FunctionComponent, useMemo } from "react"; +import React, { useMemo } from "react"; import { useFindScenes } from "src/core/StashService"; -import Slider from "react-slick"; +import Slider from "@ant-design/react-slick"; import { SceneCard } from "./SceneCard"; import { SceneQueue } from "src/models/sceneQueue"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -14,9 +14,7 @@ interface IProps { header: string; } -export const SceneRecommendationRow: FunctionComponent = ( - props: IProps -) => { +export const SceneRecommendationRow: React.FC = (props) => { const result = useFindScenes(props.filter); const cardCount = result.data?.findScenes.count; diff --git a/ui/v2.5/src/components/Scenes/Scenes.tsx b/ui/v2.5/src/components/Scenes/Scenes.tsx index 97a9ea5c6..ea966e74a 100644 --- a/ui/v2.5/src/components/Scenes/Scenes.tsx +++ b/ui/v2.5/src/components/Scenes/Scenes.tsx @@ -1,14 +1,15 @@ -import React, { lazy } from "react"; +import React from "react"; import { Route, Switch } from "react-router-dom"; import { useIntl } from "react-intl"; import { Helmet } from "react-helmet"; -import { TITLE_SUFFIX } from "src/components/Shared"; -import { PersistanceLevel } from "src/hooks/ListHook"; +import { TITLE_SUFFIX } from "src/components/Shared/constants"; +import { PersistanceLevel } from "../List/ItemList"; +import { lazyComponent } from "src/utils/lazyComponent"; -const SceneList = lazy(() => import("./SceneList")); -const SceneMarkerList = lazy(() => import("./SceneMarkerList")); -const Scene = lazy(() => import("./SceneDetails/Scene")); -const SceneCreate = lazy(() => import("./SceneDetails/SceneCreate")); +const SceneList = lazyComponent(() => import("./SceneList")); +const SceneMarkerList = lazyComponent(() => import("./SceneMarkerList")); +const Scene = lazyComponent(() => import("./SceneDetails/Scene")); +const SceneCreate = lazyComponent(() => import("./SceneDetails/SceneCreate")); const Scenes: React.FC = () => { const intl = useIntl(); @@ -16,6 +17,10 @@ const Scenes: React.FC = () => { const title_template = `${intl.formatMessage({ id: "scenes", })} ${TITLE_SUFFIX}`; + const marker_title_template = `${intl.formatMessage({ + id: "markers", + })} ${TITLE_SUFFIX}`; + return ( <> { )} /> - + ( + <> + + + + )} + /> diff --git a/ui/v2.5/src/components/Scenes/styles.scss b/ui/v2.5/src/components/Scenes/styles.scss index 20d073024..53b1a205b 100644 --- a/ui/v2.5/src/components/Scenes/styles.scss +++ b/ui/v2.5/src/components/Scenes/styles.scss @@ -463,7 +463,7 @@ input[type="range"].blue-slider { } @media (min-width: 1200px), (max-width: 575px) { - .performer-card .flag-icon { + .performer-card .fi { height: 1.33rem; width: 2rem; } diff --git a/ui/v2.5/src/components/Settings/Inputs.tsx b/ui/v2.5/src/components/Settings/Inputs.tsx index 8e70b095e..9ecf1d54c 100644 --- a/ui/v2.5/src/components/Settings/Inputs.tsx +++ b/ui/v2.5/src/components/Settings/Inputs.tsx @@ -1,9 +1,8 @@ import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; -import React, { useState } from "react"; +import React, { PropsWithChildren, useState } from "react"; import { Button, Collapse, Form, Modal, ModalProps } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; -import { PropsWithChildren } from "react-router/node_modules/@types/react"; -import { Icon } from "../Shared"; +import { Icon } from "../Shared/Icon"; import { StringListInput } from "../Shared/StringListInput"; interface ISetting { @@ -154,7 +153,7 @@ export const BooleanSetting: React.FC = (props) => { }; interface ISelectSetting extends ISetting { - value?: string | number | string[] | undefined; + value?: string | number | string[]; onChange: (v: string) => void; } @@ -429,7 +428,7 @@ export const StringListSetting: React.FC = (props) => { )} renderValue={(value) => ( diff --git a/ui/v2.5/src/components/Settings/SettingSection.tsx b/ui/v2.5/src/components/Settings/SettingSection.tsx index a14ab4c8d..41d365088 100644 --- a/ui/v2.5/src/components/Settings/SettingSection.tsx +++ b/ui/v2.5/src/components/Settings/SettingSection.tsx @@ -1,7 +1,6 @@ -import React from "react"; +import React, { PropsWithChildren } from "react"; import { Card } from "react-bootstrap"; import { useIntl } from "react-intl"; -import { PropsWithChildren } from "react-router/node_modules/@types/react"; interface ISettingGroup { id?: string; diff --git a/ui/v2.5/src/components/Settings/Settings.tsx b/ui/v2.5/src/components/Settings/Settings.tsx index 1a408dcc1..1b1149592 100644 --- a/ui/v2.5/src/components/Settings/Settings.tsx +++ b/ui/v2.5/src/components/Settings/Settings.tsx @@ -1,10 +1,9 @@ import React from "react"; -import queryString from "query-string"; import { Tab, Nav, Row, Col } from "react-bootstrap"; import { useHistory, useLocation } from "react-router-dom"; import { FormattedMessage, useIntl } from "react-intl"; import { Helmet } from "react-helmet"; -import { TITLE_SUFFIX } from "src/components/Shared"; +import { TITLE_SUFFIX } from "src/components/Shared/constants"; import { SettingsAboutPanel } from "./SettingsAboutPanel"; import { SettingsConfigurationPanel } from "./SettingsSystemPanel"; import { SettingsInterfacePanel } from "./SettingsInterfacePanel/SettingsInterfacePanel"; @@ -23,7 +22,7 @@ export const Settings: React.FC = () => { const intl = useIntl(); const location = useLocation(); const history = useHistory(); - const defaultTab = queryString.parse(location.search).tab ?? "tasks"; + const defaultTab = new URLSearchParams(location.search).get("tab") ?? "tasks"; const onSelect = (val: string) => history.push(`?tab=${val}`); diff --git a/ui/v2.5/src/components/Settings/SettingsAboutPanel.tsx b/ui/v2.5/src/components/Settings/SettingsAboutPanel.tsx index 320146ae2..13d8edde7 100644 --- a/ui/v2.5/src/components/Settings/SettingsAboutPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsAboutPanel.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Button } from "react-bootstrap"; import { useIntl } from "react-intl"; import { useLatestVersion } from "src/core/StashService"; -import { ConstantSetting, Setting, SettingGroup } from "./Inputs"; +import { ConstantSetting, SettingGroup } from "./Inputs"; import { SettingSection } from "./SettingSection"; export const SettingsAboutPanel: React.FC = () => { @@ -20,7 +20,69 @@ export const SettingsAboutPanel: React.FC = () => { networkStatus, } = useLatestVersion(); - const hasNew = dataLatest && gitHash !== dataLatest.latestversion.shorthash; + function renderLatestVersion() { + if (errorLatest) { + return ( + + ); + } else if (!dataLatest || loadingLatest || networkStatus === 4) { + return ( + + ); + } else { + let heading = dataLatest.latestversion.version; + const hashString = dataLatest.latestversion.shorthash; + if (gitHash !== hashString) { + heading += + " " + + intl.formatMessage({ + id: "config.about.new_version_notice", + }); + } + return ( + + + + + ); + } + } return ( <> @@ -39,48 +101,10 @@ export const SettingsAboutPanel: React.FC = () => { value={buildTime} /> + - - {errorLatest ? ( - - ) : !dataLatest || loadingLatest || networkStatus === 4 ? ( - - ) : ( -
    -
    -

    - {intl.formatMessage({ - id: "config.about.latest_version_build_hash", - })} -

    -
    - {dataLatest.latestversion.shorthash}{" "} - {hasNew - ? intl.formatMessage({ - id: "config.about.new_version_notice", - }) - : undefined} -
    -
    -
    - - - - -
    -
    - )} -
    + + {renderLatestVersion()} @@ -108,11 +132,11 @@ export const SettingsAboutPanel: React.FC = () => { { url: ( - Wiki + Documentation ), } diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index 8e5fb1286..97866b4d4 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -1,11 +1,9 @@ import React from "react"; import { Button, Form } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; -import { - DurationInput, - PercentInput, - LoadingIndicator, -} from "src/components/Shared"; +import { DurationInput } from "src/components/Shared/DurationInput"; +import { PercentInput } from "src/components/Shared/PercentInput"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { CheckboxGroup } from "./CheckboxGroup"; import { SettingSection } from "../SettingSection"; import { @@ -16,13 +14,13 @@ import { StringSetting, } from "../Inputs"; import { SettingStateContext } from "../context"; -import { DurationUtils } from "src/utils"; +import DurationUtils from "src/utils/duration"; import * as GQL from "src/core/generated-graphql"; import { imageLightboxDisplayModeIntlMap, imageLightboxScrollModeIntlMap, } from "src/core/enums"; -import { useInterfaceLocalForage } from "src/hooks"; +import { useInterfaceLocalForage } from "src/hooks/LocalForage"; import { ConnectionState, connectionStateLabel, @@ -37,6 +35,14 @@ import { ratingSystemIntlMap, RatingSystemType, } from "src/utils/rating"; +import { + imageWallDirectionIntlMap, + ImageWallDirection, + defaultImageWallOptions, + defaultImageWallDirection, + defaultImageWallMargin, +} from "src/utils/imageWall"; +import { defaultMaxOptionsShown } from "src/core/config"; const allMenuItems = [ { id: "scenes", headingID: "scenes" }, @@ -93,6 +99,24 @@ export const SettingsInterfacePanel: React.FC = () => { }); } + function saveImageWallMargin(m: number) { + saveUI({ + imageWallOptions: { + ...(ui.imageWallOptions ?? defaultImageWallOptions), + margin: m, + }, + }); + } + + function saveImageWallDirection(d: ImageWallDirection) { + saveUI({ + imageWallOptions: { + ...(ui.imageWallOptions ?? defaultImageWallOptions), + direction: d, + }, + }); + } + function saveRatingSystemType(t: RatingSystemType) { saveUI({ ratingSystemOptions: { @@ -354,6 +378,31 @@ export const SettingsInterfacePanel: React.FC = () => { /> + + saveImageWallMargin(v)} + /> + + saveImageWallDirection(v as ImageWallDirection)} + > + {Array.from(imageWallDirectionIntlMap.entries()).map((v) => ( + + ))} + + + { } /> + saveUI({ maxOptionsShown: v })} + /> { const intl = useIntl(); - const { - general, - loading, - error, - saveGeneral, - defaults, - saveDefaults, - } = React.useContext(SettingStateContext); + const { general, loading, error, saveGeneral, defaults, saveDefaults } = + React.useContext(SettingStateContext); function commaDelimitedToList(value: string | undefined) { if (value) { @@ -82,7 +77,7 @@ export const SettingsLibraryPanel: React.FC = () => { id: "config.general.excluded_video_patterns_desc", })} @@ -104,7 +99,7 @@ export const SettingsLibraryPanel: React.FC = () => { id: "config.general.excluded_image_gallery_patterns_desc", })} @@ -134,6 +129,14 @@ export const SettingsLibraryPanel: React.FC = () => { checked={general.writeImageThumbnails ?? false} onChange={(v) => saveGeneral({ writeImageThumbnails: v })} /> + + saveGeneral({ galleryCoverRegex: v })} + /> diff --git a/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx b/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx index 274c82b1c..0cd02703c 100644 --- a/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx @@ -3,9 +3,11 @@ import { Button } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { mutateReloadPlugins, usePlugins } from "src/core/StashService"; -import { useToast } from "src/hooks"; -import { TextUtils } from "src/utils"; -import { CollapseButton, Icon, LoadingIndicator } from "src/components/Shared"; +import { useToast } from "src/hooks/Toast"; +import TextUtils from "src/utils/text"; +import { CollapseButton } from "../Shared/CollapseButton"; +import { Icon } from "../Shared/Icon"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { SettingSection } from "./SettingSection"; import { Setting, SettingGroup } from "./Inputs"; import { faLink, faSyncAlt } from "@fortawesome/free-solid-svg-icons"; diff --git a/ui/v2.5/src/components/Settings/SettingsScrapingPanel.tsx b/ui/v2.5/src/components/Settings/SettingsScrapingPanel.tsx index ecd80ae8a..d0453fbb4 100644 --- a/ui/v2.5/src/components/Settings/SettingsScrapingPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsScrapingPanel.tsx @@ -8,9 +8,11 @@ import { useListSceneScrapers, useListGalleryScrapers, } from "src/core/StashService"; -import { useToast } from "src/hooks"; -import { TextUtils } from "src/utils"; -import { CollapseButton, Icon, LoadingIndicator } from "src/components/Shared"; +import { useToast } from "src/hooks/Toast"; +import TextUtils from "src/utils/text"; +import { CollapseButton } from "../Shared/CollapseButton"; +import { Icon } from "../Shared/Icon"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { ScrapeType } from "src/core/generated-graphql"; import { SettingSection } from "./SettingSection"; import { BooleanSetting, StringListSetting, StringSetting } from "./Inputs"; @@ -75,31 +77,17 @@ const URLList: React.FC = ({ urls }) => { export const SettingsScrapingPanel: React.FC = () => { const Toast = useToast(); const intl = useIntl(); - const { - data: performerScrapers, - loading: loadingPerformers, - } = useListPerformerScrapers(); - const { - data: sceneScrapers, - loading: loadingScenes, - } = useListSceneScrapers(); - const { - data: galleryScrapers, - loading: loadingGalleries, - } = useListGalleryScrapers(); - const { - data: movieScrapers, - loading: loadingMovies, - } = useListMovieScrapers(); + const { data: performerScrapers, loading: loadingPerformers } = + useListPerformerScrapers(); + const { data: sceneScrapers, loading: loadingScenes } = + useListSceneScrapers(); + const { data: galleryScrapers, loading: loadingGalleries } = + useListGalleryScrapers(); + const { data: movieScrapers, loading: loadingMovies } = + useListMovieScrapers(); - const { - general, - scraping, - loading, - error, - saveGeneral, - saveScraping, - } = React.useContext(SettingStateContext); + const { general, scraping, loading, error, saveGeneral, saveScraping } = + React.useContext(SettingStateContext); async function onReloadScrapers() { await mutateReloadScrapers().catch((e) => Toast.error(e)); diff --git a/ui/v2.5/src/components/Settings/SettingsSecurityPanel.tsx b/ui/v2.5/src/components/Settings/SettingsSecurityPanel.tsx index 02d1fae1d..b99217938 100644 --- a/ui/v2.5/src/components/Settings/SettingsSecurityPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsSecurityPanel.tsx @@ -5,8 +5,8 @@ import * as GQL from "src/core/generated-graphql"; import { Button, Form } from "react-bootstrap"; import { useIntl } from "react-intl"; import { SettingStateContext } from "./context"; -import { LoadingIndicator } from "../Shared"; -import { useToast } from "src/hooks"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; +import { useToast } from "src/hooks/Toast"; import { useGenerateAPIKey } from "src/core/StashService"; type AuthenticationSettingsInput = Pick< @@ -71,14 +71,8 @@ export const SettingsSecurityPanel: React.FC = () => { const intl = useIntl(); const Toast = useToast(); - const { - general, - apiKey, - loading, - error, - saveGeneral, - refetch, - } = React.useContext(SettingStateContext); + const { general, apiKey, loading, error, saveGeneral, refetch } = + React.useContext(SettingStateContext); const [generateAPIKey] = useGenerateAPIKey(); diff --git a/ui/v2.5/src/components/Settings/SettingsServicesPanel.tsx b/ui/v2.5/src/components/Settings/SettingsServicesPanel.tsx index 51d30b1dc..2db88f926 100644 --- a/ui/v2.5/src/components/Settings/SettingsServicesPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsServicesPanel.tsx @@ -8,8 +8,11 @@ import { useAddTempDLNAIP, useRemoveTempDLNAIP, } from "src/core/StashService"; -import { useToast } from "src/hooks"; -import { DurationInput, Icon, LoadingIndicator, Modal } from "../Shared"; +import { useToast } from "src/hooks/Toast"; +import { DurationInput } from "../Shared/DurationInput"; +import { Icon } from "../Shared/Icon"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; +import { ModalComponent } from "../Shared/Modal"; import { SettingSection } from "./SettingSection"; import { BooleanSetting, StringListSetting, StringSetting } from "./Inputs"; import { SettingStateContext } from "./context"; @@ -23,9 +26,12 @@ export const SettingsServicesPanel: React.FC = () => { const intl = useIntl(); const Toast = useToast(); - const { dlna, loading: configLoading, error, saveDLNA } = React.useContext( - SettingStateContext - ); + const { + dlna, + loading: configLoading, + error, + saveDLNA, + } = React.useContext(SettingStateContext); // undefined to hide dialog, true for enable, false for disable const [enableDisable, setEnableDisable] = useState( @@ -233,7 +239,7 @@ export const SettingsServicesPanel: React.FC = () => { const capitalised = `${text[0].toUpperCase()}${text.slice(1)}`; return ( - { Duration to {text} for - in minutes. - + ); } function renderTempWhitelistDialog() { return ( - { Duration to allow for - in minutes. - + ); } diff --git a/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx b/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx index 46d02ba80..c5b0f36c1 100644 --- a/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx @@ -1,12 +1,13 @@ import React from "react"; import * as GQL from "src/core/generated-graphql"; -import { LoadingIndicator } from "src/components/Shared"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { SettingSection } from "./SettingSection"; import { BooleanSetting, ModalSetting, NumberSetting, SelectSetting, + StringListSetting, StringSetting, } from "./Inputs"; import { SettingStateContext } from "./context"; @@ -14,11 +15,13 @@ import { VideoPreviewInput, VideoPreviewSettingsInput, } from "./GeneratePreviewOptions"; +import { useIntl } from "react-intl"; export const SettingsConfigurationPanel: React.FC = () => { - const { general, loading, error, saveGeneral } = React.useContext( - SettingStateContext - ); + const intl = useIntl(); + + const { general, loading, error, saveGeneral } = + React.useContext(SettingStateContext); const transcodeQualities = [ GQL.StreamingResolutionEnum.Low, @@ -94,20 +97,23 @@ export const SettingsConfigurationPanel: React.FC = () => { return GQL.HashAlgorithm.Md5; } + function blobStorageTypeToID(value: GQL.BlobsStorageType | undefined) { + switch (value) { + case GQL.BlobsStorageType.Database: + return "blobs_storage_type.database"; + case GQL.BlobsStorageType.Filesystem: + return "blobs_storage_type.filesystem"; + } + + return "blobs_storage_type.database"; + } + if (error) return

    {error.message}

    ; if (loading) return ; return ( <> - saveGeneral({ databasePath: v })} - /> - { onChange={(v) => saveGeneral({ generatedPath: v })} /> + saveGeneral({ cachePath: v })} + /> + { onChange={(v) => saveGeneral({ metadataPath: v })} /> - saveGeneral({ cachePath: v })} - /> - { /> + + saveGeneral({ databasePath: v })} + /> + + saveGeneral({ blobsStorage: v as GQL.BlobsStorageType }) + } + > + {Object.values(GQL.BlobsStorageType).map((q) => ( + + ))} + + saveGeneral({ blobsPath: v })} + /> + + { ))} + + saveGeneral({ transcodeHardwareAcceleration: v })} + /> + + saveGeneral({ transcodeInputArgs: v })} + value={general.transcodeInputArgs ?? []} + /> + saveGeneral({ transcodeOutputArgs: v })} + value={general.transcodeOutputArgs ?? []} + /> + + saveGeneral({ liveTranscodeInputArgs: v })} + value={general.liveTranscodeInputArgs ?? []} + /> + saveGeneral({ liveTranscodeOutputArgs: v })} + value={general.liveTranscodeOutputArgs ?? []} + /> @@ -285,6 +361,16 @@ export const SettingsConfigurationPanel: React.FC = () => { /> + + saveGeneral({ drawFunscriptHeatmapRange: v })} + /> + + = ({ } return ( - = ({ {msg} - + ); }; @@ -149,10 +152,12 @@ const CleanOptions: React.FC = ({ interface IDataManagementTasks { setIsBackupRunning: (v: boolean) => void; + setIsAnonymiseRunning: (v: boolean) => void; } export const DataManagementTasks: React.FC = ({ setIsBackupRunning, + setIsAnonymiseRunning, }) => { const intl = useIntl(); const Toast = useToast(); @@ -167,6 +172,17 @@ export const DataManagementTasks: React.FC = ({ dryRun: false, }); + const [migrateBlobsOptions, setMigrateBlobsOptions] = + useState({ + deleteOld: true, + }); + + const [migrateSceneScreenshotsOptions, setMigrateSceneScreenshotsOptions] = + useState({ + deleteFiles: false, + overwriteExisting: false, + }); + type DialogOpenState = typeof dialogOpen; function setDialogOpen(s: Partial) { @@ -192,7 +208,7 @@ export const DataManagementTasks: React.FC = ({ function renderImportAlert() { return ( - = ({ cancel={{ onClick: () => setDialogOpen({ importAlert: false }) }} >

    {intl.formatMessage({ id: "actions.tasks.import_warning" })}

    -
    + ); } @@ -253,13 +269,49 @@ export const DataManagementTasks: React.FC = ({ } } + async function onMigrateSceneScreenshots() { + try { + await mutateMigrateSceneScreenshots(migrateSceneScreenshotsOptions); + Toast.success({ + content: intl.formatMessage( + { id: "config.tasks.added_job_to_queue" }, + { + operation_name: intl.formatMessage({ + id: "actions.migrate_scene_screenshots", + }), + } + ), + }); + } catch (err) { + Toast.error(err); + } + } + + async function onMigrateBlobs() { + try { + await mutateMigrateBlobs(migrateBlobsOptions); + Toast.success({ + content: intl.formatMessage( + { id: "config.tasks.added_job_to_queue" }, + { + operation_name: intl.formatMessage({ + id: "actions.migrate_blobs", + }), + } + ), + }); + } catch (err) { + Toast.error(err); + } + } + async function onExport() { try { await mutateMetadataExport(); Toast.success({ content: intl.formatMessage( { id: "config.tasks.added_job_to_queue" }, - { operation_name: intl.formatMessage({ id: "actions.backup" }) } + { operation_name: intl.formatMessage({ id: "actions.export" }) } ), }); } catch (err) { @@ -286,6 +338,25 @@ export const DataManagementTasks: React.FC = ({ } } + async function onAnonymise(download?: boolean) { + try { + setIsAnonymiseRunning(true); + const ret = await mutateAnonymiseDatabase({ + download, + }); + + // download the result + if (download && ret.data && ret.data.anonymiseDatabase) { + const link = ret.data.anonymiseDatabase; + downloadFile(link); + } + } catch (e) { + Toast.error(e); + } finally { + setIsAnonymiseRunning(false); + } + } + return ( {renderImportAlert()} @@ -361,7 +432,7 @@ export const DataManagementTasks: React.FC = ({ type="submit" onClick={() => onExport()} > - + … @@ -433,6 +504,45 @@ export const DataManagementTasks: React.FC = ({
    + + + [origFilename].anonymous.sqlite.[schemaVersion].[YYYYMMDD_HHMMSS] + + ), + } + )} + > + + + + + + + + = ({ + +
    + + + + + + setMigrateBlobsOptions({ ...migrateBlobsOptions, deleteOld: v }) + } + /> +
    + +
    + + + + + + setMigrateSceneScreenshotsOptions({ + ...migrateSceneScreenshotsOptions, + overwriteExisting: v, + }) + } + /> + + + setMigrateSceneScreenshotsOptions({ + ...migrateSceneScreenshotsOptions, + deleteFiles: v, + }) + } + /> +
    ); diff --git a/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx b/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx index 84b1e0d5a..30e64ebcd 100644 --- a/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx @@ -6,7 +6,8 @@ import { import React, { useState } from "react"; import { Button, Col, Form, Row } from "react-bootstrap"; import { useIntl } from "react-intl"; -import { Icon, Modal } from "src/components/Shared"; +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"; @@ -17,12 +18,9 @@ interface IDirectorySelectionDialogProps { onClose: (paths?: string[]) => void; } -export const DirectorySelectionDialog: React.FC = ({ - animation, - allowEmpty = false, - initialPaths = [], - onClose, -}) => { +export const DirectorySelectionDialog: React.FC< + IDirectorySelectionDialogProps +> = ({ animation, allowEmpty = false, initialPaths = [], onClose }) => { const intl = useIntl(); const { configuration } = React.useContext(ConfigurationContext); @@ -42,7 +40,7 @@ export const DirectorySelectionDialog: React.FC } return ( - } /> - + ); }; diff --git a/ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx b/ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx index 5e8864f26..4436bf179 100644 --- a/ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx @@ -26,6 +26,12 @@ export const GenerateOptions: React.FC = ({ return ( <> + setOptions({ covers: v })} + /> = ( } return ( - = ( - + ); }; diff --git a/ui/v2.5/src/components/Settings/Tasks/JobTable.tsx b/ui/v2.5/src/components/Settings/Tasks/JobTable.tsx index b88107434..2d394a5e0 100644 --- a/ui/v2.5/src/components/Settings/Tasks/JobTable.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/JobTable.tsx @@ -6,7 +6,7 @@ import { useJobsSubscribe, } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; -import { Icon } from "src/components/Shared"; +import { Icon } from "src/components/Shared/Icon"; import { useIntl } from "react-intl"; import { faBan, diff --git a/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx b/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx index 82c4c3b17..6069ef433 100644 --- a/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx @@ -7,18 +7,18 @@ import { mutateMetadataGenerate, useConfigureDefaults, } from "src/core/StashService"; -import { withoutTypename } from "src/utils"; +import { withoutTypename } from "src/utils/data"; import { ConfigurationContext } from "src/hooks/Config"; import { IdentifyDialog } from "../../Dialogs/IdentifyDialog/IdentifyDialog"; import * as GQL from "src/core/generated-graphql"; import { DirectorySelectionDialog } from "./DirectorySelectionDialog"; import { ScanOptions } from "./ScanOptions"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; import { GenerateOptions } from "./GenerateOptions"; import { SettingSection } from "../SettingSection"; import { BooleanSetting, Setting, SettingGroup } from "../Inputs"; import { ManualLink } from "src/components/Help/context"; -import { Icon } from "src/components/Shared"; +import { Icon } from "src/components/Shared/Icon"; import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; interface IAutoTagOptions { @@ -81,17 +81,16 @@ export const LibraryTasks: React.FC = () => { }); const [scanOptions, setScanOptions] = useState({}); - const [ - autoTagOptions, - setAutoTagOptions, - ] = useState({ - performers: ["*"], - studios: ["*"], - tags: ["*"], - }); + const [autoTagOptions, setAutoTagOptions] = + useState({ + performers: ["*"], + studios: ["*"], + tags: ["*"], + }); function getDefaultGenerateOptions(): GQL.GenerateMetadataInput { return { + covers: true, sprites: true, phashes: true, previews: true, @@ -104,10 +103,8 @@ export const LibraryTasks: React.FC = () => { }; } - const [ - generateOptions, - setGenerateOptions, - ] = useState(getDefaultGenerateOptions()); + const [generateOptions, setGenerateOptions] = + useState(getDefaultGenerateOptions()); type DialogOpenState = typeof dialogOpen; diff --git a/ui/v2.5/src/components/Settings/Tasks/PluginTasks.tsx b/ui/v2.5/src/components/Settings/Tasks/PluginTasks.tsx index d1d9ab9e9..396e1d66a 100644 --- a/ui/v2.5/src/components/Settings/Tasks/PluginTasks.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/PluginTasks.tsx @@ -2,7 +2,7 @@ import React from "react"; import { useIntl } from "react-intl"; import { Button, Form } from "react-bootstrap"; import { mutateRunPluginTask, usePlugins } from "src/core/StashService"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; import * as GQL from "src/core/generated-graphql"; import { SettingSection } from "../SettingSection"; import { Setting, SettingGroup } from "../Inputs"; diff --git a/ui/v2.5/src/components/Settings/Tasks/ScanOptions.tsx b/ui/v2.5/src/components/Settings/Tasks/ScanOptions.tsx index 2f9497ee3..1a965ead3 100644 --- a/ui/v2.5/src/components/Settings/Tasks/ScanOptions.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/ScanOptions.tsx @@ -12,6 +12,7 @@ export const ScanOptions: React.FC = ({ setOptions: setOptionsState, }) => { const { + scanGenerateCovers, scanGeneratePreviews, scanGenerateImagePreviews, scanGenerateSprites, @@ -25,6 +26,12 @@ export const ScanOptions: React.FC = ({ return ( <> + setOptions({ scanGenerateCovers: v })} + /> { const intl = useIntl(); const [isBackupRunning, setIsBackupRunning] = useState(false); + const [isAnonymiseRunning, setIsAnonymiseRunning] = useState(false); if (isBackupRunning) { return ( @@ -18,6 +19,16 @@ export const SettingsTasksPanel: React.FC = () => { ); } + if (isAnonymiseRunning) { + return ( + + ); + } + return (
    @@ -28,7 +39,10 @@ export const SettingsTasksPanel: React.FC = () => {

    - +
    diff --git a/ui/v2.5/src/components/Settings/context.tsx b/ui/v2.5/src/components/Settings/context.tsx index 9df77a1ef..cbb353a6b 100644 --- a/ui/v2.5/src/components/Settings/context.tsx +++ b/ui/v2.5/src/components/Settings/context.tsx @@ -3,14 +3,7 @@ import { faCheckCircle, faTimesCircle, } from "@fortawesome/free-solid-svg-icons"; -import debounce from "lodash-es/debounce"; -import React, { - useState, - useEffect, - useMemo, - useCallback, - useRef, -} from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; import { Spinner } from "react-bootstrap"; import { IUIConfig } from "src/core/config"; import * as GQL from "src/core/generated-graphql"; @@ -23,9 +16,10 @@ import { useConfigureScraping, useConfigureUI, } from "src/core/StashService"; -import { useToast } from "src/hooks"; -import { withoutTypename } from "src/utils"; -import { Icon } from "../Shared"; +import { useDebounce } from "src/hooks/debounce"; +import { useToast } from "src/hooks/Toast"; +import { withoutTypename } from "src/utils/data"; +import { Icon } from "../Shared/Icon"; export interface ISettingsContextState { loading: boolean; @@ -76,40 +70,34 @@ export const SettingsContext: React.FC = ({ children }) => { const initialRef = useRef(false); const [general, setGeneral] = useState({}); - const [pendingGeneral, setPendingGeneral] = useState< - GQL.ConfigGeneralInput | undefined - >(); + const [pendingGeneral, setPendingGeneral] = + useState(); const [updateGeneralConfig] = useConfigureGeneral(); const [iface, setIface] = useState({}); - const [pendingInterface, setPendingInterface] = useState< - GQL.ConfigInterfaceInput | undefined - >(); + const [pendingInterface, setPendingInterface] = + useState(); const [updateInterfaceConfig] = useConfigureInterface(); const [defaults, setDefaults] = useState({}); - const [pendingDefaults, setPendingDefaults] = useState< - GQL.ConfigDefaultSettingsInput | undefined - >(); + const [pendingDefaults, setPendingDefaults] = + useState(); const [updateDefaultsConfig] = useConfigureDefaults(); const [scraping, setScraping] = useState({}); - const [pendingScraping, setPendingScraping] = useState< - GQL.ConfigScrapingInput | undefined - >(); + const [pendingScraping, setPendingScraping] = + useState(); const [updateScrapingConfig] = useConfigureScraping(); const [dlna, setDLNA] = useState({}); - const [pendingDLNA, setPendingDLNA] = useState< - GQL.ConfigDlnaInput | undefined - >(); + const [pendingDLNA, setPendingDLNA] = useState(); const [updateDLNAConfig] = useConfigureDLNA(); const [ui, setUI] = useState({}); - const [pendingUI, setPendingUI] = useState<{} | undefined>(); + const [pendingUI, setPendingUI] = useState<{}>(); const [updateUIConfig] = useConfigureUI(); - const [updateSuccess, setUpdateSuccess] = useState(); + const [updateSuccess, setUpdateSuccess] = useState(); const [apiKey, setApiKey] = useState(""); @@ -146,13 +134,7 @@ export const SettingsContext: React.FC = ({ children }) => { setUI(data.configuration.ui); }, [data, error]); - const resetSuccess = useMemo( - () => - debounce(() => { - setUpdateSuccess(undefined); - }, 4000), - [] - ); + const resetSuccess = useDebounce(() => setUpdateSuccess(undefined), [], 4000); const onSuccess = useCallback(() => { setUpdateSuccess(true); @@ -160,24 +142,24 @@ export const SettingsContext: React.FC = ({ children }) => { }, [resetSuccess]); // saves the configuration if no further changes are made after a half second - const saveGeneralConfig = useMemo( - () => - debounce(async (input: GQL.ConfigGeneralInput) => { - try { - setUpdateSuccess(undefined); - await updateGeneralConfig({ - variables: { - input, - }, - }); + const saveGeneralConfig = useDebounce( + async (input: GQL.ConfigGeneralInput) => { + try { + setUpdateSuccess(undefined); + await updateGeneralConfig({ + variables: { + input, + }, + }); - setPendingGeneral(undefined); - onSuccess(); - } catch (e) { - setSaveError(e); - } - }, 500), - [updateGeneralConfig, onSuccess] + setPendingGeneral(undefined); + onSuccess(); + } catch (e) { + setSaveError(e); + } + }, + [updateGeneralConfig, onSuccess], + 500 ); useEffect(() => { @@ -210,24 +192,24 @@ export const SettingsContext: React.FC = ({ children }) => { } // saves the configuration if no further changes are made after a half second - const saveInterfaceConfig = useMemo( - () => - debounce(async (input: GQL.ConfigInterfaceInput) => { - try { - setUpdateSuccess(undefined); - await updateInterfaceConfig({ - variables: { - input, - }, - }); + const saveInterfaceConfig = useDebounce( + async (input: GQL.ConfigInterfaceInput) => { + try { + setUpdateSuccess(undefined); + await updateInterfaceConfig({ + variables: { + input, + }, + }); - setPendingInterface(undefined); - onSuccess(); - } catch (e) { - setSaveError(e); - } - }, 500), - [updateInterfaceConfig, onSuccess] + setPendingInterface(undefined); + onSuccess(); + } catch (e) { + setSaveError(e); + } + }, + [updateInterfaceConfig, onSuccess], + 500 ); useEffect(() => { @@ -260,24 +242,24 @@ export const SettingsContext: React.FC = ({ children }) => { } // saves the configuration if no further changes are made after a half second - const saveDefaultsConfig = useMemo( - () => - debounce(async (input: GQL.ConfigDefaultSettingsInput) => { - try { - setUpdateSuccess(undefined); - await updateDefaultsConfig({ - variables: { - input, - }, - }); + const saveDefaultsConfig = useDebounce( + async (input: GQL.ConfigDefaultSettingsInput) => { + try { + setUpdateSuccess(undefined); + await updateDefaultsConfig({ + variables: { + input, + }, + }); - setPendingDefaults(undefined); - onSuccess(); - } catch (e) { - setSaveError(e); - } - }, 500), - [updateDefaultsConfig, onSuccess] + setPendingDefaults(undefined); + onSuccess(); + } catch (e) { + setSaveError(e); + } + }, + [updateDefaultsConfig, onSuccess], + 500 ); useEffect(() => { @@ -310,24 +292,24 @@ export const SettingsContext: React.FC = ({ children }) => { } // saves the configuration if no further changes are made after a half second - const saveScrapingConfig = useMemo( - () => - debounce(async (input: GQL.ConfigScrapingInput) => { - try { - setUpdateSuccess(undefined); - await updateScrapingConfig({ - variables: { - input, - }, - }); + const saveScrapingConfig = useDebounce( + async (input: GQL.ConfigScrapingInput) => { + try { + setUpdateSuccess(undefined); + await updateScrapingConfig({ + variables: { + input, + }, + }); - setPendingScraping(undefined); - onSuccess(); - } catch (e) { - setSaveError(e); - } - }, 500), - [updateScrapingConfig, onSuccess] + setPendingScraping(undefined); + onSuccess(); + } catch (e) { + setSaveError(e); + } + }, + [updateScrapingConfig, onSuccess], + 500 ); useEffect(() => { @@ -360,24 +342,24 @@ export const SettingsContext: React.FC = ({ children }) => { } // saves the configuration if no further changes are made after a half second - const saveDLNAConfig = useMemo( - () => - debounce(async (input: GQL.ConfigDlnaInput) => { - try { - setUpdateSuccess(undefined); - await updateDLNAConfig({ - variables: { - input, - }, - }); + const saveDLNAConfig = useDebounce( + async (input: GQL.ConfigDlnaInput) => { + try { + setUpdateSuccess(undefined); + await updateDLNAConfig({ + variables: { + input, + }, + }); - setPendingDLNA(undefined); - onSuccess(); - } catch (e) { - setSaveError(e); - } - }, 500), - [updateDLNAConfig, onSuccess] + setPendingDLNA(undefined); + onSuccess(); + } catch (e) { + setSaveError(e); + } + }, + [updateDLNAConfig, onSuccess], + 500 ); useEffect(() => { @@ -410,24 +392,24 @@ export const SettingsContext: React.FC = ({ children }) => { } // saves the configuration if no further changes are made after a half second - const saveUIConfig = useMemo( - () => - debounce(async (input: IUIConfig) => { - try { - setUpdateSuccess(undefined); - await updateUIConfig({ - variables: { - input, - }, - }); + const saveUIConfig = useDebounce( + async (input: IUIConfig) => { + try { + setUpdateSuccess(undefined); + await updateUIConfig({ + variables: { + input, + }, + }); - setPendingUI(undefined); - onSuccess(); - } catch (e) { - setSaveError(e); - } - }, 500), - [updateUIConfig, onSuccess] + setPendingUI(undefined); + onSuccess(); + } catch (e) { + setSaveError(e); + } + }, + [updateUIConfig, onSuccess], + 500 ); useEffect(() => { diff --git a/ui/v2.5/src/components/Setup/Migrate.tsx b/ui/v2.5/src/components/Setup/Migrate.tsx index 57ac073c8..9f27d7805 100644 --- a/ui/v2.5/src/components/Setup/Migrate.tsx +++ b/ui/v2.5/src/components/Setup/Migrate.tsx @@ -1,11 +1,11 @@ import React, { useEffect, useMemo, useState } from "react"; import { Button, Card, Container, Form } from "react-bootstrap"; import { useIntl, FormattedMessage } from "react-intl"; -import { getBaseURL } from "src/core/createClient"; +import { useHistory } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; import { useSystemStatus, mutateMigrate } from "src/core/StashService"; import { migrationNotes } from "src/docs/en/MigrationNotes"; -import { LoadingIndicator } from "../Shared"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { MarkdownPage } from "../Shared/MarkdownPage"; export const Migrate: React.FC = () => { @@ -15,6 +15,7 @@ export const Migrate: React.FC = () => { const [migrateError, setMigrateError] = useState(""); const intl = useIntl(); + const history = useHistory(); // if database path includes path separators, then this is passed through // to the migration path. Extract the base name of the database file. @@ -109,8 +110,7 @@ export const Migrate: React.FC = () => { systemStatus.systemStatus.status !== GQL.SystemStatusEnum.NeedsMigration ) { // redirect to main page - const newURL = new URL("/", window.location.toString()); - window.location.href = newURL.toString(); + history.push("/"); return ; } @@ -122,8 +122,7 @@ export const Migrate: React.FC = () => { backupPath: backupPath ?? "", }); - const newURL = new URL("", window.location.origin + getBaseURL()); - window.location.href = newURL.toString(); + history.push("/"); } catch (e) { if (e instanceof Error) setMigrateError(e.message ?? e.toString()); setMigrateLoading(false); diff --git a/ui/v2.5/src/components/Setup/Setup.tsx b/ui/v2.5/src/components/Setup/Setup.tsx index 16a10fcd5..f4f825322 100644 --- a/ui/v2.5/src/components/Setup/Setup.tsx +++ b/ui/v2.5/src/components/Setup/Setup.tsx @@ -9,35 +9,48 @@ import { InputGroup, } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; -import { mutateSetup, useSystemStatus } from "src/core/StashService"; -import { Link } from "react-router-dom"; +import { + mutateSetup, + useConfigureUI, + useSystemStatus, +} from "src/core/StashService"; +import { Link, useHistory } from "react-router-dom"; import { ConfigurationContext } from "src/hooks/Config"; import StashConfiguration from "../Settings/StashConfiguration"; -import { Icon, LoadingIndicator, Modal } from "../Shared"; +import { Icon } from "../Shared/Icon"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; +import { ModalComponent } from "../Shared/Modal"; import { FolderSelectDialog } from "../Shared/FolderSelect/FolderSelectDialog"; import { faEllipsisH, faExclamationTriangle, faQuestionCircle, } from "@fortawesome/free-solid-svg-icons"; +import { releaseNotes } from "src/docs/en/ReleaseNotes"; export const Setup: React.FC = () => { - const { configuration, loading: configLoading } = useContext( - ConfigurationContext - ); + const { configuration, loading: configLoading } = + useContext(ConfigurationContext); + const [saveUI] = useConfigureUI(); const [step, setStep] = useState(0); const [configLocation, setConfigLocation] = useState(""); const [stashes, setStashes] = useState([]); const [showStashAlert, setShowStashAlert] = useState(false); - const [generatedLocation, setGeneratedLocation] = useState(""); const [databaseFile, setDatabaseFile] = useState(""); + const [generatedLocation, setGeneratedLocation] = useState(""); + const [cacheLocation, setCacheLocation] = useState(""); + const [blobsLocation, setBlobsLocation] = useState("blobs"); const [loading, setLoading] = useState(false); const [setupError, setSetupError] = useState(""); const intl = useIntl(); + const history = useHistory(); - const [showGeneratedDialog, setShowGeneratedDialog] = useState(false); + const [showGeneratedSelectDialog, setShowGeneratedSelectDialog] = + useState(false); + const [showCacheSelectDialog, setShowCacheSelectDialog] = useState(false); + const [showBlobsDialog, setShowBlobsDialog] = useState(false); const { data: systemStatus, loading: statusLoading } = useSystemStatus(); @@ -111,7 +124,7 @@ export const Setup: React.FC = () => { } return ( - {

    -
    + ); } @@ -226,20 +239,36 @@ export const Setup: React.FC = () => { ); } - function onGeneratedClosed(d?: string) { + function onGeneratedSelectClosed(d?: string) { if (d) { setGeneratedLocation(d); } - setShowGeneratedDialog(false); + setShowGeneratedSelectDialog(false); } function maybeRenderGeneratedSelectDialog() { - if (!showGeneratedDialog) { + if (!showGeneratedSelectDialog) { return; } - return ; + return ; + } + + function onBlobsClosed(d?: string) { + if (d) { + setBlobsLocation(d); + } + + setShowBlobsDialog(false); + } + + function maybeRenderBlobsSelectDialog() { + if (!showBlobsDialog) { + return; + } + + return ; } function maybeRenderGenerated() { @@ -272,7 +301,114 @@ export const Setup: React.FC = () => { + + + + ); + } + } + + function onCacheSelectClosed(d?: string) { + if (d) { + setCacheLocation(d); + } + + setShowCacheSelectDialog(false); + } + + function maybeRenderCacheSelectDialog() { + if (!showCacheSelectDialog) { + return; + } + + return ; + } + + function maybeRenderCache() { + if (!configuration?.general.cachePath) { + return ( + +

    + +

    +

    + {chunks}, + }} + /> +

    + + ) => + setCacheLocation(e.currentTarget.value) + } + /> + + + + +
    + ); + } + } + + function maybeRenderBlobs() { + if (!configuration?.general.blobsPath) { + return ( + +

    + +

    +

    + {chunks}, + }} + /> +

    +

    + {chunks}, + strong: (chunks: string) => {chunks}, + }} + /> +

    + + ) => + setBlobsLocation(e.currentTarget.value) + } + /> + + @@ -321,6 +457,13 @@ export const Setup: React.FC = () => { code: (chunks: string) => {chunks}, }} /> +
    + {chunks}, + }} + />

    { />
    {maybeRenderGenerated()} + {maybeRenderCache()} + {maybeRenderBlobs()}
    @@ -397,8 +542,19 @@ export const Setup: React.FC = () => { configLocation, databaseFile, generatedLocation, + cacheLocation, + blobsLocation, stashes, }); + // Set lastNoteSeen to hide release notes dialog + await saveUI({ + variables: { + input: { + ...configuration?.ui, + lastNoteSeen: releaseNotes[0].date, + }, + }, + }); } catch (e) { if (e instanceof Error) setSetupError(e.message ?? e.toString()); } finally { @@ -457,6 +613,32 @@ export const Setup: React.FC = () => { +
    +
    + +
    +
    + + {cacheLocation !== "" + ? cacheLocation + : intl.formatMessage({ + id: "setup.confirm.default_cache_location", + })} + +
    +
    + +
    +
    + + {blobsLocation !== "" + ? blobsLocation + : intl.formatMessage({ + id: "setup.confirm.default_blobs_location", + })} + +
    +
    @@ -576,7 +758,7 @@ export const Setup: React.FC = () => {
    - @@ -600,12 +782,12 @@ export const Setup: React.FC = () => { } if ( + step === 0 && systemStatus && systemStatus.systemStatus.status !== GQL.SystemStatusEnum.Setup ) { // redirect to main page - const newURL = new URL("/", window.location.toString()); - window.location.href = newURL.toString(); + history.push("/"); return ; } @@ -638,6 +820,8 @@ export const Setup: React.FC = () => { return ( {maybeRenderGeneratedSelectDialog()} + {maybeRenderCacheSelectDialog()} + {maybeRenderBlobsSelectDialog()}

    diff --git a/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx b/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx index a64754d61..542ab5b3b 100644 --- a/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx +++ b/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx @@ -2,7 +2,7 @@ import { faBan } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button, Form, FormControlProps, InputGroup } from "react-bootstrap"; import { useIntl } from "react-intl"; -import Icon from "./Icon"; +import { Icon } from "./Icon"; interface IBulkUpdateTextInputProps extends FormControlProps { valueChanged: (value: string | undefined) => void; diff --git a/ui/v2.5/src/components/Shared/CollapseButton.tsx b/ui/v2.5/src/components/Shared/CollapseButton.tsx index 2216121de..78099d0e8 100644 --- a/ui/v2.5/src/components/Shared/CollapseButton.tsx +++ b/ui/v2.5/src/components/Shared/CollapseButton.tsx @@ -4,7 +4,7 @@ import { } from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Collapse } from "react-bootstrap"; -import Icon from "src/components/Shared/Icon"; +import { Icon } from "./Icon"; interface IProps { text: string; diff --git a/ui/v2.5/src/components/Shared/Counter.tsx b/ui/v2.5/src/components/Shared/Counter.tsx index d0552605f..c0aad2faf 100644 --- a/ui/v2.5/src/components/Shared/Counter.tsx +++ b/ui/v2.5/src/components/Shared/Counter.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Badge } from "react-bootstrap"; import { FormattedNumber, useIntl } from "react-intl"; -import { TextUtils } from "src/utils"; +import TextUtils from "src/utils/text"; interface IProps { abbreviateCounter?: boolean; @@ -17,7 +17,12 @@ export const Counter: React.FC = ({ if (abbreviateCounter) { const formated = TextUtils.abbreviateCounter(count); return ( - + = ({ ); } else { return ( - + {intl.formatNumber(count)} ); } }; - -export default Counter; diff --git a/ui/v2.5/src/components/Shared/CountryFlag.tsx b/ui/v2.5/src/components/Shared/CountryFlag.tsx index 3d73c280e..c5f5959f7 100644 --- a/ui/v2.5/src/components/Shared/CountryFlag.tsx +++ b/ui/v2.5/src/components/Shared/CountryFlag.tsx @@ -1,13 +1,13 @@ import React from "react"; import { useIntl } from "react-intl"; -import { getCountryByISO } from "src/utils"; +import { getCountryByISO } from "src/utils/country"; interface ICountryFlag { country?: string | null; className?: string; } -const CountryFlag: React.FC = ({ +export const CountryFlag: React.FC = ({ className, country: isoCountry, }) => { @@ -19,12 +19,8 @@ const CountryFlag: React.FC = ({ return ( ); }; - -export default CountryFlag; diff --git a/ui/v2.5/src/components/Shared/CountryLabel.tsx b/ui/v2.5/src/components/Shared/CountryLabel.tsx index 82c83bfc4..4c200399f 100644 --- a/ui/v2.5/src/components/Shared/CountryLabel.tsx +++ b/ui/v2.5/src/components/Shared/CountryLabel.tsx @@ -1,14 +1,17 @@ import React from "react"; import { useIntl } from "react-intl"; -import { CountryFlag } from "src/components/Shared"; -import { getCountryByISO } from "src/utils"; +import { CountryFlag } from "./CountryFlag"; +import { getCountryByISO } from "src/utils/country"; interface IProps { country: string | undefined; showFlag?: boolean; } -const CountryLabel: React.FC = ({ country, showFlag = true }) => { +export const CountryLabel: React.FC = ({ + country, + showFlag = true, +}) => { const { locale } = useIntl(); // #3063 - use alpha2 values only @@ -22,5 +25,3 @@ const CountryLabel: React.FC = ({ country, showFlag = true }) => {
    ); }; - -export default CountryLabel; diff --git a/ui/v2.5/src/components/Shared/CountrySelect.tsx b/ui/v2.5/src/components/Shared/CountrySelect.tsx index e354279fd..659731725 100644 --- a/ui/v2.5/src/components/Shared/CountrySelect.tsx +++ b/ui/v2.5/src/components/Shared/CountrySelect.tsx @@ -1,25 +1,27 @@ import React from "react"; import Creatable from "react-select/creatable"; import { useIntl } from "react-intl"; -import { getCountries } from "src/utils"; -import CountryLabel from "./CountryLabel"; +import { getCountries } from "src/utils/country"; +import { CountryLabel } from "./CountryLabel"; interface IProps { - value?: string | undefined; + value?: string; onChange?: (value: string) => void; disabled?: boolean; className?: string; showFlag?: boolean; isClearable?: boolean; + menuPortalTarget?: HTMLElement | null; } -const CountrySelect: React.FC = ({ +export const CountrySelect: React.FC = ({ value, onChange, disabled = false, isClearable = true, showFlag, className, + menuPortalTarget, }) => { const { locale } = useIntl(); const options = getCountries(locale); @@ -44,8 +46,7 @@ const CountrySelect: React.FC = ({ IndicatorSeparator: null, }} className={`CountrySelect ${className}`} + menuPortalTarget={menuPortalTarget} /> ); }; - -export default CountrySelect; diff --git a/ui/v2.5/src/components/Shared/DateInput.tsx b/ui/v2.5/src/components/Shared/DateInput.tsx new file mode 100644 index 000000000..aa57fd37f --- /dev/null +++ b/ui/v2.5/src/components/Shared/DateInput.tsx @@ -0,0 +1,105 @@ +import { faCalendar } from "@fortawesome/free-regular-svg-icons"; +import React, { useMemo } from "react"; +import { Button, InputGroup, Form } from "react-bootstrap"; +import ReactDatePicker from "react-datepicker"; +import TextUtils from "src/utils/text"; +import { Icon } from "./Icon"; + +import "react-datepicker/dist/react-datepicker.css"; +import { useIntl } from "react-intl"; + +interface IProps { + disabled?: boolean; + value: string | undefined; + isTime?: boolean; + onValueChange(value: string): void; + placeholder?: string; + error?: string; +} + +export const DateInput: React.FC = (props: IProps) => { + const intl = useIntl(); + + const date = useMemo(() => { + const toDate = props.isTime + ? TextUtils.stringToFuzzyDateTime + : TextUtils.stringToFuzzyDate; + if (props.value) { + const ret = toDate(props.value); + if (!ret || isNaN(ret.getTime())) { + return undefined; + } + + return ret; + } + }, [props.value, props.isTime]); + + function maybeRenderButton() { + if (!props.disabled) { + const ShowPickerButton = ({ + onClick, + }: { + onClick: ( + event: React.MouseEvent + ) => void; + }) => ( + + ); + + const dateToString = props.isTime + ? TextUtils.dateTimeToString + : TextUtils.dateToString; + + return ( + { + props.onValueChange(v ? dateToString(v) : ""); + }} + customInput={React.createElement(ShowPickerButton)} + showMonthDropdown + showYearDropdown + scrollableMonthYearDropdown + scrollableYearDropdown + maxDate={new Date()} + yearDropdownItemNumber={100} + portalId="date-picker-portal" + showTimeSelect={props.isTime} + /> + ); + } + } + + const placeholderText = intl.formatMessage({ + id: props.isTime ? "datetime_format" : "date_format", + }); + + return ( +
    + + ) => + props.onValueChange(e.currentTarget.value) + } + placeholder={ + !props.disabled + ? props.placeholder + ? `${props.placeholder} (${placeholderText})` + : placeholderText + : undefined + } + isInvalid={!!props.error} + /> + {maybeRenderButton()} + + {props.error} + + +
    + ); +}; diff --git a/ui/v2.5/src/components/Shared/DeleteEntityDialog.tsx b/ui/v2.5/src/components/Shared/DeleteEntityDialog.tsx index cf6284982..4fd5114c8 100644 --- a/ui/v2.5/src/components/Shared/DeleteEntityDialog.tsx +++ b/ui/v2.5/src/components/Shared/DeleteEntityDialog.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { defineMessages, FormattedMessage, useIntl } from "react-intl"; import { FetchResult } from "@apollo/client"; -import Modal from "src/components/Shared/Modal"; -import { useToast } from "src/hooks"; +import { ModalComponent } from "./Modal"; +import { useToast } from "src/hooks/Toast"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; interface IDeletionEntity { @@ -39,7 +39,7 @@ const messages = defineMessages({ }, }); -const DeleteEntityDialog: React.FC = ({ +export const DeleteEntityDialog: React.FC = ({ selected, onClose, singularEntity, @@ -77,7 +77,7 @@ const DeleteEntityDialog: React.FC = ({ } return ( - = ({ /> )} - + ); }; - -export default DeleteEntityDialog; diff --git a/ui/v2.5/src/components/Shared/DeleteFilesDialog.tsx b/ui/v2.5/src/components/Shared/DeleteFilesDialog.tsx index 9e23c400f..e6898c87a 100644 --- a/ui/v2.5/src/components/Shared/DeleteFilesDialog.tsx +++ b/ui/v2.5/src/components/Shared/DeleteFilesDialog.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import { mutateDeleteFiles } from "src/core/StashService"; -import { Modal } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { ModalComponent } from "./Modal"; +import { useToast } from "src/hooks/Toast"; import { FormattedMessage, useIntl } from "react-intl"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; @@ -88,7 +88,7 @@ export const DeleteFilesDialog: React.FC = ( } return ( - = ( >

    {message}

    {renderDeleteFileAlert()} -
    + ); }; - -export default DeleteFilesDialog; diff --git a/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx b/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx index 5c4854c02..fb5646ff5 100644 --- a/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx +++ b/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx @@ -1,7 +1,7 @@ import { Button, Modal } from "react-bootstrap"; import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { ImageInput } from "src/components/Shared/ImageInput"; +import { ImageInput } from "./ImageInput"; import cx from "classnames"; interface IProps { diff --git a/ui/v2.5/src/components/Shared/DurationInput.tsx b/ui/v2.5/src/components/Shared/DurationInput.tsx index b5839790b..0e346acd7 100644 --- a/ui/v2.5/src/components/Shared/DurationInput.tsx +++ b/ui/v2.5/src/components/Shared/DurationInput.tsx @@ -5,8 +5,8 @@ import { } from "@fortawesome/free-solid-svg-icons"; import React, { useState, useEffect } from "react"; import { Button, ButtonGroup, InputGroup, Form } from "react-bootstrap"; -import Icon from "src/components/Shared/Icon"; -import { DurationUtils } from "src/utils"; +import { Icon } from "./Icon"; +import DurationUtils from "src/utils/duration"; interface IProps { disabled?: boolean; diff --git a/ui/v2.5/src/components/Shared/ErrorMessage.tsx b/ui/v2.5/src/components/Shared/ErrorMessage.tsx index 2e40a35df..b0cdb12f3 100644 --- a/ui/v2.5/src/components/Shared/ErrorMessage.tsx +++ b/ui/v2.5/src/components/Shared/ErrorMessage.tsx @@ -4,10 +4,8 @@ interface IProps { error: string | ReactNode; } -const ErrorMessage: React.FC = ({ error }) => ( +export const ErrorMessage: React.FC = ({ error }) => (

    Error: {error}

    ); - -export default ErrorMessage; diff --git a/ui/v2.5/src/components/Shared/ExportDialog.tsx b/ui/v2.5/src/components/Shared/ExportDialog.tsx index 3c0ad5b7c..bf855d732 100644 --- a/ui/v2.5/src/components/Shared/ExportDialog.tsx +++ b/ui/v2.5/src/components/Shared/ExportDialog.tsx @@ -1,8 +1,8 @@ import React, { useState } from "react"; import { Form } from "react-bootstrap"; import { mutateExportObjects } from "src/core/StashService"; -import Modal from "src/components/Shared/Modal"; -import useToast from "src/hooks/Toast"; +import { ModalComponent } from "./Modal"; +import { useToast } from "src/hooks/Toast"; import downloadFile from "src/utils/download"; import { ExportObjectsInput } from "src/core/generated-graphql"; import { useIntl } from "react-intl"; @@ -46,7 +46,7 @@ export const ExportDialog: React.FC = ( } return ( - = ( /> - + ); }; diff --git a/ui/v2.5/src/components/Shared/FolderSelect/FolderSelect.tsx b/ui/v2.5/src/components/Shared/FolderSelect/FolderSelect.tsx index f6c6de544..9dfd61f5b 100644 --- a/ui/v2.5/src/components/Shared/FolderSelect/FolderSelect.tsx +++ b/ui/v2.5/src/components/Shared/FolderSelect/FolderSelect.tsx @@ -1,17 +1,20 @@ -import React, { useEffect, useState, useMemo } from "react"; +import React, { useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { Button, InputGroup, Form } from "react-bootstrap"; -import debounce from "lodash-es/debounce"; -import Icon from "src/components/Shared/Icon"; -import LoadingIndicator from "src/components/Shared/LoadingIndicator"; +import { Button, InputGroup, Form, Collapse } from "react-bootstrap"; +import { Icon } from "../Icon"; +import { LoadingIndicator } from "../LoadingIndicator"; import { useDirectory } from "src/core/StashService"; -import { faTimes } from "@fortawesome/free-solid-svg-icons"; +import { faEllipsis, faTimes } from "@fortawesome/free-solid-svg-icons"; +import { useDebouncedSetState } from "src/hooks/debounce"; interface IProps { currentDirectory: string; setCurrentDirectory: (value: string) => void; defaultDirectories?: string[]; appendButton?: JSX.Element; + collapsible?: boolean; + quoteSpaced?: boolean; + hideError?: boolean; } export const FolderSelect: React.FC = ({ @@ -19,33 +22,43 @@ export const FolderSelect: React.FC = ({ setCurrentDirectory, defaultDirectories, appendButton, + collapsible = false, + quoteSpaced = false, + hideError = false, }) => { - const [debouncedDirectory, setDebouncedDirectory] = useState( - currentDirectory + const [showBrowser, setShowBrowser] = React.useState(false); + const [directory, setDirectory] = useState(currentDirectory); + + const isQuoted = + quoteSpaced && directory.startsWith('"') && directory.endsWith('"'); + const { data, error, loading } = useDirectory( + isQuoted ? directory.slice(1, -1) : directory ); - const { data, error, loading } = useDirectory(debouncedDirectory); + const intl = useIntl(); - const selectableDirectories: string[] = currentDirectory - ? data?.directory.directories ?? defaultDirectories ?? [] - : defaultDirectories ?? []; + const defaultDirectoriesOrEmpty = defaultDirectories ?? []; - const debouncedSetDirectory = useMemo( - () => - debounce((input: string) => { - setDebouncedDirectory(input); - }, 250), - [] - ); + const selectableDirectories: string[] = currentDirectory + ? data?.directory.directories ?? + (error && hideError ? [] : defaultDirectoriesOrEmpty) + : defaultDirectoriesOrEmpty; + + const debouncedSetDirectory = useDebouncedSetState(setDirectory, 250); useEffect(() => { - if (currentDirectory === "" && !defaultDirectories && data?.directory.path) - setCurrentDirectory(data.directory.path); - }, [currentDirectory, setCurrentDirectory, data, defaultDirectories]); + if (currentDirectory !== directory) { + debouncedSetDirectory(currentDirectory); + } + }, [currentDirectory, directory, debouncedSetDirectory]); function setInstant(value: string) { + if (quoteSpaced && value.includes(" ")) { + value = `"${value}"`; + } + setCurrentDirectory(value); - setDebouncedDirectory(value); + setDirectory(value); } function setDebounced(value: string) { @@ -74,6 +87,7 @@ export const FolderSelect: React.FC = ({ <> ) => { setDebounced(e.currentTarget.value); @@ -84,31 +98,43 @@ export const FolderSelect: React.FC = ({ {appendButton ? ( {appendButton} ) : undefined} + {collapsible ? ( + + + + ) : undefined} {!data || !data.directory || loading ? ( {loading ? ( - ) : ( + ) : !hideError ? ( - )} + ) : undefined} ) : undefined} - {error !== undefined && ( + {!hideError && error !== undefined && (
    Error: {error.message}
    )} -
      - {topDirectory} - {selectableDirectories.map((path) => { - return ( -
    • - -
    • - ); - })} -
    + +
      + {topDirectory} + {selectableDirectories.map((path) => { + return ( +
    • + +
    • + ); + })} +
    +
    ); }; diff --git a/ui/v2.5/src/components/Shared/GridCard.tsx b/ui/v2.5/src/components/Shared/GridCard.tsx index f7ec2ee81..332bcaf04 100644 --- a/ui/v2.5/src/components/Shared/GridCard.tsx +++ b/ui/v2.5/src/components/Shared/GridCard.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Card, Form } from "react-bootstrap"; import { Link } from "react-router-dom"; import cx from "classnames"; -import TruncatedText from "./TruncatedText"; +import { TruncatedText } from "./TruncatedText"; interface ICardProps { className?: string; @@ -10,7 +10,7 @@ interface ICardProps { thumbnailSectionClassName?: string; url: string; pretitleIcon?: JSX.Element; - title: string; + title: JSX.Element | string; image: JSX.Element; details?: JSX.Element; overlays?: JSX.Element; diff --git a/ui/v2.5/src/components/Shared/Icon.tsx b/ui/v2.5/src/components/Shared/Icon.tsx index d1f2616f6..f5cb23964 100644 --- a/ui/v2.5/src/components/Shared/Icon.tsx +++ b/ui/v2.5/src/components/Shared/Icon.tsx @@ -9,7 +9,7 @@ interface IIcon { size?: SizeProp; } -const Icon: React.FC = ({ icon, className, color, size }) => ( +export const Icon: React.FC = ({ icon, className, color, size }) => ( = ({ icon, className, color, size }) => ( size={size} /> ); - -export default Icon; diff --git a/ui/v2.5/src/components/Shared/ImageInput.tsx b/ui/v2.5/src/components/Shared/ImageInput.tsx index f1ace6f83..cf25aa887 100644 --- a/ui/v2.5/src/components/Shared/ImageInput.tsx +++ b/ui/v2.5/src/components/Shared/ImageInput.tsx @@ -8,8 +8,8 @@ import { Row, } from "react-bootstrap"; import { useIntl } from "react-intl"; -import Modal from "./Modal"; -import Icon from "./Icon"; +import { ModalComponent } from "./Modal"; +import { Icon } from "./Icon"; import { faFile, faLink } from "@fortawesome/free-solid-svg-icons"; interface IImageInput { @@ -64,7 +64,7 @@ export const ImageInput: React.FC = ({ function renderDialog() { return ( - setIsShowDialog(false)} header={intl.formatMessage({ id: "dialogs.set_image_url_title" })} @@ -90,7 +90,7 @@ export const ImageInput: React.FC = ({
    - + ); } diff --git a/ui/v2.5/src/components/Shared/LoadingIndicator.tsx b/ui/v2.5/src/components/Shared/LoadingIndicator.tsx index f52498f0b..3b9247c91 100644 --- a/ui/v2.5/src/components/Shared/LoadingIndicator.tsx +++ b/ui/v2.5/src/components/Shared/LoadingIndicator.tsx @@ -12,7 +12,7 @@ interface ILoadingProps { const CLASSNAME = "LoadingIndicator"; const CLASSNAME_MESSAGE = `${CLASSNAME}-message`; -const LoadingIndicator: React.FC = ({ +export const LoadingIndicator: React.FC = ({ message, inline = false, small = false, @@ -27,5 +27,3 @@ const LoadingIndicator: React.FC = ({ )}
    ); - -export default LoadingIndicator; diff --git a/ui/v2.5/src/components/Shared/MarkdownPage.tsx b/ui/v2.5/src/components/Shared/MarkdownPage.tsx index b9fc93eba..c9713be55 100644 --- a/ui/v2.5/src/components/Shared/MarkdownPage.tsx +++ b/ui/v2.5/src/components/Shared/MarkdownPage.tsx @@ -1,11 +1,10 @@ import React, { useEffect, useState } from "react"; -import ReactMarkdown from "react-markdown"; -import gfm from "remark-gfm"; +import { Remark } from "react-remark"; +import remarkGfm from "remark-gfm"; interface IPageProps { // page is a markdown module - // eslint-disable-next-line @typescript-eslint/no-explicit-any - page: any; + page: string; } export const MarkdownPage: React.FC = ({ page }) => { @@ -20,8 +19,8 @@ export const MarkdownPage: React.FC = ({ page }) => { }, [page, markdown]); return ( - - {markdown} - +
    + {markdown} +
    ); }; diff --git a/ui/v2.5/src/components/Shared/Modal.tsx b/ui/v2.5/src/components/Shared/Modal.tsx index 8cf3b8029..df2e2f270 100644 --- a/ui/v2.5/src/components/Shared/Modal.tsx +++ b/ui/v2.5/src/components/Shared/Modal.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Button, Modal, Spinner, ModalProps } from "react-bootstrap"; -import Icon from "src/components/Shared/Icon"; +import { Icon } from "./Icon"; import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { FormattedMessage } from "react-intl"; @@ -27,7 +27,7 @@ interface IModal { const defaultOnHide = () => {}; -const ModalComponent: React.FC = ({ +export const ModalComponent: React.FC = ({ children, show, icon, @@ -99,5 +99,3 @@ const ModalComponent: React.FC = ({ ); - -export default ModalComponent; diff --git a/ui/v2.5/src/components/Shared/MultiSet.tsx b/ui/v2.5/src/components/Shared/MultiSet.tsx index 6f7aa08e4..c19922683 100644 --- a/ui/v2.5/src/components/Shared/MultiSet.tsx +++ b/ui/v2.5/src/components/Shared/MultiSet.tsx @@ -1,15 +1,9 @@ -import * as React from "react"; +import React from "react"; import { useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { Button, ButtonGroup } from "react-bootstrap"; -import { FilterSelect } from "./Select"; - -type ValidTypes = - | GQL.SlimPerformerDataFragment - | GQL.SlimTagDataFragment - | GQL.SlimStudioDataFragment - | GQL.SlimMovieDataFragment; +import { FilterSelect, SelectObject } from "./Select"; interface IMultiSetProps { type: "performers" | "studios" | "tags" | "movies"; @@ -21,9 +15,7 @@ interface IMultiSetProps { onSetMode: (mode: GQL.BulkUpdateIdMode) => void; } -const MultiSet: React.FunctionComponent = ( - props: IMultiSetProps -) => { +export const MultiSet: React.FC = (props) => { const intl = useIntl(); const modes = [ GQL.BulkUpdateIdMode.Set, @@ -31,7 +23,7 @@ const MultiSet: React.FunctionComponent = ( GQL.BulkUpdateIdMode.Remove, ]; - function onUpdate(items: ValidTypes[]) { + function onUpdate(items: SelectObject[]) { props.onUpdate(items.map((i) => i.id)); } @@ -102,5 +94,3 @@ const MultiSet: React.FunctionComponent = (
    ); }; - -export default MultiSet; diff --git a/ui/v2.5/src/components/Shared/OperationButton.tsx b/ui/v2.5/src/components/Shared/OperationButton.tsx index c71d9e139..89155467c 100644 --- a/ui/v2.5/src/components/Shared/OperationButton.tsx +++ b/ui/v2.5/src/components/Shared/OperationButton.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useEffect } from "react"; import { Button, ButtonProps } from "react-bootstrap"; -import LoadingIndicator from "src/components/Shared/LoadingIndicator"; +import { LoadingIndicator } from "./LoadingIndicator"; interface IOperationButton extends ButtonProps { operation?: () => Promise; diff --git a/ui/v2.5/src/components/Shared/PercentInput.tsx b/ui/v2.5/src/components/Shared/PercentInput.tsx index 783ead755..66357a05b 100644 --- a/ui/v2.5/src/components/Shared/PercentInput.tsx +++ b/ui/v2.5/src/components/Shared/PercentInput.tsx @@ -5,8 +5,8 @@ import { } from "@fortawesome/free-solid-svg-icons"; import React, { useState, useEffect } from "react"; import { Button, ButtonGroup, InputGroup, Form } from "react-bootstrap"; -import Icon from "src/components/Shared/Icon"; -import { PercentUtils } from "src/utils"; +import { Icon } from "./Icon"; +import PercentUtils from "src/utils/percent"; interface IProps { disabled?: boolean; diff --git a/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx b/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx index d12da10d3..9d0cfb6fe 100644 --- a/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx +++ b/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx @@ -5,7 +5,7 @@ import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; import { sortPerformers } from "src/core/performers"; import { HoverPopover } from "./HoverPopover"; -import Icon from "./Icon"; +import { Icon } from "./Icon"; import { TagLink } from "./TagLink"; interface IProps { diff --git a/ui/v2.5/src/components/Shared/PopoverCountButton.tsx b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx index 901424b98..6d6ef8571 100644 --- a/ui/v2.5/src/components/Shared/PopoverCountButton.tsx +++ b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx @@ -3,6 +3,7 @@ import { faImage, faImages, faPlayCircle, + faUser, } from "@fortawesome/free-solid-svg-icons"; import React, { useMemo } from "react"; import { Button } from "react-bootstrap"; @@ -10,10 +11,10 @@ import { FormattedNumber, useIntl } from "react-intl"; import { Link } from "react-router-dom"; import { IUIConfig } from "src/core/config"; import { ConfigurationContext } from "src/hooks/Config"; -import { TextUtils } from "src/utils"; -import Icon from "./Icon"; +import TextUtils from "src/utils/text"; +import { Icon } from "./Icon"; -type PopoverLinkType = "scene" | "image" | "gallery" | "movie"; +type PopoverLinkType = "scene" | "image" | "gallery" | "movie" | "performer"; interface IProps { className?: string; @@ -44,6 +45,8 @@ export const PopoverCountButton: React.FC = ({ return faImages; case "movie": return faFilm; + case "performer": + return faUser; } } @@ -69,6 +72,11 @@ export const PopoverCountButton: React.FC = ({ one: "movie", other: "movies", }; + case "performer": + return { + one: "performer", + other: "performers", + }; } } diff --git a/ui/v2.5/src/components/Shared/Rating/RatingStars.tsx b/ui/v2.5/src/components/Shared/Rating/RatingStars.tsx index e2801ee80..99e2e5be6 100644 --- a/ui/v2.5/src/components/Shared/Rating/RatingStars.tsx +++ b/ui/v2.5/src/components/Shared/Rating/RatingStars.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { Button } from "react-bootstrap"; -import Icon from "src/components/Shared/Icon"; +import { Icon } from "../Icon"; import { faStar as fasStar } from "@fortawesome/free-solid-svg-icons"; import { faStar as farStar } from "@fortawesome/free-regular-svg-icons"; import { @@ -32,7 +32,8 @@ export const RatingStars: React.FC = ( starPrecision: props.precision, }); const stars = rating ? Math.floor(rating) : 0; - const fraction = rating ? rating % 1 : 0; + // the upscaling was necesary to fix rounding issue present with tenth place precision + const fraction = rating ? ((rating * 10) % 10) / 10 : 0; const max = 5; const precision = getRatingPrecision(props.precision); @@ -223,11 +224,28 @@ export const RatingStars: React.FC = ( ); }; + const maybeRenderStarRatingNumber = () => { + const ratingFraction = getCurrentSelectedRating(); + if ( + !ratingFraction || + (ratingFraction.rating == 0 && ratingFraction.fraction == 0) + ) { + return; + } + + return ( + + {ratingFraction.rating + ratingFraction.fraction} + + ); + }; + return (
    {Array.from(Array(max)).map((value, index) => renderRatingButton(index + 1) )} + {maybeRenderStarRatingNumber()}
    ); }; diff --git a/ui/v2.5/src/components/Shared/Rating/styles.scss b/ui/v2.5/src/components/Shared/Rating/styles.scss index 2784943bb..f7b463359 100644 --- a/ui/v2.5/src/components/Shared/Rating/styles.scss +++ b/ui/v2.5/src/components/Shared/Rating/styles.scss @@ -21,18 +21,50 @@ width: 0; } + &.star-fill-10 .filled-star { + width: 10%; + } + + &.star-fill-20 .filled-star { + width: 20%; + } + &.star-fill-25 .filled-star { width: 35%; } + &.star-fill-30 .filled-star { + width: 30%; + } + + &.star-fill-40 .filled-star { + width: 40%; + } + &.star-fill-50 .filled-star { width: 50%; } + &.star-fill-60 .filled-star { + width: 60%; + } + &.star-fill-75 .filled-star { width: 65%; } + &.star-fill-70 .filled-star { + width: 70%; + } + + &.star-fill-80 .filled-star { + width: 80%; + } + + &.star-fill-90 .filled-star { + width: 90%; + } + &.star-fill-100 .filled-star { width: 100%; } @@ -56,6 +88,11 @@ } } +.star-rating-number { + font-size: 1rem; + margin: auto 0.5rem; +} + .rating-number.disabled { align-items: center; display: inline-flex; diff --git a/ui/v2.5/src/components/Shared/ReassignFilesDialog.tsx b/ui/v2.5/src/components/Shared/ReassignFilesDialog.tsx index 8a15359b2..7629d0379 100644 --- a/ui/v2.5/src/components/Shared/ReassignFilesDialog.tsx +++ b/ui/v2.5/src/components/Shared/ReassignFilesDialog.tsx @@ -1,10 +1,11 @@ import React, { useState } from "react"; -import { Modal, SceneSelect } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { ModalComponent } from "./Modal"; +import { SceneSelect } from "./Select"; +import { useToast } from "src/hooks/Toast"; import { useIntl } from "react-intl"; import { faSignOutAlt } from "@fortawesome/free-solid-svg-icons"; import { Col, Form, Row } from "react-bootstrap"; -import { FormUtils } from "src/utils"; +import FormUtils from "src/utils/form"; import { mutateSceneAssignFile } from "src/core/StashService"; interface IFile { @@ -59,7 +60,7 @@ export const ReassignFilesDialog: React.FC = ( } return ( - = ( - + ); }; - -export default ReassignFilesDialog; diff --git a/ui/v2.5/src/components/Shared/ScrapeDialog.tsx b/ui/v2.5/src/components/Shared/ScrapeDialog.tsx index 57609635e..0a5b1b9b2 100644 --- a/ui/v2.5/src/components/Shared/ScrapeDialog.tsx +++ b/ui/v2.5/src/components/Shared/ScrapeDialog.tsx @@ -8,9 +8,9 @@ import { FormControl, Badge, } from "react-bootstrap"; -import { CollapseButton } from "src/components/Shared/CollapseButton"; -import Icon from "src/components/Shared/Icon"; -import Modal from "src/components/Shared/Modal"; +import { CollapseButton } from "./CollapseButton"; +import { Icon } from "./Icon"; +import { ModalComponent } from "./Modal"; import isEqual from "lodash-es/isEqual"; import clone from "lodash-es/clone"; import { FormattedMessage, useIntl } from "react-intl"; @@ -20,8 +20,8 @@ import { faPlus, faTimes, } from "@fortawesome/free-solid-svg-icons"; -import { getCountryByISO } from "src/utils"; -import CountrySelect from "./CountrySelect"; +import { getCountryByISO } from "src/utils/country"; +import { CountrySelect } from "./CountrySelect"; export class ScrapeResult { public newValue?: T; @@ -36,10 +36,13 @@ export class ScrapeResult { ) { this.originalValue = originalValue ?? undefined; this.newValue = newValue ?? undefined; + // NOTE: this means that zero values are treated as null + // this is incorrect for numbers and booleans, but correct for strings + const hasNewValue = !!this.newValue; const valuesEqual = isEqual(originalValue, newValue); - this.useNewValue = useNewValue ?? (!!this.newValue && !valuesEqual); - this.scraped = !!this.newValue && !valuesEqual; + this.useNewValue = useNewValue ?? (hasNewValue && !valuesEqual); + this.scraped = hasNewValue && !valuesEqual; } public setOriginalValue(value?: T) { @@ -67,6 +70,23 @@ export class ScrapeResult { } } +// for types where !!value is a valid value (boolean and number) +export class ZeroableScrapeResult extends ScrapeResult { + public constructor( + originalValue?: T | null, + newValue?: T | null, + useNewValue?: boolean + ) { + super(originalValue, newValue, useNewValue); + + const hasNewValue = this.newValue !== undefined; + + const valuesEqual = isEqual(originalValue, newValue); + this.useNewValue = useNewValue ?? (hasNewValue && !valuesEqual); + this.scraped = hasNewValue && !valuesEqual; + } +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any export function hasScrapedValues(values: ScrapeResult[]) { return values.some((r) => r.scraped); @@ -367,7 +387,7 @@ export const ScrapeDialog: React.FC = ( ) => { const intl = useIntl(); return ( - = ( {props.renderScrapeRows()} - + ); }; diff --git a/ui/v2.5/src/components/Shared/Select.tsx b/ui/v2.5/src/components/Shared/Select.tsx index 8b44b864c..195c52bd7 100644 --- a/ui/v2.5/src/components/Shared/Select.tsx +++ b/ui/v2.5/src/components/Shared/Select.tsx @@ -1,16 +1,15 @@ import React, { useEffect, useMemo, useState } from "react"; import Select, { - ValueType, - Styles, + OnChangeValue, + StylesConfig, OptionProps, components as reactSelectComponents, - GroupedOptionsType, - OptionsType, - MenuListComponentProps, - GroupTypeBase, + Options, + MenuListProps, + GroupBase, + OptionsOrGroups, } from "react-select"; import CreatableSelect from "react-select/creatable"; -import debounce from "lodash-es/debounce"; import * as GQL from "src/core/generated-graphql"; import { @@ -23,19 +22,21 @@ import { useStudioCreate, usePerformerCreate, } from "src/core/StashService"; -import { useToast } from "src/hooks"; -import { SelectComponents } from "react-select/src/components"; +import { useToast } from "src/hooks/Toast"; +import { SelectComponents } from "react-select/dist/declarations/src/components"; import { ConfigurationContext } from "src/hooks/Config"; import { useIntl } from "react-intl"; import { objectTitle } from "src/core/files"; import { galleryTitle } from "src/core/galleries"; import { TagPopover } from "../Tags/TagPopover"; +import { defaultMaxOptionsShown, IUIConfig } from "src/core/config"; +import { useDebouncedSetState } from "src/hooks/debounce"; -export type ValidTypes = - | GQL.SlimPerformerDataFragment - | GQL.SlimTagDataFragment - | GQL.SlimStudioDataFragment - | GQL.SlimMovieDataFragment; +export type SelectObject = { + id: string; + name?: string | null; + title?: string | null; +}; type Option = { value: string; label: string }; interface ITypeProps { @@ -53,7 +54,7 @@ interface ITypeProps { interface IFilterProps { ids?: string[]; initialIds?: string[]; - onSelect?: (item: ValidTypes[]) => void; + onSelect?: (item: SelectObject[]) => void; noSelectionString?: string; className?: string; isMulti?: boolean; @@ -65,22 +66,22 @@ interface IFilterProps { interface ISelectProps { className?: string; items: Option[]; - selectedOptions?: ValueType; + selectedOptions?: OnChangeValue; creatable?: boolean; onCreateOption?: (value: string) => void; isLoading: boolean; isDisabled?: boolean; - onChange: (item: ValueType) => void; + onChange: (item: OnChangeValue) => void; initialIds?: string[]; isMulti: T; isClearable?: boolean; onInputChange?: (input: string) => void; - components?: Partial>; + components?: Partial>>; filterOption?: (option: Option, rawInput: string) => boolean; isValidNewOption?: ( inputValue: string, - value: ValueType, - options: OptionsType
    - {stashID.stash_id} - - ) : ( -
    {stashID.stash_id}
    - ); + if (stashID !== undefined) { + const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0]; + const link = base ? ( + + {stashID.stash_id} + + ) : ( +
    {stashID.stash_id}
    + ); - const endpoint = - stashBoxes?.findIndex((box) => box.endpoint === stashID.endpoint) ?? - -1; + const endpointIndex = + stashBoxes?.findIndex((box) => box.endpoint === stashID.endpoint) ?? + -1; - return ( -
    - - {link} - - {endpoint !== -1 && ( - - )} - - - {error[performer.id] && ( -
    - - Error: - {error[performer.id]?.message} - -
    {error[performer.id]?.details}
    -
    - )} -
    - ); - }); - subContent = <>{stashLinks}; + subContent = ( +
    + + {link} + + {endpointIndex !== -1 && ( + + )} + + + {error[performer.id] && ( +
    + + Error: + {error[performer.id]?.message} + +
    {error[performer.id]?.details}
    +
    + )} +
    + ); } else if (searchErrors[performer.id]) { subContent = (
    @@ -368,7 +547,14 @@ const PerformerTaggerList: React.FC = ({ to={`/performers/${performer.id}`} className={`${CLASSNAME}-header`} > -

    {performer.name}

    +

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

    {mainContent}
    {subContent}
    @@ -380,128 +566,24 @@ const PerformerTaggerList: React.FC = ({ return ( - setShowBatchUpdate(false), - }} - disabled={!isIdle} - > - - -
    - -
    -
    - } - defaultChecked - onChange={() => setQueryAll(false)} - /> - setQueryAll(true)} - /> -
    - - -
    - -
    -
    - setRefresh(false)} - /> - - - - setRefresh(true)} - /> - - - -
    - - - refresh ? p.stash_ids.length > 0 : p.stash_ids.length === 0 - ).length, - }} - /> - -
    - setShowBatchAdd(false), - }} - disabled={!isIdle} - > - setShowBatchUpdate(false)} + isIdle={isIdle} + selectedEndpoint={selectedEndpoint} + performers={performers} + onBatchUpdate={handleBatchUpdate} /> - - - - + )} + + {showBatchAdd && ( + setShowBatchAdd(false)} + isIdle={isIdle} + onBatchAdd={handleBatchAdd} + /> + )} +
    )); diff --git a/ui/v2.5/src/components/Tagger/scenes/Config.tsx b/ui/v2.5/src/components/Tagger/scenes/Config.tsx index 2eaa94a43..a04f78fcb 100644 --- a/ui/v2.5/src/components/Tagger/scenes/Config.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/Config.tsx @@ -10,7 +10,7 @@ import { } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; -import { Icon } from "src/components/Shared"; +import { Icon } from "src/components/Shared/Icon"; import { ParseMode, TagOperation } from "../constants"; import { TaggerStateContext } from "../context"; diff --git a/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx index 12bbef5d9..b17f7aedc 100755 --- a/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx @@ -4,12 +4,9 @@ import { FormattedMessage } from "react-intl"; import cx from "classnames"; import * as GQL from "src/core/generated-graphql"; -import { - Icon, - OperationButton, - PerformerSelect, - ValidTypes, -} from "src/components/Shared"; +import { Icon } from "src/components/Shared/Icon"; +import { OperationButton } from "src/components/Shared/OperationButton"; +import { PerformerSelect, SelectObject } from "src/components/Shared/Select"; import { OptionalField } from "../IncludeButton"; import { faSave } from "@fortawesome/free-solid-svg-icons"; @@ -30,20 +27,20 @@ const PerformerResult: React.FC = ({ onLink, endpoint, }) => { - const { - data: performerData, - loading: stashLoading, - } = GQL.useFindPerformerQuery({ - variables: { id: performer.stored_id ?? "" }, - skip: !performer.stored_id, - }); + const { data: performerData, loading: stashLoading } = + GQL.useFindPerformerQuery({ + variables: { id: performer.stored_id ?? "" }, + skip: !performer.stored_id, + }); const matchedPerformer = performerData?.findPerformer; const matchedStashID = matchedPerformer?.stash_ids.some( - (stashID) => stashID.endpoint === endpoint && stashID.stash_id + (stashID) => + stashID.endpoint === endpoint && + stashID.stash_id === performer.remote_site_id ); - const handlePerformerSelect = (performers: ValidTypes[]) => { + const handlePerformerSelect = (performers: SelectObject[]) => { if (performers.length) { setSelectedID(performers[0].id); } else { diff --git a/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx b/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx index 69270317d..96dd2cc1d 100755 --- a/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx @@ -1,10 +1,11 @@ -import React, { useContext, useState } from "react"; +import React, { useContext, useMemo, useState } from "react"; import * as GQL from "src/core/generated-graphql"; import { SceneQueue } from "src/models/sceneQueue"; import { Button, Form } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; -import { Icon, LoadingIndicator } from "src/components/Shared"; +import { Icon } from "src/components/Shared/Icon"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { OperationButton } from "src/components/Shared/OperationButton"; import { IScrapedScene, TaggerStateContext } from "../context"; import Config from "./Config"; @@ -14,6 +15,7 @@ import { SceneSearchResults } from "./StashSearchResult"; import { ConfigurationContext } from "src/hooks/Config"; import { faCog } from "@fortawesome/free-solid-svg-icons"; import { distance } from "src/utils/hamming"; +import { useLightbox } from "src/hooks/Lightbox/hooks"; interface ITaggerProps { scenes: GQL.SlimSceneDataFragment[]; @@ -181,14 +183,10 @@ export const Tagger: React.FC = ({ scenes, queue }) => { return -1; } - const [ - nbPhashMatchSceneA, - ratioPhashMatchSceneA, - ] = calculatePhashComparisonScore(stashScene, sceneA); - const [ - nbPhashMatchSceneB, - ratioPhashMatchSceneB, - ] = calculatePhashComparisonScore(stashScene, sceneB); + const [nbPhashMatchSceneA, ratioPhashMatchSceneA] = + calculatePhashComparisonScore(stashScene, sceneA); + const [nbPhashMatchSceneB, ratioPhashMatchSceneB] = + calculatePhashComparisonScore(stashScene, sceneB); if (nbPhashMatchSceneA != nbPhashMatchSceneB) { return nbPhashMatchSceneB - nbPhashMatchSceneA; @@ -224,6 +222,19 @@ export const Tagger: React.FC = ({ scenes, queue }) => { return minDurationDiffSceneA - minDurationDiffSceneB; } + const [spriteImage, setSpriteImage] = useState(null); + const lightboxImage = useMemo( + () => [{ paths: { thumbnail: spriteImage, image: spriteImage } }], + [spriteImage] + ); + const showLightbox = useLightbox({ + images: lightboxImage, + }); + function showLightboxImage(imagePath: string) { + setSpriteImage(imagePath); + showLightbox(); + } + function renderScenes() { const filteredScenes = !hideUnmatched ? scenes @@ -270,6 +281,7 @@ export const Tagger: React.FC = ({ scenes, queue }) => { } : undefined } + showLightboxImage={showLightboxImage} > {searchResult && searchResult.results?.length ? ( diff --git a/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx b/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx index 5ced87910..155f4950f 100755 --- a/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx @@ -7,16 +7,14 @@ import { blobToBase64 } from "base64-blob"; import { distance } from "src/utils/hamming"; import * as GQL from "src/core/generated-graphql"; -import { - HoverPopover, - Icon, - LoadingIndicator, - SuccessIcon, - TagSelect, - TruncatedText, - OperationButton, -} from "src/components/Shared"; -import { FormUtils } from "src/utils"; +import { HoverPopover } from "src/components/Shared/HoverPopover"; +import { Icon } from "src/components/Shared/Icon"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { SuccessIcon } from "src/components/Shared/SuccessIcon"; +import { TagSelect } from "src/components/Shared/Select"; +import { TruncatedText } from "src/components/Shared/TruncatedText"; +import { OperationButton } from "src/components/Shared/OperationButton"; +import FormUtils from "src/utils/form"; import { stringToGender } from "src/utils/gender"; import { IScrapedScene, TaggerStateContext } from "../context"; import { OptionalField } from "../IncludeButton"; diff --git a/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx b/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx index 4ce707297..326a2a83c 100644 --- a/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx @@ -3,7 +3,9 @@ import { FormattedMessage, useIntl } from "react-intl"; import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import * as GQL from "src/core/generated-graphql"; -import { Icon, Modal, TruncatedText } from "src/components/Shared"; +import { Icon } from "src/components/Shared/Icon"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { TruncatedText } from "src/components/Shared/TruncatedText"; import { TaggerStateContext } from "../context"; import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons"; @@ -75,7 +77,7 @@ const StudioModal: React.FC = ({ const link = base ? `${base}studios/${studio.remote_site_id}` : undefined; return ( - = ({
    */} - + ); }; diff --git a/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx b/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx index 25a97c112..ed76e5e70 100755 --- a/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx @@ -3,12 +3,9 @@ import { Button, ButtonGroup } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; import cx from "classnames"; -import { - Icon, - OperationButton, - StudioSelect, - ValidTypes, -} from "src/components/Shared"; +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"; @@ -41,7 +38,7 @@ const StudioResult: React.FC = ({ (stashID) => stashID.endpoint === endpoint && stashID.stash_id ); - const handleSelect = (studios: ValidTypes[]) => { + const handleSelect = (studios: SelectObject[]) => { if (studios.length) { setSelectedID(studios[0].id); } else { diff --git a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx index 4a4c9de44..7988a5b82 100644 --- a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx @@ -5,16 +5,18 @@ import { Button, Collapse, Form, InputGroup } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; import { sortPerformers } from "src/core/performers"; -import { - Icon, - OperationButton, - TagLink, - TruncatedText, -} from "src/components/Shared"; +import { Icon } from "src/components/Shared/Icon"; +import { OperationButton } from "src/components/Shared/OperationButton"; +import { TagLink } from "src/components/Shared/TagLink"; +import { TruncatedText } from "src/components/Shared/TruncatedText"; import { parsePath, prepareQueryString } from "src/components/Tagger/utils"; import { ScenePreview } from "src/components/Scenes/SceneCard"; import { TaggerStateContext } from "../context"; -import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; +import { + faChevronDown, + faChevronUp, + faImage, +} from "@fortawesome/free-solid-svg-icons"; import { objectPath, objectTitle } from "src/core/files"; interface ITaggerSceneDetails { @@ -86,6 +88,7 @@ interface ITaggerScene { doSceneQuery?: (queryString: string) => void; scrapeSceneFragment?: (scene: GQL.SlimSceneDataFragment) => void; loading?: boolean; + showLightboxImage: (imagePath: string) => void; } export const TaggerScene: React.FC> = ({ @@ -96,6 +99,7 @@ export const TaggerScene: React.FC> = ({ scrapeSceneFragment, errorMessage, children, + showLightboxImage, }) => { const { config } = useContext(TaggerStateContext); const [queryString, setQueryString] = useState(""); @@ -188,6 +192,27 @@ export const TaggerScene: React.FC> = ({ } } + function onSpriteClick(ev: React.MouseEvent) { + ev.preventDefault(); + showLightboxImage(scene.paths.sprite ?? ""); + } + + function maybeRenderSpriteIcon() { + // If a scene doesn't have any files, or doesn't have a sprite generated, the + // path will be http://localhost:9999/scene/_sprite.jpg + if (scene.files.length > 0) { + return ( + + ); + } + } + return (
    @@ -200,6 +225,7 @@ export const TaggerScene: React.FC> = ({ isPortrait={isPortrait} soundActive={false} /> + {maybeRenderSpriteIcon()}
    diff --git a/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx b/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx index fe670e32b..12161aaa3 100644 --- a/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx @@ -21,12 +21,11 @@ export interface ISceneTaggerModalsContextState { ) => void; } -export const SceneTaggerModalsState = React.createContext( - { +export const SceneTaggerModalsState = + React.createContext({ createPerformerModal: () => {}, createStudioModal: () => {}, - } -); + }); export const SceneTaggerModals: React.FC = ({ children }) => { const { currentSource } = useContext(TaggerStateContext); diff --git a/ui/v2.5/src/components/Tagger/styles.scss b/ui/v2.5/src/components/Tagger/styles.scss index d19494cf8..3a7594ca4 100644 --- a/ui/v2.5/src/components/Tagger/styles.scss +++ b/ui/v2.5/src/components/Tagger/styles.scss @@ -6,6 +6,10 @@ padding-bottom: 0; } + .scene-card { + position: relative; + } + .scene-card-preview { border-radius: 3px; margin-bottom: 0; @@ -18,6 +22,14 @@ } } + .sprite-button { + bottom: 5px; + filter: drop-shadow(1px 1px 1px #222); + padding: 0; + position: absolute; + right: 5px; + } + .sub-content { min-height: 1.5rem; } diff --git a/ui/v2.5/src/components/Tags/TagCard.tsx b/ui/v2.5/src/components/Tags/TagCard.tsx index 82286d899..7c2d2afcf 100644 --- a/ui/v2.5/src/components/Tags/TagCard.tsx +++ b/ui/v2.5/src/components/Tags/TagCard.tsx @@ -2,9 +2,10 @@ import { Button, ButtonGroup } from "react-bootstrap"; import React from "react"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; -import { NavUtils } from "src/utils"; +import NavUtils from "src/utils/navigation"; import { FormattedMessage } from "react-intl"; -import { Icon, TruncatedText } from "../Shared"; +import { Icon } from "../Shared/Icon"; +import { TruncatedText } from "../Shared/TruncatedText"; import { GridCard } from "../Shared/GridCard"; import { PopoverCountButton } from "../Shared/PopoverCountButton"; import { faMapMarkerAlt, faUser } from "@fortawesome/free-solid-svg-icons"; @@ -196,9 +197,9 @@ export const TagCard: React.FC = ({ {maybeRenderDescription()} {maybeRenderParents()} {maybeRenderChildren()} - {maybeRenderPopoverButtonGroup()} } + popovers={maybeRenderPopoverButtonGroup()} selected={selected} selecting={selecting} onSelectedChanged={onSelectedChanged} diff --git a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx index 57665b18d..7f60aa323 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 } from "react-bootstrap"; +import { Button, Tabs, Tab, Dropdown } from "react-bootstrap"; import React, { useEffect, useState } from "react"; import { useParams, useHistory } from "react-router-dom"; import { FormattedMessage, useIntl } from "react-intl"; @@ -12,16 +12,13 @@ import { useTagDestroy, mutateMetadataAutoTag, } from "src/core/StashService"; -import { ImageUtils } from "src/utils"; -import { - Counter, - DetailsEditNavbar, - ErrorMessage, - Modal, - LoadingIndicator, - Icon, -} from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { Counter } from "src/components/Shared/Counter"; +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 { tagRelationHook } from "src/core/tags"; import { TagScenesPanel } from "./TagScenesPanel"; @@ -52,6 +49,8 @@ const TagPage: React.FC = ({ tag }) => { const Toast = useToast(); const intl = useIntl(); + const [collapsed, setCollapsed] = useState(false); + // Configuration settings const { configuration } = React.useContext(ConfigurationContext); const abbreviateCounter = @@ -66,6 +65,7 @@ const TagPage: React.FC = ({ tag }) => { // Editing tag state const [image, setImage] = useState(); + const [encodingImage, setEncodingImage] = useState(false); const [updateTag] = useTagUpdate(); const [deleteTag] = useTagDestroy({ id: tag.id }); @@ -87,7 +87,10 @@ const TagPage: React.FC = ({ tag }) => { // set up hotkeys useEffect(() => { Mousetrap.bind("e", () => setIsEditing(true)); - Mousetrap.bind("d d", () => onDelete()); + Mousetrap.bind("d d", () => { + onDelete(); + }); + Mousetrap.bind(",", () => setCollapsed(!collapsed)); return () => { if (isEditing) { @@ -96,30 +99,11 @@ const TagPage: React.FC = ({ tag }) => { Mousetrap.unbind("e"); Mousetrap.unbind("d d"); + Mousetrap.unbind(","); }; }); - function onImageLoad(imageData: string) { - setImage(imageData); - } - - const imageEncoding = ImageUtils.usePasteImage(onImageLoad, isEditing); - - function getTagInput( - input: Partial - ) { - const ret: Partial = { - ...input, - image, - id: tag.id, - }; - - return ret; - } - - async function onSave( - input: Partial - ) { + async function onSave(input: GQL.TagCreateInput) { try { const oldRelations = { parents: tag.parents ?? [], @@ -127,7 +111,10 @@ const TagPage: React.FC = ({ tag }) => { }; const result = await updateTag({ variables: { - input: getTagInput(input) as GQL.TagUpdateInput, + input: { + id: tag.id, + ...input, + }, }, }); if (result.data?.tagUpdate) { @@ -177,7 +164,7 @@ const TagPage: React.FC = ({ tag }) => { function renderDeleteAlert() { return ( - = ({ tag }) => { }} />

    -
    + ); } @@ -262,15 +249,21 @@ const TagPage: React.FC = ({ tag }) => { ); } + function getCollapseButtonText() { + return collapsed ? ">" : "<"; + } + return ( <> {tag.name}
    -
    +
    - {imageEncoding ? ( + {encodingImage ? ( ) : ( renderImage() @@ -303,13 +296,20 @@ const TagPage: React.FC = ({ tag }) => { onCancel={onToggleEdit} onDelete={onDelete} setImage={setImage} + setEncodingImage={setEncodingImage} /> )}
    -
    +
    + +
    +
    @@ -325,7 +325,7 @@ const TagPage: React.FC = ({ tag }) => { } > - + = ({ tag }) => { } > - + = ({ tag }) => { } > - + = ({ tag }) => { } > - + = ({ tag }) => { } > - +
    diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx index b15ceedab..428d339b0 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx @@ -1,60 +1,37 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { useHistory, useLocation } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; import { useTagCreate } from "src/core/StashService"; -import { ImageUtils } from "src/utils"; -import { LoadingIndicator } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { useToast } from "src/hooks/Toast"; import { tagRelationHook } from "src/core/tags"; import { TagEditPanel } from "./TagEditPanel"; const TagCreate: React.FC = () => { const history = useHistory(); + const location = useLocation(); const Toast = useToast(); - function useQuery() { - const { search } = useLocation(); - return React.useMemo(() => new URLSearchParams(search), [search]); - } - - const query = useQuery(); - const nameQuery = query.get("name"); + const query = useMemo(() => new URLSearchParams(location.search), [location]); + const tag = { + name: query.get("q") ?? undefined, + }; // Editing tag state const [image, setImage] = useState(); + const [encodingImage, setEncodingImage] = useState(false); const [createTag] = useTagCreate(); - function onImageLoad(imageData: string) { - setImage(imageData); - } - - const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true); - - function getTagInput( - input: Partial - ) { - const ret: Partial = { - ...input, - image, - }; - - return ret; - } - - async function onSave( - input: Partial - ) { + async function onSave(input: GQL.TagCreateInput) { try { const oldRelations = { parents: [], children: [], }; const result = await createTag({ - variables: { - input: getTagInput(input) as GQL.TagCreateInput, - }, + variables: { input }, }); if (result.data?.tagCreate?.id) { const created = result.data.tagCreate; @@ -62,7 +39,7 @@ const TagCreate: React.FC = () => { parents: created.parents, children: created.children, }); - return created.id; + history.push(`/tags/${result.data.tagCreate.id}`); } } catch (e) { Toast.error(e); @@ -79,18 +56,19 @@ const TagCreate: React.FC = () => {
    - {imageEncoding ? ( + {encodingImage ? ( ) : ( renderImage() )}
    history.push("/tags")} onDelete={() => {}} setImage={setImage} + setEncodingImage={setEncodingImage} />
    diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx index 282340130..625da6106 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx @@ -2,27 +2,25 @@ import React, { useEffect } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import * as yup from "yup"; -import { DetailsEditNavbar, TagSelect } from "src/components/Shared"; +import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; +import { TagSelect } from "src/components/Shared/Select"; import { Form, Col, Row } from "react-bootstrap"; -import { FormUtils, ImageUtils } from "src/utils"; +import FormUtils from "src/utils/form"; +import ImageUtils from "src/utils/image"; import { useFormik } from "formik"; -import { Prompt, useHistory, useParams } from "react-router-dom"; +import { Prompt } from "react-router-dom"; import Mousetrap from "mousetrap"; import { StringListInput } from "src/components/Shared/StringListInput"; +import isEqual from "lodash-es/isEqual"; interface ITagEditPanel { - tag?: Partial; + tag: Partial; // returns id - onSubmit: ( - tag: Partial - ) => Promise; + onSubmit: (tag: GQL.TagCreateInput) => void; onCancel: () => void; onDelete: () => void; setImage: (image?: string | null) => void; -} - -interface ITagEditPanelParams { - id?: string; + setEncodingImage: (loading: boolean) => void; } export const TagEditPanel: React.FC = ({ @@ -31,14 +29,11 @@ export const TagEditPanel: React.FC = ({ onCancel, onDelete, setImage, + setEncodingImage, }) => { const intl = useIntl(); - const history = useHistory(); - const params = useParams(); - const idParam = params.id; - - const isNew = idParam === undefined; + const isNew = tag.id === undefined; const labelXS = 3; const labelXL = 3; @@ -47,47 +42,49 @@ export const TagEditPanel: React.FC = ({ const schema = yup.object({ name: yup.string().required(), - description: yup.string().optional().nullable(), aliases: yup .array(yup.string().required()) - .optional() + .defined() .test({ name: "unique", - // eslint-disable-next-line @typescript-eslint/no-explicit-any - test: (value: any) => { - return (value ?? []).length === new Set(value).size; + test: (value, context) => { + if (!value) return true; + const aliases = new Set(value); + aliases.add(context.parent.name); + return value.length + 1 === aliases.size; }, - message: intl.formatMessage({ id: "dialogs.aliases_must_be_unique" }), + message: intl.formatMessage({ + id: "validation.aliases_must_be_unique", + }), }), - parent_ids: yup.array(yup.string().required()).optional().nullable(), - child_ids: yup.array(yup.string().required()).optional().nullable(), - ignore_auto_tag: yup.boolean().optional(), + 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(), + image: yup.string().nullable().optional(), }); const initialValues = { - name: tag?.name, - description: tag?.description, - aliases: tag?.aliases, + name: tag?.name ?? "", + aliases: tag?.aliases ?? [], + description: tag?.description ?? "", parent_ids: (tag?.parents ?? []).map((t) => t.id), child_ids: (tag?.children ?? []).map((t) => t.id), ignore_auto_tag: tag?.ignore_auto_tag ?? false, }; - type InputValues = typeof initialValues; + type InputValues = yup.InferType; - const formik = useFormik({ + const formik = useFormik({ initialValues, validationSchema: schema, enableReinitialize: true, - onSubmit: doSubmit, + onSubmit: (values) => onSubmit(values), }); - async function doSubmit(values: InputValues) { - const id = await onSubmit(getTagInput(values)); - if (id) { - formik.resetForm({ values }); - history.push(`/tags/${id}`); - } + function onCancelEditing() { + setImage(undefined); + onCancel?.(); } // set up hotkeys @@ -99,19 +96,22 @@ export const TagEditPanel: React.FC = ({ }; }); - function getTagInput(values: InputValues) { - const input: Partial = { - ...values, - }; + const encodingImage = ImageUtils.usePasteImage(onImageLoad); - if (tag && tag.id) { - (input as GQL.TagUpdateInput).id = tag.id; - } - return input; + useEffect(() => { + setImage(formik.values.image); + }, [formik.values.image, setImage]); + + useEffect(() => { + setEncodingImage(encodingImage); + }, [setEncodingImage, encodingImage]); + + function onImageLoad(imageData: string | null) { + formik.setFieldValue("image", imageData); } function onImageChange(event: React.FormEvent) { - ImageUtils.onImageChange(event, setImage); + ImageUtils.onImageChange(event, onImageLoad); } const isEditing = true; @@ -130,8 +130,9 @@ export const TagEditPanel: React.FC = ({ { - if (!isNew && location.pathname.startsWith(`/tags/${tag?.id}`)) { + message={(location, action) => { + // Check if it's a redirect after movie creation + if (action === "PUSH" && location.pathname.startsWith("/tags/")) { return true; } return intl.formatMessage({ id: "dialogs.unsaved_changes" }); @@ -162,9 +163,13 @@ export const TagEditPanel: React.FC = ({ formik.setFieldValue("aliases", value)} - errors={formik.errors.aliases} + errors={ + Array.isArray(formik.errors.aliases) + ? formik.errors.aliases[0] + : formik.errors.aliases + } /> @@ -202,9 +207,10 @@ export const TagEditPanel: React.FC = ({ ) } ids={formik.values.parent_ids} - excludeIds={(tag?.id ? [tag.id] : []).concat( - ...formik.values.child_ids - )} + excludeIds={[ + ...(tag?.id ? [tag.id] : []), + ...formik.values.child_ids, + ]} creatable={false} /> @@ -229,9 +235,10 @@ export const TagEditPanel: React.FC = ({ ) } ids={formik.values.child_ids} - excludeIds={(tag?.id ? [tag.id] : []).concat( - ...formik.values.parent_ids - )} + excludeIds={[ + ...(tag?.id ? [tag.id] : []), + ...formik.values.parent_ids, + ]} creatable={false} /> @@ -258,13 +265,12 @@ export const TagEditPanel: React.FC = ({ objectName={tag?.name ?? intl.formatMessage({ id: "tag" })} isNew={isNew} isEditing={isEditing} - onToggleEdit={onCancel} - onSave={() => formik.handleSubmit()} + onToggleEdit={onCancelEditing} + onSave={formik.handleSubmit} + saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})} onImageChange={onImageChange} - onImageChangeURL={setImage} - onClearImage={() => { - setImage(null); - }} + 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 dd6ff61c3..203715bfb 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagGalleriesPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagGalleriesPanel.tsx @@ -4,10 +4,14 @@ import { useTagFilterHook } from "src/core/tags"; import { GalleryList } from "src/components/Galleries/GalleryList"; interface ITagGalleriesPanel { + active: boolean; tag: GQL.TagDataFragment; } -export const TagGalleriesPanel: React.FC = ({ tag }) => { +export const TagGalleriesPanel: React.FC = ({ + active, + tag, +}) => { const filterHook = useTagFilterHook(tag); - return ; + return ; }; diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagImagesPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagImagesPanel.tsx index c037cdb4b..1c6ea2ec9 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagImagesPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagImagesPanel.tsx @@ -4,10 +4,11 @@ import { useTagFilterHook } from "src/core/tags"; import { ImageList } from "src/components/Images/ImageList"; interface ITagImagesPanel { + active: boolean; tag: GQL.TagDataFragment; } -export const TagImagesPanel: React.FC = ({ tag }) => { +export const TagImagesPanel: React.FC = ({ active, tag }) => { const filterHook = useTagFilterHook(tag); - return ; + return ; }; diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagMarkersPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagMarkersPanel.tsx index 575729eb2..37d33ea2c 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagMarkersPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagMarkersPanel.tsx @@ -8,10 +8,14 @@ import { import { SceneMarkerList } from "src/components/Scenes/SceneMarkerList"; interface ITagMarkersPanel { + active: boolean; tag: GQL.TagDataFragment; } -export const TagMarkersPanel: React.FC = ({ tag }) => { +export const TagMarkersPanel: React.FC = ({ + active, + tag, +}) => { function filterHook(filter: ListFilterModel) { const tagValue = { id: tag.id, label: tag.name }; // if tag is already present, then we modify it, otherwise add @@ -47,5 +51,5 @@ export const TagMarkersPanel: React.FC = ({ tag }) => { return filter; } - return ; + return ; }; diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagMergeDialog.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagMergeDialog.tsx index a2c51383f..55eeb2f2e 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagMergeDialog.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagMergeDialog.tsx @@ -1,11 +1,12 @@ import { Form, Col, Row } from "react-bootstrap"; import React, { useState } from "react"; import * as GQL from "src/core/generated-graphql"; -import { Modal, TagSelect } from "src/components/Shared"; -import { FormUtils } from "src/utils"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { TagSelect } from "src/components/Shared/Select"; +import FormUtils from "src/utils/form"; import { useTagsMerge } from "src/core/StashService"; import { useIntl } from "react-intl"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; import { useHistory } from "react-router-dom"; import { faSignInAlt, faSignOutAlt } from "@fortawesome/free-solid-svg-icons"; @@ -72,7 +73,7 @@ export const TagMergeModal: React.FC = ({ } return ( - = ({ )}
    - + ); }; diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagPerformersPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagPerformersPanel.tsx index 43a447ed3..255acaf64 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagPerformersPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagPerformersPanel.tsx @@ -4,10 +4,14 @@ import { useTagFilterHook } from "src/core/tags"; import { PerformerList } from "src/components/Performers/PerformerList"; interface ITagPerformersPanel { + active: boolean; tag: GQL.TagDataFragment; } -export const TagPerformersPanel: React.FC = ({ tag }) => { +export const TagPerformersPanel: React.FC = ({ + active, + tag, +}) => { const filterHook = useTagFilterHook(tag); - return ; + return ; }; diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagScenesPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagScenesPanel.tsx index 972c19d16..5de9ed916 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagScenesPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagScenesPanel.tsx @@ -4,10 +4,11 @@ import { SceneList } from "src/components/Scenes/SceneList"; import { useTagFilterHook } from "src/core/tags"; interface ITagScenesPanel { + active: boolean; tag: GQL.TagDataFragment; } -export const TagScenesPanel: React.FC = ({ tag }) => { +export const TagScenesPanel: React.FC = ({ active, tag }) => { const filterHook = useTagFilterHook(tag); - return ; + return ; }; diff --git a/ui/v2.5/src/components/Tags/TagList.tsx b/ui/v2.5/src/components/Tags/TagList.tsx index e6201064f..98035f12e 100644 --- a/ui/v2.5/src/components/Tags/TagList.tsx +++ b/ui/v2.5/src/components/Tags/TagList.tsx @@ -1,27 +1,29 @@ import React, { useState } from "react"; import cloneDeep from "lodash-es/cloneDeep"; import Mousetrap from "mousetrap"; -import { FindTagsQueryResult } from "src/core/generated-graphql"; import { ListFilterModel } from "src/models/list-filter/filter"; import { DisplayMode } from "src/models/list-filter/types"; import { - showWhenSelected, - useTagsList, + makeItemList, PersistanceLevel, -} from "src/hooks/ListHook"; + showWhenSelected, +} from "../List/ItemList"; import { Button } from "react-bootstrap"; import { Link, useHistory } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; import { queryFindTags, mutateMetadataAutoTag, + useFindTags, useTagDestroy, useTagsDestroy, } from "src/core/StashService"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; -import { NavUtils } from "src/utils"; -import { Icon, Modal, DeleteEntityDialog } from "src/components/Shared"; +import NavUtils from "src/utils/navigation"; +import { Icon } from "../Shared/Icon"; +import { ModalComponent } from "../Shared/Modal"; +import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog"; import { TagCard } from "./TagCard"; import { ExportDialog } from "../Shared/ExportDialog"; import { tagRelationHook } from "../../core/tags"; @@ -29,16 +31,33 @@ import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; interface ITagList { filterHook?: (filter: ListFilterModel) => ListFilterModel; + alterQuery?: boolean; } -export const TagList: React.FC = ({ filterHook }) => { - const Toast = useToast(); - const [ - deletingTag, - setDeletingTag, - ] = useState | null>(null); +const TagItemList = makeItemList({ + filterMode: GQL.FilterMode.Tags, + useResult: useFindTags, + getItems(result: GQL.FindTagsQueryResult) { + return result?.data?.findTags?.tags ?? []; + }, + getCount(result: GQL.FindTagsQueryResult) { + return result?.data?.findTags?.count ?? 0; + }, +}); - const [deleteTag] = useTagDestroy(getDeleteTagInput() as GQL.TagDestroyInput); +export const TagList: React.FC = ({ filterHook, alterQuery }) => { + const Toast = useToast(); + const [deletingTag, setDeletingTag] = + useState | null>(null); + + 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(); @@ -61,10 +80,10 @@ export const TagList: React.FC = ({ filterHook }) => { }, ]; - const addKeybinds = ( - result: FindTagsQueryResult, + function addKeybinds( + result: GQL.FindTagsQueryResult, filter: ListFilterModel - ) => { + ) { Mousetrap.bind("p r", () => { viewRandom(result, filter); }); @@ -72,14 +91,14 @@ export const TagList: React.FC = ({ filterHook }) => { return () => { Mousetrap.unbind("p r"); }; - }; + } async function viewRandom( - result: FindTagsQueryResult, + result: GQL.FindTagsQueryResult, filter: ListFilterModel ) { // query for a random tag - if (result.data && result.data.findTags) { + if (result.data?.findTags) { const { count } = result.data.findTags; const index = Math.floor(Math.random() * count); @@ -87,13 +106,8 @@ export const TagList: React.FC = ({ filterHook }) => { filterCopy.itemsPerPage = 1; filterCopy.currentPage = index + 1; const singleResult = await queryFindTags(filterCopy); - if ( - singleResult && - singleResult.data && - singleResult.data.findTags && - singleResult.data.findTags.tags.length === 1 - ) { - const { id } = singleResult!.data!.findTags!.tags[0]; + if (singleResult.data.findTags.tags.length === 1) { + const { id } = singleResult.data.findTags.tags[0]; // navigate to the tag page history.push(`/tags/${id}`); } @@ -110,68 +124,6 @@ export const TagList: React.FC = ({ filterHook }) => { setIsExportDialogOpen(true); } - function maybeRenderExportDialog(selectedIds: Set) { - if (isExportDialogOpen) { - return ( - <> - { - setIsExportDialogOpen(false); - }} - /> - - ); - } - } - - const renderDeleteDialog = ( - selectedTags: GQL.TagDataFragment[], - onClose: (confirmed: boolean) => void - ) => ( - { - selectedTags.forEach((t) => - tagRelationHook( - t, - { parents: t.parents ?? [], children: t.children ?? [] }, - { parents: [], children: [] } - ) - ); - }} - /> - ); - - const listData = useTagsList({ - renderContent, - filterHook, - addKeybinds, - otherOperations, - selectable: true, - zoomable: true, - defaultZoomIndex: 0, - persistState: PersistanceLevel.ALL, - renderDeleteDialog, - }); - - function getDeleteTagInput() { - const tagInput: Partial = {}; - if (deletingTag) { - tagInput.id = deletingTag.id; - } - return tagInput; - } - async function onAutoTag(tag: GQL.TagDataFragment) { if (!tag) return; try { @@ -211,165 +163,214 @@ export const TagList: React.FC = ({ filterHook }) => { } } - function renderTags( - result: FindTagsQueryResult, + function renderContent( + result: GQL.FindTagsQueryResult, filter: ListFilterModel, - selectedIds: Set + selectedIds: Set, + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void ) { - if (!result.data?.findTags) return; - - if (filter.displayMode === DisplayMode.Grid) { - return ( -
    - {result.data.findTags.tags.map((tag) => ( - 0} - selected={selectedIds.has(tag.id)} - onSelectedChanged={(selected: boolean, shiftKey: boolean) => - listData.onSelectChange(tag.id, selected, shiftKey) - } - /> - ))} -
    - ); - } - 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) => { + function maybeRenderExportDialog() { + if (isExportDialogOpen) { return ( -
    - {tag.name} + setIsExportDialogOpen(false)} + /> + ); + } + } -
    - - - - - - - :{" "} - - - -
    + function renderTags() { + if (!result.data?.findTags) return; + + if (filter.displayMode === DisplayMode.Grid) { + return ( +
    + {result.data.findTags.tags.map((tag) => ( + 0} + selected={selectedIds.has(tag.id)} + onSelectedChanged={(selected: boolean, shiftKey: boolean) => + onSelectChange(tag.id, selected, shiftKey) + } + /> + ))}
    ); - }); + } + 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) }} + > + + + + + ); - return ( -
    - {tagElements} - {deleteAlert} -
    - ); - } - if (filter.displayMode === DisplayMode.Wall) { - return

    TODO

    ; - } - } + const tagElements = result.data.findTags.tags.map((tag) => { + return ( +
    + {tag.name} - function renderContent( - result: FindTagsQueryResult, - filter: ListFilterModel, - selectedIds: Set - ) { +
    + + + + + + + :{" "} + + + +
    +
    + ); + }); + + return ( +
    + {tagElements} + {deleteAlert} +
    + ); + } + if (filter.displayMode === DisplayMode.Wall) { + return

    TODO

    ; + } + } return ( <> - {maybeRenderExportDialog(selectedIds)} - {renderTags(result, filter, selectedIds)} + {maybeRenderExportDialog()} + {renderTags()} ); } - return listData.template; + function renderDeleteDialog( + selectedTags: GQL.TagDataFragment[], + onClose: (confirmed: boolean) => void + ) { + return ( + { + selectedTags.forEach((t) => + tagRelationHook( + t, + { parents: t.parents ?? [], children: t.children ?? [] }, + { parents: [], children: [] } + ) + ); + }} + /> + ); + } + + return ( + + ); }; diff --git a/ui/v2.5/src/components/Tags/TagPopover.tsx b/ui/v2.5/src/components/Tags/TagPopover.tsx index 8ec2ff36e..38b220486 100644 --- a/ui/v2.5/src/components/Tags/TagPopover.tsx +++ b/ui/v2.5/src/components/Tags/TagPopover.tsx @@ -1,6 +1,7 @@ import React from "react"; -import { ErrorMessage, LoadingIndicator } from "../Shared"; -import { HoverPopover } from "src/components/Shared"; +import { ErrorMessage } from "../Shared/ErrorMessage"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; +import { HoverPopover } from "../Shared/HoverPopover"; import { useFindTag } from "../../core/StashService"; import { TagCard } from "./TagCard"; import { ConfigurationContext } from "../../hooks/Config"; diff --git a/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx b/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx index c684bc2c4..4e50b4989 100644 --- a/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx +++ b/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx @@ -1,6 +1,6 @@ -import React, { FunctionComponent } from "react"; +import React from "react"; import { useFindTags } from "src/core/StashService"; -import Slider from "react-slick"; +import Slider from "@ant-design/react-slick"; import { TagCard } from "./TagCard"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getSlickSliderSettings } from "src/core/recommendations"; @@ -13,9 +13,7 @@ interface IProps { header: string; } -export const TagRecommendationRow: FunctionComponent = ( - props: IProps -) => { +export const TagRecommendationRow: React.FC = (props) => { const result = useFindTags(props.filter); const cardCount = result.data?.findTags.count; diff --git a/ui/v2.5/src/components/Tags/Tags.tsx b/ui/v2.5/src/components/Tags/Tags.tsx index c5f8bd9dc..66e2e9ab6 100644 --- a/ui/v2.5/src/components/Tags/Tags.tsx +++ b/ui/v2.5/src/components/Tags/Tags.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Route, Switch } from "react-router-dom"; import { useIntl } from "react-intl"; import { Helmet } from "react-helmet"; -import { TITLE_SUFFIX } from "src/components/Shared"; +import { TITLE_SUFFIX } from "src/components/Shared/constants"; import Tag from "./TagDetails/Tag"; import TagCreate from "./TagDetails/TagCreate"; import { TagList } from "./TagList"; diff --git a/ui/v2.5/src/components/Wall/WallItem.tsx b/ui/v2.5/src/components/Wall/WallItem.tsx index 25e4ce5aa..8f3555944 100644 --- a/ui/v2.5/src/components/Wall/WallItem.tsx +++ b/ui/v2.5/src/components/Wall/WallItem.tsx @@ -1,11 +1,13 @@ -import React, { useRef, useState, useEffect } from "react"; +import React, { useRef, useState, useEffect, useMemo } from "react"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; -import { TextUtils, NavUtils } from "src/utils"; +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 { markerTitle } from "src/core/markers"; +import { objectTitle } from "src/core/files"; interface IWallItemProps { index?: number; @@ -180,14 +182,23 @@ export const WallItem: React.FC = (props: IWallItemProps) => { } } + const title = useMemo(() => { + if (props.sceneMarker) { + return `${markerTitle( + props.sceneMarker + )} - ${TextUtils.secondsToTimestamp(props.sceneMarker.seconds)}`; + } + + if (props.scene) { + return objectTitle(props.scene); + } + + return ""; + }, [props.sceneMarker, props.scene]); + const renderText = () => { if (!showTextContainer) return; - const title = props.sceneMarker - ? `${markerTitle(props.sceneMarker)} - ${TextUtils.secondsToTimestamp( - props.sceneMarker.seconds - )}` - : props.scene?.title ?? ""; const tags = props.sceneMarker ? [props.sceneMarker.primary_tag, ...props.sceneMarker.tags] : []; diff --git a/ui/v2.5/src/components/Wall/WallPanel.tsx b/ui/v2.5/src/components/Wall/WallPanel.tsx index a50ce1414..2d7d5c932 100644 --- a/ui/v2.5/src/components/Wall/WallPanel.tsx +++ b/ui/v2.5/src/components/Wall/WallPanel.tsx @@ -53,16 +53,16 @@ export const WallPanel: React.FC = ( /> )); - const sceneMarkers = ( - props.sceneMarkers ?? [] - ).map((marker, index, markerArray) => ( - - )); + const sceneMarkers = (props.sceneMarkers ?? []).map( + (marker, index, markerArray) => ( + + ) + ); const images = (props.images ?? []).map((image, index, imageArray) => ( GQL.usePluginsQuery(); export const usePluginTasks = () => GQL.usePluginTasksQuery(); export const useMarkerStrings = () => GQL.useMarkerStringsQuery(); -export const useAllTags = () => GQL.useAllTagsQuery(); export const useAllTagsForFilter = () => GQL.useAllTagsForFilterQuery(); export const useAllPerformersForFilter = () => GQL.useAllPerformersForFilterQuery(); @@ -332,14 +331,22 @@ export const mutateSetup = (input: GQL.SetupInput) => client.mutate({ mutation: GQL.SetupDocument, variables: { input }, - refetchQueries: getQueryNames([GQL.ConfigurationDocument]), - update: deleteCache([GQL.ConfigurationDocument]), + refetchQueries: getQueryNames([ + GQL.ConfigurationDocument, + GQL.SystemStatusDocument, + ]), + update: deleteCache([GQL.ConfigurationDocument, GQL.SystemStatusDocument]), }); export const mutateMigrate = (input: GQL.MigrateInput) => client.mutate({ mutation: GQL.MigrateDocument, variables: { input }, + refetchQueries: getQueryNames([ + GQL.ConfigurationDocument, + GQL.SystemStatusDocument, + ]), + update: deleteCache([GQL.ConfigurationDocument, GQL.SystemStatusDocument]), }); export const useDirectory = (path?: string) => @@ -408,7 +415,6 @@ const sceneMutationImpactedQueries = [ GQL.FindMoviesDocument, GQL.FindTagDocument, GQL.FindTagsDocument, - GQL.AllTagsDocument, ]; export const useSceneUpdate = () => @@ -436,23 +442,13 @@ const updateSceneO = ( cache: ApolloCache, updatedOCount?: number ) => { - const scene = cache.readQuery< - GQL.FindSceneQuery, - GQL.FindSceneQueryVariables - >({ - query: GQL.FindSceneDocument, - variables: { id }, - }); - if (updatedOCount === undefined || !scene?.findScene) return; + if (updatedOCount === undefined) return; - cache.writeQuery({ - query: GQL.FindSceneDocument, - variables: { id }, - data: { - ...scene, - findScene: { - ...scene.findScene, - o_counter: updatedOCount, + cache.modify({ + id: cache.identify({ __typename: "Scene", id }), + fields: { + o_counter() { + return updatedOCount; }, }, }); @@ -572,7 +568,6 @@ const imageMutationImpactedQueries = [ GQL.FindStudiosDocument, GQL.FindTagDocument, GQL.FindTagsDocument, - GQL.AllTagsDocument, GQL.FindGalleryDocument, GQL.FindGalleriesDocument, ]; @@ -706,7 +701,6 @@ const galleryMutationImpactedQueries = [ GQL.FindStudiosDocument, GQL.FindTagDocument, GQL.FindTagsDocument, - GQL.AllTagsDocument, GQL.FindGalleryDocument, GQL.FindGalleriesDocument, ]; @@ -758,6 +752,27 @@ export const mutateGallerySetPrimaryFile = (id: string, fileID: string) => update: deleteCache(galleryMutationImpactedQueries), }); +const galleryChapterMutationImpactedQueries = [ + GQL.FindGalleryDocument, + GQL.FindGalleriesDocument, +]; + +export const useGalleryChapterCreate = () => + GQL.useGalleryChapterCreateMutation({ + refetchQueries: getQueryNames([GQL.FindGalleryDocument]), + update: deleteCache(galleryChapterMutationImpactedQueries), + }); +export const useGalleryChapterUpdate = () => + GQL.useGalleryChapterUpdateMutation({ + refetchQueries: getQueryNames([GQL.FindGalleryDocument]), + update: deleteCache(galleryChapterMutationImpactedQueries), + }); +export const useGalleryChapterDestroy = () => + GQL.useGalleryChapterDestroyMutation({ + refetchQueries: getQueryNames([GQL.FindGalleryDocument]), + update: deleteCache(galleryChapterMutationImpactedQueries), + }); + export const studioMutationImpactedQueries = [ GQL.FindStudiosDocument, GQL.FindSceneDocument, @@ -853,7 +868,6 @@ export const tagMutationImpactedQueries = [ GQL.FindSceneDocument, GQL.FindScenesDocument, GQL.FindSceneMarkersDocument, - GQL.AllTagsDocument, GQL.AllTagsForFilterDocument, GQL.FindTagsDocument, ]; @@ -861,15 +875,10 @@ export const tagMutationImpactedQueries = [ export const useTagCreate = () => GQL.useTagCreateMutation({ refetchQueries: getQueryNames([ - GQL.AllTagsDocument, GQL.AllTagsForFilterDocument, GQL.FindTagsDocument, ]), - update: deleteCache([ - GQL.FindTagsDocument, - GQL.AllTagsDocument, - GQL.AllTagsForFilterDocument, - ]), + update: deleteCache([GQL.AllTagsForFilterDocument, GQL.FindTagsDocument]), }); export const useTagUpdate = () => GQL.useTagUpdateMutation({ @@ -1206,6 +1215,20 @@ export const mutateMigrateHashNaming = () => mutation: GQL.MigrateHashNamingDocument, }); +export const mutateMigrateSceneScreenshots = ( + input: GQL.MigrateSceneScreenshotsInput +) => + client.mutate({ + mutation: GQL.MigrateSceneScreenshotsDocument, + variables: { input }, + }); + +export const mutateMigrateBlobs = (input: GQL.MigrateBlobsInput) => + client.mutate({ + mutation: GQL.MigrateBlobsDocument, + variables: { input }, + }); + export const mutateMetadataExport = () => client.mutate({ mutation: GQL.MetadataExportDocument, @@ -1234,6 +1257,12 @@ export const mutateBackupDatabase = (input: GQL.BackupDatabaseInput) => variables: { input }, }); +export const mutateAnonymiseDatabase = (input: GQL.AnonymiseDatabaseInput) => + client.mutate({ + mutation: GQL.AnonymiseDatabaseDocument, + variables: { input }, + }); + export const mutateStashBoxBatchPerformerTag = ( input: GQL.StashBoxBatchPerformerTagInput ) => diff --git a/ui/v2.5/src/core/config.ts b/ui/v2.5/src/core/config.ts index c43780735..024914c94 100644 --- a/ui/v2.5/src/core/config.ts +++ b/ui/v2.5/src/core/config.ts @@ -1,5 +1,6 @@ import { IntlShape } from "react-intl"; -import { ITypename } from "src/utils"; +import { ITypename } from "src/utils/data"; +import { ImageWallOptions } from "src/utils/imageWall"; import { RatingSystemOptions } from "src/utils/rating"; import { FilterMode, SortDirectionEnum } from "./generated-graphql"; @@ -26,6 +27,8 @@ export interface ICustomFilter extends ITypename { export type FrontPageContent = ISavedFilterRow | ICustomFilter; +export const defaultMaxOptionsShown = 200; + export interface IUIConfig { frontPageContent?: FrontPageContent[]; @@ -45,6 +48,12 @@ export interface IUIConfig { // before the play count is incremented minimumPlayPercent?: number; + // maximum number of items to shown in the dropdown list - defaults to 200 + // upper limit of 1000 + maxOptionsShown?: number; + + imageWallOptions?: ImageWallOptions; + lastNoteSeen?: number; } diff --git a/ui/v2.5/src/core/createClient.ts b/ui/v2.5/src/core/createClient.ts index f86de7990..2fcdaf717 100644 --- a/ui/v2.5/src/core/createClient.ts +++ b/ui/v2.5/src/core/createClient.ts @@ -6,7 +6,8 @@ import { ServerError, TypePolicies, } from "@apollo/client"; -import { WebSocketLink } from "@apollo/client/link/ws"; +import { GraphQLWsLink } from "@apollo/client/link/subscriptions"; +import { createClient as createWSClient } from "graphql-ws"; import { onError } from "@apollo/client/link/error"; import { getMainDefinition } from "@apollo/client/utilities"; import { createUploadLink } from "apollo-upload-client"; @@ -105,7 +106,11 @@ export const getPlatformURL = (ws?: boolean) => { } if (ws) { - platformUrl.protocol = "ws:"; + if (platformUrl.protocol === "https:") { + platformUrl.protocol = "wss:"; + } else { + platformUrl.protocol = "ws:"; + } } return platformUrl; @@ -115,30 +120,27 @@ export const createClient = () => { const platformUrl = getPlatformURL(); const wsPlatformUrl = getPlatformURL(true); - if (platformUrl.protocol === "https:") { - wsPlatformUrl.protocol = "wss:"; - } + const url = `${platformUrl}graphql`; + const wsUrl = `${wsPlatformUrl}graphql`; - const url = `${platformUrl.toString()}graphql`; - const wsUrl = `${wsPlatformUrl.toString()}graphql`; + const httpLink = createUploadLink({ uri: url }); - const httpLink = createUploadLink({ - uri: url, - }); - - const wsLink = new WebSocketLink({ - uri: wsUrl, - options: { - reconnect: true, - }, - }); + const wsLink = new GraphQLWsLink( + createWSClient({ + url: wsUrl, + retryAttempts: Infinity, + shouldRetry() { + return true; + }, + }) + ); const errorLink = onError(({ networkError }) => { // handle unauthorized error by redirecting to the login page if (networkError && (networkError as ServerError).statusCode === 401) { // redirect to login page const newURL = new URL( - `${window.STASH_BASE_URL}login`, + `${getBaseURL()}login`, window.location.toString() ); newURL.searchParams.append("returnURL", window.location.href); @@ -155,7 +157,6 @@ export const createClient = () => { ); }, wsLink, - // @ts-ignore httpLink ); diff --git a/ui/v2.5/src/core/files.ts b/ui/v2.5/src/core/files.ts index 78095bf65..1c2505840 100644 --- a/ui/v2.5/src/core/files.ts +++ b/ui/v2.5/src/core/files.ts @@ -1,4 +1,4 @@ -import { TextUtils } from "src/utils"; +import TextUtils from "src/utils/text"; import * as GQL from "src/core/generated-graphql"; interface IFile { diff --git a/ui/v2.5/src/core/galleries.ts b/ui/v2.5/src/core/galleries.ts index 21bf662cd..bedc2453e 100644 --- a/ui/v2.5/src/core/galleries.ts +++ b/ui/v2.5/src/core/galleries.ts @@ -1,4 +1,4 @@ -import { TextUtils } from "src/utils"; +import TextUtils from "src/utils/text"; import * as GQL from "src/core/generated-graphql"; interface IFile { diff --git a/ui/v2.5/src/core/performers.ts b/ui/v2.5/src/core/performers.ts index 44787b121..e13ac8885 100644 --- a/ui/v2.5/src/core/performers.ts +++ b/ui/v2.5/src/core/performers.ts @@ -15,25 +15,29 @@ export const usePerformerFilterHook = ( return c.criterionOption.type === "performers"; }) as PerformersCriterion; - if ( - performerCriterion && - (performerCriterion.modifier === GQL.CriterionModifier.IncludesAll || - performerCriterion.modifier === GQL.CriterionModifier.Includes) - ) { - // add the performer if not present + if (performerCriterion) { if ( - !performerCriterion.value.find((p) => { - return p.id === performer.id; - }) + performerCriterion.modifier === GQL.CriterionModifier.IncludesAll || + performerCriterion.modifier === GQL.CriterionModifier.Includes ) { - performerCriterion.value.push(performerValue); + // add the performer if not present + if ( + !performerCriterion.value.find((p) => { + return p.id === performer.id; + }) + ) { + performerCriterion.value.push(performerValue); + } + } else { + // overwrite + performerCriterion.value = [performerValue]; } performerCriterion.modifier = GQL.CriterionModifier.IncludesAll; } else { - // overwrite performerCriterion = new PerformersCriterion(); performerCriterion.value = [performerValue]; + performerCriterion.modifier = GQL.CriterionModifier.IncludesAll; filter.criteria.push(performerCriterion); } diff --git a/ui/v2.5/src/core/studios.ts b/ui/v2.5/src/core/studios.ts index cee81bcc4..ef93f191c 100644 --- a/ui/v2.5/src/core/studios.ts +++ b/ui/v2.5/src/core/studios.ts @@ -14,21 +14,11 @@ export const useStudioFilterHook = (studio: GQL.StudioDataFragment) => { return c.criterionOption.type === "studios"; }) as StudiosCriterion; - if ( - studioCriterion && - (studioCriterion.modifier === GQL.CriterionModifier.IncludesAll || - studioCriterion.modifier === GQL.CriterionModifier.Includes) - ) { + if (studioCriterion) { // we should be showing studio only. Remove other values - studioCriterion.value.items = studioCriterion.value.items.filter( - (v) => v.id === studio.id - ); - - if (studioCriterion.value.items.length === 0) { - studioCriterion.value.items.push(studioValue); - } + studioCriterion.value.items = [studioValue]; + studioCriterion.modifier = GQL.CriterionModifier.Includes; } else { - // overwrite studioCriterion = new StudiosCriterion(); studioCriterion.value = { items: [studioValue], @@ -36,6 +26,7 @@ export const useStudioFilterHook = (studio: GQL.StudioDataFragment) => { ? -1 : 0, }; + studioCriterion.modifier = GQL.CriterionModifier.Includes; filter.criteria.push(studioCriterion); } diff --git a/ui/v2.5/src/core/tags.ts b/ui/v2.5/src/core/tags.ts index e24f23fcb..3ec042c84 100644 --- a/ui/v2.5/src/core/tags.ts +++ b/ui/v2.5/src/core/tags.ts @@ -19,23 +19,26 @@ export const useTagFilterHook = (tag: GQL.TagDataFragment) => { return c.criterionOption.type === "tags"; }) as TagsCriterion; - if ( - tagCriterion && - (tagCriterion.modifier === GQL.CriterionModifier.IncludesAll || - tagCriterion.modifier === GQL.CriterionModifier.Includes) - ) { - // add the tag if not present + if (tagCriterion) { if ( - !tagCriterion.value.items.find((p) => { - return p.id === tag.id; - }) + tagCriterion.modifier === GQL.CriterionModifier.IncludesAll || + tagCriterion.modifier === GQL.CriterionModifier.Includes ) { - tagCriterion.value.items.push(tagValue); + // add the tag if not present + if ( + !tagCriterion.value.items.find((p) => { + return p.id === tag.id; + }) + ) { + tagCriterion.value.items.push(tagValue); + } + } else { + // overwrite + tagCriterion.value.items = [tagValue]; } tagCriterion.modifier = GQL.CriterionModifier.IncludesAll; } else { - // overwrite tagCriterion = new TagsCriterion(TagsCriterionOption); tagCriterion.value = { items: [tagValue], @@ -43,6 +46,7 @@ export const useTagFilterHook = (tag: GQL.TagDataFragment) => { ? -1 : 0, }; + tagCriterion.modifier = GQL.CriterionModifier.IncludesAll; filter.criteria.push(tagCriterion); } diff --git a/ui/v2.5/src/docs/en/Changelog/v0180.md b/ui/v2.5/src/docs/en/Changelog/v0180.md index 2f6ed4210..ccf42200a 100644 --- a/ui/v2.5/src/docs/en/Changelog/v0180.md +++ b/ui/v2.5/src/docs/en/Changelog/v0180.md @@ -1,4 +1,8 @@ +### 💥 Known issues +* Performer autotagging does not currently match on performer aliases. This will be addressed when finer control over the matching is implemented. + ### ✨ New Features +* Added disambiguation field to Performers, to differentiate between performers with the same name. ([#3113](https://github.com/stashapp/stash/pull/3113)) * Added ability to track play count and duration for scenes. ([#3055](https://github.com/stashapp/stash/pull/3055)) * Scenes now optionally show the last point watched, and can be resumed from that point. ([#3055](https://github.com/stashapp/stash/pull/3055)) * Added support for filtering stash ids by endpoint. ([#3005](https://github.com/stashapp/stash/pull/3005)) @@ -13,6 +17,7 @@ * Added tag description filter criterion. ([#3011](https://github.com/stashapp/stash/pull/3011)) ### 🎨 Improvements +* Changed performer aliases to be a list, rather than a string field. ([#3113](https://github.com/stashapp/stash/pull/3113)) * Jump back/forward buttons on mobile have been replaced with double-tap gestures on mobile. ([#3120](https://github.com/stashapp/stash/pull/3120)) * Added shift- and ctrl-keybinds for seeking for shorter and longer intervals, respectively. ([#3120](https://github.com/stashapp/stash/pull/3120)) * Limit number of items in selector drop-downs to 200. ([#3062](https://github.com/stashapp/stash/pull/3062)) diff --git a/ui/v2.5/src/docs/en/Changelog/v0190.md b/ui/v2.5/src/docs/en/Changelog/v0190.md new file mode 100644 index 000000000..9de63607b --- /dev/null +++ b/ui/v2.5/src/docs/en/Changelog/v0190.md @@ -0,0 +1,29 @@ +### 💥 Known issues +* Performer autotagging does not currently match on performer aliases. This will be addressed when finer control over the matching is implemented. + +### ✨ New Features +* Added support for specifying the use of a proxy for network requests. ([#3284](https://github.com/stashapp/stash/pull/3284)) +* Added support for injecting arguments into `ffmpeg` during generation and live-transcoding. ([#3216](https://github.com/stashapp/stash/pull/3216)) +* Added URL and Date fields to Images. ([#3015](https://github.com/stashapp/stash/pull/3015)) +* Added support for plugins to add injected CSS and Javascript to the UI. ([#3195](https://github.com/stashapp/stash/pull/3195)) +* Added disambiguation field to Performers, to differentiate between performers with the same name. ([#3113](https://github.com/stashapp/stash/pull/3113)) +* Added Anonymise task to generate an anonymised version of the database. ([#3186](https://github.com/stashapp/stash/pull/3186)) + +### 🎨 Improvements +* Added `r x x` keyboard shortcuts to set decimal ratings. ([#3226](https://github.com/stashapp/stash/pull/3226)) +* Changed performer aliases to be a list, rather than a string field. ([#3113](https://github.com/stashapp/stash/pull/3113)) + +### 🐛 Bug fixes +* **[0.19.1]** Fixed performance issues with Scene Tagger view. ([#3444](https://github.com/stashapp/stash/pull/3444), [#3452](https://github.com/stashapp/stash/pull/3452)) +* **[0.19.1]** Fixed panic when batch adding performers from the Tagger view. ([#3456](https://github.com/stashapp/stash/pull/3456)) +* Fixed folder symlinks not being handled correctly during clean. ([#3415](https://github.com/stashapp/stash/pull/3415)) +* Fixed error when clicking Scrape All when a file-less scene is in the scene list. ([#3414](https://github.com/stashapp/stash/pull/3414)) +* Fixed clicking popover pills not clearing search term. ([#3408](https://github.com/stashapp/stash/pull/3408)) +* Fixed URL not being preserved when redirected to login. ([#3305](https://github.com/stashapp/stash/pull/3305)) +* Fixed scene previews not being overwritten when Overwrite option is selected. ([#3256](https://github.com/stashapp/stash/pull/3256)) +* Fixed objects without titles not being sorted correctly with objects with titles. ([#3244](https://github.com/stashapp/stash/pull/3244)) +* Fixed incorrect new Performer pill being removed when creating Performer from scrape dialog. ([#3251](https://github.com/stashapp/stash/pull/3251)) +* Fixed date fields not being nulled correctly when cleared. ([#3243](https://github.com/stashapp/stash/pull/3243)) +* Fixed scene wall items to show file base name where scene has no title set. ([#3242](https://github.com/stashapp/stash/pull/3242)) +* Fixed image exclusion pattern being applied to all files. ([#3241](https://github.com/stashapp/stash/pull/3241)) +* Fixed missing captions not being removed during scan. ([#3240](https://github.com/stashapp/stash/pull/3240)) \ No newline at end of file diff --git a/ui/v2.5/src/docs/en/Changelog/v0200.md b/ui/v2.5/src/docs/en/Changelog/v0200.md new file mode 100644 index 000000000..e3174fe52 --- /dev/null +++ b/ui/v2.5/src/docs/en/Changelog/v0200.md @@ -0,0 +1,56 @@ +##### 💥 Note: The cache directory is now required if using HLS/DASH streaming. Please set the cache directory in the System Settings page. + +##### 💥 Note: The image data subsystem has been reworked in this release. Existing systems will have their storage system set to `Database`, which stores all image data in the database. This can be changed in the System Settings page. + +A migration is required to change the storage system, and can be accessed from the Tasks page. + +The `Database` storage system is not recommended for large libraries, as it can cause performance issues. The `Filesystem` storage system is recommended for large libraries, and is the default for new systems. + +##### 💥 Note: the `generated/screenshots` jpg files are now considered legacy. These files can be migrated into the blob storage system by running the `Migrate Screenshots` task from the Tasks page. + +Once migrated, these files can be deleted. The files can be optionally deleted during the migration. + +### ✨ New Features +* Added `Is Missing Cover` scene filter criterion. ([#3187](https://github.com/stashapp/stash/pull/3187)) +* Added Chapters to Galleries. ([#3289](https://github.com/stashapp/stash/pull/3289)) +* Added button to tagger scene cards to view scene sprite. ([#3536](https://github.com/stashapp/stash/pull/3536)) +* Added hardware acceleration support (for a limited number of encoders) for transcoding. ([#3419](https://github.com/stashapp/stash/pull/3419)) +* Added support for DASH streaming. ([#3275](https://github.com/stashapp/stash/pull/3275)) +* Added configuration option for the maximum number of items in selector drop-downs. ([#3277](https://github.com/stashapp/stash/pull/3277)) +* Added configuration option to perform generation operations sequentially after scanning a new video file. ([#3378](https://github.com/stashapp/stash/pull/3378)) +* Optionally show range in generated funscript heatmaps. ([#3373](https://github.com/stashapp/stash/pull/3373)) +* Show funscript heatmaps in scene player scrubber. ([#3374](https://github.com/stashapp/stash/pull/3374)) +* Support customising the filename regex used for determining the gallery cover image. ([#3391](https://github.com/stashapp/stash/pull/3391)) +* Added tenth-place rating precision option. ([#3343](https://github.com/stashapp/stash/pull/3343)) +* Added toggleable favorite button to Performer cards. ([#3369](https://github.com/stashapp/stash/pull/3369)) + +### 🎨 Improvements +* Added date/time pickers for date and timestamp fields. ([#3572](https://github.com/stashapp/stash/pull/3572)) +* Added folder browser to path filter UI. ([#3570](https://github.com/stashapp/stash/pull/3570)) +* Include Organized flag in merge dialog. ([#3565](https://github.com/stashapp/stash/pull/3565)) +* Scene cover generation is now optional during scanning, and can be generated using the Generate task. ([#3187](https://github.com/stashapp/stash/pull/3187)) +* Overhauled the image blob storage system and added filesystem-based blob storage. ([#3187](https://github.com/stashapp/stash/pull/3187)) +* Overhauled filtering interface to allow setting filter criteria from a single dialog. ([#3515](https://github.com/stashapp/stash/pull/3515)) +* Removed upper limit on page size. ([#3544](https://github.com/stashapp/stash/pull/3544)) +* Anonymise task now obfuscates Marker titles. ([#3542](https://github.com/stashapp/stash/pull/3542)) +* Improved Images wall view layout and added Interface settings to adjust the layout. ([#3511](https://github.com/stashapp/stash/pull/3511)) +* Added collapsible divider to Gallery, Performer, Studio and Tag pages. ([#3508](https://github.com/stashapp/stash/pull/3508), [#3514](https://github.com/stashapp/stash/pull/3514)) +* Overhauled and improved HLS streaming. ([#3274](https://github.com/stashapp/stash/pull/3274)) + +### 🐛 Bug fixes +* **[0.20.2]** Fixed empty strings being preferred in scrape dialog. ([#3647](https://github.com/stashapp/stash/pull/3647)) +* **[0.20.2]** Fixed scene covers being regenerated when video file was moved. ([#3646](https://github.com/stashapp/stash/pull/3646)) +* **[0.20.1]** Fixed null values being preferred in scrape dialog. ([#3621](https://github.com/stashapp/stash/pull/3621)) +* Fixed login screen not working correctly from the logout screen. ([#3555](https://github.com/stashapp/stash/pull/3555)) +* Fixed incorrect stash ID being overwritten when updating performer with multiple stash-box endpoints. ([#3543](https://github.com/stashapp/stash/pull/3543) +* Fixed batch performer update overwriting incorrect stash IDs when multiple endpoints are configured. ([#3548](https://github.com/stashapp/stash/pull/3548)) +* Fixed `/stream` endpoint serving directory list. ([#3541](https://github.com/stashapp/stash/pull/3541)) +* Fixed error when querying with a large or unlimited page size. ([#3544](https://github.com/stashapp/stash/pull/3544)) +* Fixed sprites not being displayed for scenes with numeric-only hashes. ([#3513](https://github.com/stashapp/stash/pull/3513)) +* Fixed Save button being disabled when stting Tag image. ([#3509](https://github.com/stashapp/stash/pull/3509)) +* Fixed incorrect performer with identical name being matched when scraping from stash-box. ([#3488](https://github.com/stashapp/stash/pull/3488)) +* Fixed scene cover not being included when submitting file-less scenes to stash-box. ([#3465](https://github.com/stashapp/stash/pull/3465)) +* Fixed URL not being during stash-box scrape if the Studio URL is not set. ([#3439](https://github.com/stashapp/stash/pull/3439)) +* Fixed generating previews for variable frame rate videos. ([#3376](https://github.com/stashapp/stash/pull/3376)) +* Fixed errors reading zip files with non-UTF8 encoding. ([#3389](https://github.com/stashapp/stash/pull/3389)) +* Fixed plugins not able to access API during zip scan operations on systems with authentication enabled. ([#3433](https://github.com/stashapp/stash/pull/3433)) diff --git a/ui/v2.5/src/docs/en/Manual/Configuration.md b/ui/v2.5/src/docs/en/Manual/Configuration.md index 2d993cf9c..99b00f219 100644 --- a/ui/v2.5/src/docs/en/Manual/Configuration.md +++ b/ui/v2.5/src/docs/en/Manual/Configuration.md @@ -77,6 +77,22 @@ This setting can be used to increase/decrease overall CPU utilisation in two sce Note: If this is set too high it will decrease overall performance and causes failures (out of memory). +## Hardware Accelerated Live Transcoding + +Hardware accelerated live transcoding can be enabled by setting the `FFmpeg hardware encoding` setting. Stash outputs the supported hardware encoders to the log file on startup at the Info log level. If a given hardware encoder is not supported, it's error message is logged to the Debug log level for debugging purposes. + +## HLS/DASH Streaming + +To stream using HLS (such as on Apple devices) or DASH, the Cache path must be set. This directory is used to store temporary files during the live-transcoding process. The Cache path can be set in the System settings page. + +## ffmpeg arguments + +Additional arguments can be injected into ffmpeg when generating previews and sprites, and when live-transcoding videos. + +The ffmpeg arguments configuration is split into `Input` and `Output` arguments. Input arguments are injected before the input file argument, and output arguments are injected before the output file argument. + +Arguments are accepted as a list of strings. Each string is a separate argument. For example, a single argument of `-foo bar` would be treated as a single argument `"-foo bar"`. The correct way to pass this argument would be to split it into two separate arguments: `"-foo", "bar"`. + ## Scraping ### User Agent string @@ -121,6 +137,10 @@ These options are typically not exposed in the UI and must be changed manually i | `custom_ui_location` | The file system folder where the UI files will be served from, instead of using the embedded UI. Empty to disable. Stash must be restarted to take effect. | | `max_upload_size` | Maximum file upload size for import files. Defaults to 1GB. | | `theme_color` | Sets the `theme-color` property in the UI. | +| `gallery_cover_regex` | The regex responsible for selecting images as gallery covers | +| `proxy` | The url of a HTTP(S) proxy to be used when stash makes calls to online services Example: https://user:password@my.proxy:8080 | +| `no_proxy` | A list of domains for which the proxy must not be used. Default is all local LAN: localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12 | +| `sequential_scanning` | Modifies behaviour of the scanning functionality to generate support files (previews/sprites/phash) at the same time as fingerprinting/screenshotting. Useful when scanning cached remote files. | ### Custom served folders diff --git a/ui/v2.5/src/docs/en/Manual/EmbeddedPlugins.md b/ui/v2.5/src/docs/en/Manual/EmbeddedPlugins.md index 9b69f140c..cb0f3b1b8 100644 --- a/ui/v2.5/src/docs/en/Manual/EmbeddedPlugins.md +++ b/ui/v2.5/src/docs/en/Manual/EmbeddedPlugins.md @@ -1,20 +1,20 @@ -# Embedded Plugins +# Embedded Plugin Tasks -Embedded plugins are executed within the stash process using a scripting system. +Embedded plugin tasks are executed within the stash process using a scripting system. ## Supported script languages -Stash currently supports Javascript embedded plugins using [otto](https://github.com/robertkrimen/otto). +Stash currently supports Javascript embedded plugin tasks using [otto](https://github.com/robertkrimen/otto). # Javascript plugins ## Plugin input -The input is provided to Javascript plugins 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 that the `server_connection` field should not be necessary in most embedded plugins. ## Plugin output -The output of a Javascript plugin is derived from the evaluated value of the script. The output should conform to the structure provided in the `Plugin output` section of the [Plugins](/help/Plugins.md) page. +The output of a Javascript plugin task is derived from the evaluated value of the script. The output should conform to the structure provided in the `Plugin output` section of the [Plugins](/help/Plugins.md) page. There are a number of ways to return the plugin output: @@ -53,22 +53,6 @@ See the `Javascript API` section below on how to log with Javascript plugins. # Plugin configuration file format -The basic structure of an embedded plugin configuration file is as follows: - -``` -name: -description: -version: -url: -exec: - - -interface: [interface type] -tasks: - - ... -``` - -The `name`, `description`, `version` and `url` fields are displayed on the plugins page. - ## exec For embedded plugins, the `exec` field is a list with the first element being the path to the Javascript file that will be executed. It is expected that the path to the Javascript file is relative to the directory of the plugin configuration file. diff --git a/ui/v2.5/src/docs/en/Manual/ExternalPlugins.md b/ui/v2.5/src/docs/en/Manual/ExternalPlugins.md index aabe3ac7a..872cc31ec 100644 --- a/ui/v2.5/src/docs/en/Manual/ExternalPlugins.md +++ b/ui/v2.5/src/docs/en/Manual/ExternalPlugins.md @@ -1,10 +1,10 @@ -# External Plugins +# External Plugin Tasks -External plugins are executed by running an external binary. +External plugin tasks are executed by running an external binary. ## Plugin interfaces -Stash communicates with external plugins using an interface. Stash currently supports RPC and raw interface types. +Stash communicates with external plugin tasks using an interface. Stash currently supports RPC and raw interface types. ### RPC interface @@ -30,27 +30,9 @@ Plugins can log for specific levels or log progress by prefixing the output stri # Plugin configuration file format -The basic structure of an external plugin configuration file is as follows: - -``` -name: -description: -version: -url: -exec: - - - - -interface: [interface type] -errLog: [one of none trace, debug, info, warning, error] -tasks: - - ... -``` - -The `name`, `description`, `version` and `url` fields are displayed on the plugins page. - ## exec -For external plugins, the `exec` field is a list with the first element being the binary that will be executed, and the subsequent elements are the arguments passed. The execution process will search the path for the binary, then will attempt to find the program in the same directory as the plugin configuration file. The `exe` extension is not necessary on Windows systems. +For external plugin tasks, the `exec` field is a list with the first element being the binary that will be executed, and the subsequent elements are the arguments passed. The execution process will search the path for the binary, then will attempt to find the program in the same directory as the plugin configuration file. The `exe` extension is not necessary on Windows systems. > **⚠️ Note:** The plugin execution process sets the current working directory to that of the stash process. @@ -75,7 +57,7 @@ exec: ## interface -For external plugins, the `interface` field must be set to one of the following values: +For external plugin tasks, the `interface` field must be set to one of the following values: * `rpc` * `raw` @@ -89,7 +71,7 @@ The `errLog` field tells stash what the default log level should be when the plu # Task configuration -In addition to the standard task configuration, external tags may be configured with an optional `execArgs` field to add extra parameters to the execution arguments for the task. +In addition to the standard task configuration, external tasks may be configured with an optional `execArgs` field to add extra parameters to the execution arguments for the task. For example: diff --git a/ui/v2.5/src/docs/en/Manual/Galleries.md b/ui/v2.5/src/docs/en/Manual/Galleries.md index e7d9120c6..c31e2b1c4 100644 --- a/ui/v2.5/src/docs/en/Manual/Galleries.md +++ b/ui/v2.5/src/docs/en/Manual/Galleries.md @@ -6,7 +6,7 @@ Galleries are automatically created from zip files found during scanning that co 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. -If an 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. +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. Images can be added to a gallery by navigating to the gallery's page, selecting the "Add" tab, querying for and selecting the images to add, then selecting "Add to Gallery" from the `...` menu button. Likewise, images may be removed from a gallery by selecting the "Images" tab, selecting the images to remove and selecting "Remove from Gallery" from the `...` menu button. diff --git a/ui/v2.5/src/docs/en/Manual/Help.md b/ui/v2.5/src/docs/en/Manual/Help.md index 6d52920af..f5d1d69e5 100644 --- a/ui/v2.5/src/docs/en/Manual/Help.md +++ b/ui/v2.5/src/docs/en/Manual/Help.md @@ -2,6 +2,6 @@ Join our [Discord](https://discord.gg/2TsNFKt). -The [Github wiki](https://github.com/stashapp/stash/wiki) covers some areas not covered in the in-app help. +The [Stash-Docs](https://docs.stashapp.cc) covers some areas not covered in the in-app help. -Raise a [github issue](https://github.com/stashapp/stash/issues). +Raise a [GitHub issue](https://github.com/stashapp/stash/issues). diff --git a/ui/v2.5/src/docs/en/Manual/Interface.md b/ui/v2.5/src/docs/en/Manual/Interface.md index c948dd438..4803b4eaf 100644 --- a/ui/v2.5/src/docs/en/Manual/Interface.md +++ b/ui/v2.5/src/docs/en/Manual/Interface.md @@ -30,9 +30,9 @@ By default, when a scene has a resume point, the scene player will automatically ## Custom CSS -The stash UI can be customised using custom CSS. See [here](https://github.com/stashapp/stash/wiki/Custom-CSS-snippets) for a community-curated set of CSS snippets to customise your UI. +The stash UI can be customised using custom CSS. See [here](https://docs.stashapp.cc/user-interface-ui/custom-css-snippets) for a community-curated set of CSS snippets to customise your UI. -[Stash Plex Theme](https://github.com/stashapp/stash/wiki/Theme-Plex) is a community created theme inspired by the popular Plex interface. +[Stash Plex Theme](https://docs.stashapp.cc/user-interface-ui/themes/plex) is a community created theme inspired by the popular Plex interface. ## Custom Javascript diff --git a/ui/v2.5/src/docs/en/Manual/JSONSpec.md b/ui/v2.5/src/docs/en/Manual/JSONSpec.md index 0749f7170..ae0e76e0a 100644 --- a/ui/v2.5/src/docs/en/Manual/JSONSpec.md +++ b/ui/v2.5/src/docs/en/Manual/JSONSpec.md @@ -218,7 +218,7 @@ For those preferring the json-format, defined [here](https://json-schema.org/), ``` json { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://github.com/stashapp/stash/wiki/JSON-Specification/performer.json", + "$id": "https://docs.stashapp.cc/in-app-manual/tasks/jsonspec#performerjson", "title": "performer", "description": "A json file representing a performer. The file is named by a MD5 Code.", "type": "object", @@ -318,7 +318,7 @@ For those preferring the json-format, defined [here](https://json-schema.org/), ``` json { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://github.com/stashapp/stash/wiki/JSON-Specification/studio.json", + "$id": "https://docs.stashapp.cc/in-app-manual/tasks/jsonspec#studiojson", "title": "studio", "description": "A json file representing a studio. The file is named by a MD5 Code.", "type": "object", @@ -357,7 +357,7 @@ For those preferring the json-format, defined [here](https://json-schema.org/), ```json { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://github.com/stashapp/stash/wiki/JSON-Specification/scene.json", + "$id": "https://docs.stashapp.cc/in-app-manual/tasks/jsonspec#scenejson", "title": "scene", "description": "A json file representing a scene. The file is named by the MD5 Code of the file its data is referring to.", "type": "object", diff --git a/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md b/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md index 29408438e..7eee67ef4 100644 --- a/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md +++ b/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md @@ -84,8 +84,10 @@ | Keyboard sequence | Action | |-------------------|--------| -| `r {1-5}` | Set rating | -| `r 0` | Unset rating | +| `r {1-5}` | Set rating (stars) | +| `r 0` | Unset rating (stars) | +| `r {0-9} {0-9}` | Set rating (decimal - `00` for `10.0`) | +| ``r ` `` | Unset rating (decimal) | | `s s` | Save Scene | | `d d` | Delete Scene | | `Ctrl + v` | Paste Scene cover | @@ -110,8 +112,10 @@ | `e` | Edit Movie | | `s s` | Save Movie | | `d d` | Delete Movie | -| `r {1-5}` | Set rating (in edit mode) | -| `r 0` | Unset rating (in edit mode) | +| `r {1-5}` | [Edit mode] Set rating (stars) | +| `r 0` | [Edit mode] Unset rating (stars) | +| `r {0-9} {0-9}` | [Edit mode] Set rating (decimal - `r 0 0` for `10.0`) | +| ``r ` `` | [Edit mode] Unset rating (decimal) | | `Ctrl + v` | Paste Movie image | [//]: # "Commented until implementation is dealt with" diff --git a/ui/v2.5/src/docs/en/Manual/Plugins.md b/ui/v2.5/src/docs/en/Manual/Plugins.md index fbb224fcb..e7c80ded1 100644 --- a/ui/v2.5/src/docs/en/Manual/Plugins.md +++ b/ui/v2.5/src/docs/en/Manual/Plugins.md @@ -1,8 +1,12 @@ # Plugins -Stash supports the running tasks via plugins. Plugins can be implemented using embedded Javascript, or by calling an external binary. +Stash supports plugins that can do the following: +- perform custom tasks when triggered by the user from the Tasks page +- perform custom tasks when triggered from specific events +- add custom CSS to the UI +- add custom JavaScript to the UI -Stash also supports triggering of plugin hooks from specific stash operations. +Plugin tasks can be implemented using embedded Javascript, or by calling an external binary. > **⚠️ Note:** Plugin support is still experimental and is likely to change. @@ -20,13 +24,45 @@ Plugins provide tasks which can be run from the Tasks page. # Creating plugins -See [External Plugins](/help/ExternalPlugins.md) for details for making external plugins. +## Plugin configuration file format -See [Embedded Plugins](/help/EmbeddedPlugins.md) for details for making embedded plugins. +The basic structure of a plugin configuration file is as follows: -## Plugin input +``` +name: +description: +version: +url: -Plugins may accept an input from the stash server. This input is encoded according to the interface, and has the following structure (presented here in JSON format): +ui: + # optional list of css files to include in the UI + css: + - + + # optional list of js files to include in the UI + javascript: + - + +# the following are used for plugin tasks only +exec: + - ... +interface: [interface type] +errLog: [one of none trace, debug, info, warning, error] +tasks: + - ... +``` + +The `name`, `description`, `version` and `url` fields are displayed on the plugins page. + +The `exec`, `interface`, `errLog` and `tasks` fields are used only for plugins with tasks. + +See [External Plugins](/help/ExternalPlugins.md) for details for making plugins with external tasks. + +See [Embedded Plugins](/help/EmbeddedPlugins.md) for details for making plugins with embedded tasks. + +## Plugin task input + +Plugin tasks may accept an input from the stash server. This input is encoded according to the interface, and has the following structure (presented here in JSON format): ``` { "server_connection": { @@ -57,9 +93,9 @@ Plugins may accept an input from the stash server. This input is encoded accordi The `server_connection` field contains all the information needed for a plugin to access the parent stash server, if necessary. -## Plugin output +## Plugin task output -Plugin output is expected in the following structure (presented here as JSON format): +Plugin task output is expected in the following structure (presented here as JSON format): ``` { diff --git a/ui/v2.5/src/docs/en/MigrationNotes/index.ts b/ui/v2.5/src/docs/en/MigrationNotes/index.ts index 407c49eeb..6611333cf 100644 --- a/ui/v2.5/src/docs/en/MigrationNotes/index.ts +++ b/ui/v2.5/src/docs/en/MigrationNotes/index.ts @@ -1,9 +1,7 @@ import migration32 from "./32.md"; import migration39 from "./39.md"; -type Module = typeof migration32; - -export const migrationNotes: Record = { +export const migrationNotes: Record = { 32: migration32, 39: migration39, }; diff --git a/ui/v2.5/src/docs/en/ReleaseNotes/index.ts b/ui/v2.5/src/docs/en/ReleaseNotes/index.ts index 8a1e5000e..9eb1a8cb9 100644 --- a/ui/v2.5/src/docs/en/ReleaseNotes/index.ts +++ b/ui/v2.5/src/docs/en/ReleaseNotes/index.ts @@ -1,16 +1,22 @@ import v0170 from "./v0170.md"; +import v0200 from "./v0200.md"; -export type Module = typeof v0170; - -interface IReleaseNotes { +export interface IReleaseNotes { // handle should be in the form of YYYYMMDD date: number; - content: Module; + version: string; + content: string; } export const releaseNotes: IReleaseNotes[] = [ + { + date: 20230301, + version: "v0.20.0", + content: v0200, + }, { date: 20220906, + version: "v0.17.0", content: v0170, }, ]; diff --git a/ui/v2.5/src/docs/en/ReleaseNotes/v0170.md b/ui/v2.5/src/docs/en/ReleaseNotes/v0170.md index 68dd42573..a9511e922 100644 --- a/ui/v2.5/src/docs/en/ReleaseNotes/v0170.md +++ b/ui/v2.5/src/docs/en/ReleaseNotes/v0170.md @@ -1,7 +1,7 @@ After migrating, please run a scan on your entire library to populate missing data, and to ingest identical files which were previously ignored. -### Other changes: +##### Other changes: * Import/export schema has changed and is incompatible with the previous version. * Changelog has been moved from the stats page to a section in the Settings page. * Object titles are now displayed as the file basename if the title is not explicitly set. The `Don't include file extension as part of the title` scan flag is no longer supported. -* `Set name, date, details from embedded file metadata` scan flag is no longer supported. This functionality may be implemented as a built-in scraper in the future. \ No newline at end of file +* `Set name, date, details from embedded file metadata` scan flag is no longer supported. This functionality may be implemented as a built-in scraper in the future. diff --git a/ui/v2.5/src/docs/en/ReleaseNotes/v0200.md b/ui/v2.5/src/docs/en/ReleaseNotes/v0200.md new file mode 100644 index 000000000..c14895101 --- /dev/null +++ b/ui/v2.5/src/docs/en/ReleaseNotes/v0200.md @@ -0,0 +1,7 @@ +The image data subsystem has been reworked in this release. Existing systems will have their storage system set to `Database`, which stores all image data in the database. This can be changed in the System Settings page. **Note:** a migration is required to change the storage system, and can be accessed from the Tasks page. + +The `Database` storage system is not recommended for large libraries, as it can cause performance issues. The `Filesystem` storage system is recommended for large libraries, and is the default for new systems. + +**Important:** the `generated/screenshots` jpg files are considered legacy. These files can be migrated into the blob storage system by running the `Migrate Screenshots` task from the Tasks page. Once migrated, these files can be deleted. The files can be optionally deleted during the migration. + +The cache directory is now required if using HLS/DASH streaming. Please set the cache directory in the System Settings page. diff --git a/ui/v2.5/src/globals.d.ts b/ui/v2.5/src/globals.d.ts index 1874dfb59..3bd1b67fe 100644 --- a/ui/v2.5/src/globals.d.ts +++ b/ui/v2.5/src/globals.d.ts @@ -1,23 +1,17 @@ // eslint-disable-next-line no-var declare var STASH_BASE_URL: string; -declare module "*.md"; -declare module "string.prototype.replaceall"; -declare module "mousetrap-pause"; -declare module "hamming-distance"; -declare module "@formatjs/intl-pluralrules/locale-data/en"; -declare module "@formatjs/intl-numberformat/locale-data/en"; -declare module "@formatjs/intl-numberformat/locale-data/en-GB"; +declare module "intersection-observer"; -/* eslint-disable @typescript-eslint/naming-convention */ -interface ImportMetaEnv extends Readonly> { +declare module "*.md" { + const src: string; + export default src; +} + +/* eslint-disable-next-line @typescript-eslint/naming-convention */ +interface ImportMetaEnv { readonly VITE_APP_GITHASH?: string; readonly VITE_APP_STASH_VERSION?: string; readonly VITE_APP_DATE?: string; readonly VITE_APP_PLATFORM_PORT?: string; readonly VITE_APP_HTTPS?: string; } - -interface ImportMeta { - readonly env: ImportMetaEnv; -} -/* eslint-enable @typescript-eslint/no-unused-vars */ diff --git a/ui/v2.5/src/hooks/Interval.ts b/ui/v2.5/src/hooks/Interval.ts index 838ea9764..87c4d528f 100644 --- a/ui/v2.5/src/hooks/Interval.ts +++ b/ui/v2.5/src/hooks/Interval.ts @@ -1,14 +1,15 @@ import { useEffect, useRef, useState } from "react"; -import noop from "lodash-es/noop"; const MIN_VALID_INTERVAL = 1000; +function noop() {} + const useInterval = ( callback: () => void, delay: number | null = 5000 ): (() => void)[] => { const savedCallback = useRef<() => void>(); - const savedIntervalId = useRef(); + const savedIntervalId = useRef(); const [savedDelay, setSavedDelay] = useState(delay); useEffect(() => { diff --git a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx index a16a765cc..ca79805d7 100644 --- a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx +++ b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx @@ -7,13 +7,16 @@ import { Popover, Form, Row, + Dropdown, } from "react-bootstrap"; import cx from "classnames"; import Mousetrap from "mousetrap"; -import debounce from "lodash-es/debounce"; -import { Icon, LoadingIndicator } from "src/components/Shared"; -import { useInterval, usePageVisibility, useToast } from "src/hooks"; +import { Icon } from "src/components/Shared/Icon"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import useInterval from "../Interval"; +import usePageVisibility from "../PageVisibility"; +import { useToast } from "../Toast"; import { FormattedMessage, useIntl } from "react-intl"; import { LightboxImage } from "./LightboxImage"; import { ConfigurationContext } from "../Config"; @@ -28,7 +31,7 @@ import { import * as GQL from "src/core/generated-graphql"; import { useInterfaceLocalForage } from "../LocalForage"; import { imageLightboxDisplayModeIntlMap } from "src/core/enums"; -import { ILightboxImage } from "./types"; +import { ILightboxImage, IChapter } from "./types"; import { faArrowLeft, faArrowRight, @@ -40,12 +43,16 @@ import { faPlay, faSearchMinus, faTimes, + faBars, } from "@fortawesome/free-solid-svg-icons"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; +import { useDebounce } from "../debounce"; const CLASSNAME = "Lightbox"; const CLASSNAME_HEADER = `${CLASSNAME}-header`; const CLASSNAME_LEFT_SPACER = `${CLASSNAME_HEADER}-left-spacer`; +const CLASSNAME_CHAPTERS = `${CLASSNAME_HEADER}-chapters`; +const CLASSNAME_CHAPTER_BUTTON = `${CLASSNAME_HEADER}-chapter-button`; const CLASSNAME_INDICATOR = `${CLASSNAME_HEADER}-indicator`; const CLASSNAME_OPTIONS = `${CLASSNAME_HEADER}-options`; const CLASSNAME_OPTIONS_ICON = `${CLASSNAME_OPTIONS}-icon`; @@ -73,8 +80,11 @@ interface IProps { initialIndex?: number; showNavigation: boolean; slideshowEnabled?: boolean; - pageHeader?: string; - pageCallback?: (direction: number) => void; + page?: number; + pages?: number; + pageSize?: number; + pageCallback?: (props: { direction?: number; page?: number }) => void; + chapters?: IChapter[]; hide: () => void; } @@ -85,12 +95,16 @@ export const LightboxComponent: React.FC = ({ initialIndex = 0, showNavigation, slideshowEnabled = false, - pageHeader, + page, + pages, + pageSize: pageSize = 40, pageCallback, + chapters = [], hide, }) => { const [updateImage] = useImageUpdate(); + // zero-based const [index, setIndex] = useState(null); const [movingLeft, setMovingLeft] = useState(false); const oldIndex = useRef(null); @@ -98,6 +112,7 @@ export const LightboxComponent: React.FC = ({ const [isSwitchingPage, setIsSwitchingPage] = useState(true); const [isFullscreen, setFullscreen] = useState(false); const [showOptions, setShowOptions] = useState(false); + const [showChapters, setShowChapters] = useState(false); const [imagesLoaded, setImagesLoaded] = useState(0); const [navOffset, setNavOffset] = useState(); @@ -119,10 +134,8 @@ export const LightboxComponent: React.FC = ({ const Toast = useToast(); const intl = useIntl(); const { configuration: config } = React.useContext(ConfigurationContext); - const [ - interfaceLocalForage, - setInterfaceLocalForage, - ] = useInterfaceLocalForage(); + const [interfaceLocalForage, setInterfaceLocalForage] = + useInterfaceLocalForage(); const lightboxSettings = interfaceLocalForage.data?.imageLightbox; @@ -186,10 +199,8 @@ export const LightboxComponent: React.FC = ({ null ); - const [ - displayedSlideshowInterval, - setDisplayedSlideshowInterval, - ] = useState((slideshowDelay / SECONDS_TO_MS).toString()); + const [displayedSlideshowInterval, setDisplayedSlideshowInterval] = + useState((slideshowDelay / SECONDS_TO_MS).toString()); useEffect(() => { if (images !== oldImages.current && isSwitchingPage) { @@ -198,8 +209,9 @@ export const LightboxComponent: React.FC = ({ } }, [isSwitchingPage, images, index]); - const disableInstantTransition = debounce( + const disableInstantTransition = useDebounce( () => setInstantTransition(false), + [], 400 ); @@ -310,12 +322,13 @@ export const LightboxComponent: React.FC = ({ (isUserAction = true) => { if (isSwitchingPage || index === -1) return; + setShowChapters(false); setMovingLeft(true); if (index === 0) { // go to next page, or loop back if no callback is set if (pageCallback) { - pageCallback(-1); + pageCallback({ direction: -1 }); setIndex(-1); oldImages.current = images; setIsSwitchingPage(true); @@ -334,11 +347,12 @@ export const LightboxComponent: React.FC = ({ if (isSwitchingPage) return; setMovingLeft(false); + setShowChapters(false); if (index === images.length - 1) { // go to preview page, or loop back if no callback is set if (pageCallback) { - pageCallback(1); + pageCallback({ direction: 1 }); oldImages.current = images; setIsSwitchingPage(true); setIndex(0); @@ -449,6 +463,65 @@ export const LightboxComponent: React.FC = ({ const currentIndex = index === null ? initialIndex : index; + function gotoPage(imageIndex: number) { + const indexInPage = (imageIndex - 1) % pageSize; + if (pageCallback) { + let jumppage = Math.floor(imageIndex / pageSize) + 1; + if (page !== jumppage) { + pageCallback({ page: jumppage }); + oldImages.current = images; + setIsSwitchingPage(true); + } + } + + setIndex(indexInPage); + setShowChapters(false); + } + + function chapterHeader() { + const imageNumber = (index ?? 0) + 1; + const globalIndex = page + ? (page - 1) * pageSize + imageNumber + : imageNumber; + + let chapterTitle = ""; + chapters.forEach(function (chapter) { + if (chapter.image_index > globalIndex) { + return; + } + chapterTitle = chapter.title; + }); + + return chapterTitle ?? ""; + } + + const renderChapterMenu = () => { + if (chapters.length <= 0) return; + + const popoverContent = chapters.map(({ id, title, image_index }) => ( + gotoPage(image_index)}> + {" "} + {title} + {title.length > 0 ? " - #" : "#"} + {image_index} + + )); + + return ( + setShowChapters(!showChapters)} + > + + + + + {popoverContent} + + + ); + }; + // #2451: making OptionsForm an inline component means it // get re-rendered each time. This makes the text // field lose focus on input. Use function instead. @@ -634,6 +707,14 @@ export const LightboxComponent: React.FC = ({ } } + const pageHeader = + page && pages + ? intl.formatMessage( + { id: "dialogs.lightbox.page_header" }, + { page, total: pages } + ) + : ""; + return (
    = ({ onClick={handleClose} >
    -
    +
    {renderChapterMenu()}
    - {pageHeader} + + {chapterHeader()} {pageHeader} + {images.length > 1 ? ( {`${currentIndex + 1} / ${images.length}`} ) : undefined} diff --git a/ui/v2.5/src/hooks/Lightbox/context.tsx b/ui/v2.5/src/hooks/Lightbox/context.tsx index 2f857d5fb..b44a1127d 100644 --- a/ui/v2.5/src/hooks/Lightbox/context.tsx +++ b/ui/v2.5/src/hooks/Lightbox/context.tsx @@ -1,7 +1,8 @@ -import React, { lazy, Suspense, useCallback, useState } from "react"; -import { ILightboxImage } from "./types"; +import React, { Suspense, useCallback, useState } from "react"; +import { lazyComponent } from "src/utils/lazyComponent"; +import { ILightboxImage, IChapter } from "./types"; -const LightboxComponent = lazy(() => import("./Lightbox")); +const LightboxComponent = lazyComponent(() => import("./Lightbox")); export interface IState { images: ILightboxImage[]; @@ -9,8 +10,11 @@ export interface IState { isLoading: boolean; showNavigation: boolean; initialIndex?: number; - pageCallback?: (direction: number) => void; - pageHeader?: string; + pageCallback?: (props: { direction?: number; page?: number }) => void; + chapters?: IChapter[]; + page?: number; + pages?: number; + pageSize?: number; slideshowEnabled: boolean; onClose?: () => void; } @@ -21,7 +25,8 @@ interface IContext { export const LightboxContext = React.createContext({ setLightboxState: () => {}, }); -const Lightbox: React.FC = ({ children }) => { + +export const LightboxProvider: React.FC = ({ children }) => { const [lightboxState, setLightboxState] = useState({ images: [], isVisible: false, @@ -58,5 +63,3 @@ const Lightbox: React.FC = ({ children }) => { ); }; - -export default Lightbox; diff --git a/ui/v2.5/src/hooks/Lightbox/hooks.ts b/ui/v2.5/src/hooks/Lightbox/hooks.ts index dffa0fb5c..8ec511f52 100644 --- a/ui/v2.5/src/hooks/Lightbox/hooks.ts +++ b/ui/v2.5/src/hooks/Lightbox/hooks.ts @@ -1,8 +1,12 @@ import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import * as GQL from "src/core/generated-graphql"; import { LightboxContext, IState } from "./context"; +import { IChapter } from "./types"; -export const useLightbox = (state: Partial>) => { +export const useLightbox = ( + state: Partial>, + chapters: IChapter[] = [] +) => { const { setLightboxState } = useContext(LightboxContext); useEffect(() => { @@ -10,7 +14,9 @@ export const useLightbox = (state: Partial>) => { images: state.images, showNavigation: state.showNavigation, pageCallback: state.pageCallback, - pageHeader: state.pageHeader, + page: state.page, + pages: state.pages, + pageSize: state.pageSize, slideshowEnabled: state.slideshowEnabled, onClose: state.onClose, }); @@ -19,7 +25,9 @@ export const useLightbox = (state: Partial>) => { state.images, state.showNavigation, state.pageCallback, - state.pageHeader, + state.page, + state.pages, + state.pageSize, state.slideshowEnabled, state.onClose, ]); @@ -30,14 +38,18 @@ export const useLightbox = (state: Partial>) => { initialIndex: index, isVisible: true, slideshowEnabled, + page: state.page, + pages: state.pages, + pageSize: state.pageSize, + chapters: chapters, }); }, - [setLightboxState] + [setLightboxState, state.page, state.pages, state.pageSize, chapters] ); return show; }; -export const useGalleryLightbox = (id: string) => { +export const useGalleryLightbox = (id: string, chapters: IChapter[] = []) => { const { setLightboxState } = useContext(LightboxContext); const pageSize = 40; @@ -69,20 +81,26 @@ export const useGalleryLightbox = (id: string) => { }, [data?.findImages.count]); const handleLightBoxPage = useCallback( - (direction: number) => { - if (direction === -1) { - if (page === 1) { - setPage(pages); - } else { - setPage(page - 1); - } - } else if (direction === 1) { - if (page === pages) { - // return to the first page - setPage(1); - } else { - setPage(page + 1); + (props: { direction?: number; page?: number }) => { + const { direction, page: newPage } = props; + + if (direction !== undefined) { + if (direction < 0) { + if (page === 1) { + setPage(pages); + } else { + setPage(page + direction); + } + } else if (direction > 0) { + if (page === pages) { + // return to the first page + setPage(1); + } else { + setPage(page + direction); + } } + } else if (newPage !== undefined) { + setPage(newPage); } }, [page, pages] @@ -95,25 +113,39 @@ export const useGalleryLightbox = (id: string) => { isVisible: true, images: data.findImages?.images ?? [], pageCallback: pages > 1 ? handleLightBoxPage : undefined, - pageHeader: `Page ${page} / ${pages}`, + page, + pages, }); }, [setLightboxState, data, handleLightBoxPage, page, pages]); - const show = () => { + const show = (index: number = 0) => { + if (index > pageSize) { + setPage(Math.floor(index / pageSize) + 1); + index = index % pageSize; + } else { + setPage(1); + } if (data) setLightboxState({ isLoading: false, isVisible: true, + initialIndex: index, images: data.findImages?.images ?? [], pageCallback: pages > 1 ? handleLightBoxPage : undefined, - pageHeader: `Page ${page} / ${pages}`, + page, + pages, + pageSize, + chapters: chapters, }); else { setLightboxState({ isLoading: true, isVisible: true, + initialIndex: index, pageCallback: undefined, - pageHeader: undefined, + page: undefined, + pageSize, + chapters: chapters, }); fetchGallery(); } diff --git a/ui/v2.5/src/hooks/Lightbox/index.ts b/ui/v2.5/src/hooks/Lightbox/index.ts deleted file mode 100644 index 493207ae7..000000000 --- a/ui/v2.5/src/hooks/Lightbox/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./context"; -export * from "./hooks"; diff --git a/ui/v2.5/src/hooks/Lightbox/lightbox.scss b/ui/v2.5/src/hooks/Lightbox/lightbox.scss index 5a91b17e1..a4c82d639 100644 --- a/ui/v2.5/src/hooks/Lightbox/lightbox.scss +++ b/ui/v2.5/src/hooks/Lightbox/lightbox.scss @@ -30,10 +30,11 @@ display: flex; flex: 1; justify-content: center; + } - @media (max-width: 575px) { - display: none; - } + &-chapters { + max-height: 90%; + overflow: auto; } &-indicator { diff --git a/ui/v2.5/src/hooks/Lightbox/types.ts b/ui/v2.5/src/hooks/Lightbox/types.ts index ce630c812..6b60422fd 100644 --- a/ui/v2.5/src/hooks/Lightbox/types.ts +++ b/ui/v2.5/src/hooks/Lightbox/types.ts @@ -12,3 +12,9 @@ export interface ILightboxImage { o_counter?: GQL.Maybe; paths: IImagePaths; } + +export interface IChapter { + id: string; + title: string; + image_index: number; +} diff --git a/ui/v2.5/src/hooks/ListHook.tsx b/ui/v2.5/src/hooks/ListHook.tsx deleted file mode 100644 index 620950951..000000000 --- a/ui/v2.5/src/hooks/ListHook.tsx +++ /dev/null @@ -1,978 +0,0 @@ -import clone from "lodash-es/clone"; -import cloneDeep from "lodash-es/cloneDeep"; -import isEqual from "lodash-es/isEqual"; -import queryString from "query-string"; -import React, { - useCallback, - useRef, - useState, - useEffect, - useMemo, - useContext, -} from "react"; -import { ApolloError } from "@apollo/client"; -import { useHistory, useLocation } from "react-router-dom"; -import Mousetrap from "mousetrap"; -import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; -import { - SlimSceneDataFragment, - SceneMarkerDataFragment, - SlimGalleryDataFragment, - StudioDataFragment, - PerformerDataFragment, - FindScenesQueryResult, - FindSceneMarkersQueryResult, - FindGalleriesQueryResult, - FindStudiosQueryResult, - FindPerformersQueryResult, - FindMoviesQueryResult, - MovieDataFragment, - FindTagsQueryResult, - TagDataFragment, - FindImagesQueryResult, - SlimImageDataFragment, - FilterMode, -} from "src/core/generated-graphql"; -import { useInterfaceLocalForage } from "src/hooks/LocalForage"; -import { LoadingIndicator } from "src/components/Shared"; -import { ListFilter } from "src/components/List/ListFilter"; -import { FilterTags } from "src/components/List/FilterTags"; -import { Pagination, PaginationIndex } from "src/components/List/Pagination"; -import { - useFindDefaultFilter, - useFindScenes, - useFindSceneMarkers, - useFindImages, - useFindMovies, - useFindStudios, - useFindGalleries, - useFindPerformers, - useFindTags, -} from "src/core/StashService"; -import { ListFilterModel } from "src/models/list-filter/filter"; -import { DisplayMode } from "src/models/list-filter/types"; -import { ListFilterOptions } from "src/models/list-filter/filter-options"; -import { getFilterOptions } from "src/models/list-filter/factory"; -import { ButtonToolbar } from "react-bootstrap"; -import { ListViewOptions } from "src/components/List/ListViewOptions"; -import { ListOperationButtons } from "src/components/List/ListOperationButtons"; -import { - Criterion, - CriterionValue, -} from "src/models/list-filter/criteria/criterion"; -import { AddFilterDialog } from "src/components/List/AddFilterDialog"; -import { TextUtils } from "src/utils"; -import { FormattedNumber } from "react-intl"; -import { ConfigurationContext } from "./Config"; - -const getSelectedData = ( - result: I[], - selectedIds: Set -) => { - // find the selected items from the ids - const selectedResults: I[] = []; - - selectedIds.forEach((id) => { - const item = result.find((s) => s.id === id); - - if (item) { - selectedResults.push(item); - } - }); - - return selectedResults; -}; - -interface IListHookData { - filter: ListFilterModel; - template: React.ReactElement; - onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; - onChangePage: (page: number) => void; -} - -export interface IListHookOperation { - text: string; - onClick: ( - result: T, - filter: ListFilterModel, - selectedIds: Set - ) => Promise; - isDisplayed?: ( - result: T, - filter: ListFilterModel, - selectedIds: Set - ) => boolean; - postRefetch?: boolean; - icon?: IconDefinition; - buttonVariant?: string; -} - -export enum PersistanceLevel { - // do not load default query or persist display mode - NONE, - // load default query, don't load or persist display mode - ALL, - // load and persist display mode only - VIEW, -} - -interface IListHookOptions { - persistState?: PersistanceLevel; - persistanceKey?: string; - defaultSort?: string; - filterHook?: (filter: ListFilterModel) => ListFilterModel; - filterDialog?: ( - criteria: Criterion[], - setCriteria: (v: Criterion[]) => void - ) => React.ReactNode; - zoomable?: boolean; - selectable?: boolean; - defaultZoomIndex?: number; - otherOperations?: IListHookOperation[]; - renderContent: ( - result: T, - filter: ListFilterModel, - selectedIds: Set, - onChangePage: (page: number) => void, - pageCount: number - ) => React.ReactNode; - renderEditDialog?: ( - selected: E[], - onClose: (applied: boolean) => void - ) => React.ReactNode; - renderDeleteDialog?: ( - selected: E[], - onClose: (confirmed: boolean) => void - ) => React.ReactNode; - addKeybinds?: ( - result: T, - filter: ListFilterModel, - selectedIds: Set - ) => () => void; -} - -interface IDataItem { - id: string; -} -interface IQueryResult { - error?: ApolloError; - loading: boolean; - refetch: () => void; -} - -interface IQuery { - filterMode: FilterMode; - useData: (filter?: ListFilterModel) => T; - getData: (data: T) => T2[]; - getCount: (data: T) => number; - getMetadataByline: (data: T) => React.ReactNode; -} - -interface IRenderListProps { - filter?: ListFilterModel; - filterOptions: ListFilterOptions; - onChangePage: (page: number) => void; - updateFilter: (filter: ListFilterModel) => void; -} - -const useRenderList = < - QueryResult extends IQueryResult, - QueryData extends IDataItem ->({ - filter, - filterOptions, - onChangePage, - addKeybinds, - useData, - getCount, - getData, - getMetadataByline, - otherOperations, - renderContent, - zoomable, - selectable, - renderEditDialog, - renderDeleteDialog, - updateFilter, - filterDialog, - persistState, -}: IListHookOptions & - IQuery & - IRenderListProps) => { - const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [selectedIds, setSelectedIds] = useState>(new Set()); - const [lastClickedId, setLastClickedId] = useState(); - - const [editingCriterion, setEditingCriterion] = useState< - Criterion - >(); - const [newCriterion, setNewCriterion] = useState(false); - - const result = useData(filter); - const totalCount = getCount(result); - const metadataByline = getMetadataByline(result); - const items = getData(result); - - // handle case where page is more than there are pages - useEffect(() => { - if (filter === undefined) return; - - const pages = Math.ceil(totalCount / filter.itemsPerPage); - if (pages > 0 && filter.currentPage > pages) { - onChangePage(pages); - } - }, [filter, onChangePage, totalCount]); - - // set up hotkeys - useEffect(() => { - if (filter === undefined) return; - - Mousetrap.bind("f", () => setNewCriterion(true)); - - return () => { - Mousetrap.unbind("f"); - }; - }, [filter]); - useEffect(() => { - if (filter === undefined) return; - - const pages = Math.ceil(totalCount / filter.itemsPerPage); - Mousetrap.bind("right", () => { - if (filter.currentPage < pages) { - onChangePage(filter.currentPage + 1); - } - }); - Mousetrap.bind("left", () => { - if (filter.currentPage > 1) { - onChangePage(filter.currentPage - 1); - } - }); - Mousetrap.bind("shift+right", () => { - onChangePage(Math.min(pages, filter.currentPage + 10)); - }); - Mousetrap.bind("shift+left", () => { - onChangePage(Math.max(1, filter.currentPage - 10)); - }); - Mousetrap.bind("ctrl+end", () => { - onChangePage(pages); - }); - Mousetrap.bind("ctrl+home", () => { - onChangePage(1); - }); - - return () => { - Mousetrap.unbind("right"); - Mousetrap.unbind("left"); - Mousetrap.unbind("shift+right"); - Mousetrap.unbind("shift+left"); - Mousetrap.unbind("ctrl+end"); - Mousetrap.unbind("ctrl+home"); - }; - }, [filter, onChangePage, totalCount]); - useEffect(() => { - if (filter === undefined) return; - - if (addKeybinds) { - const unbindExtras = addKeybinds(result, filter, selectedIds); - return () => { - unbindExtras(); - }; - } - }, [addKeybinds, filter, result, selectedIds]); - - // Don't continue if filter is undefined - // There are no hooks below this point so this is valid - if (filter === undefined) return; - - function singleSelect(id: string, selected: boolean) { - setLastClickedId(id); - - const newSelectedIds = clone(selectedIds); - if (selected) { - newSelectedIds.add(id); - } else { - newSelectedIds.delete(id); - } - - setSelectedIds(newSelectedIds); - } - - function selectRange(startIndex: number, endIndex: number) { - let start = startIndex; - let end = endIndex; - if (start > end) { - const tmp = start; - start = end; - end = tmp; - } - - const subset = items.slice(start, end + 1); - const newSelectedIds: Set = new Set(); - - subset.forEach((item) => { - newSelectedIds.add(item.id); - }); - - setSelectedIds(newSelectedIds); - } - - function multiSelect(id: string) { - let startIndex = 0; - let thisIndex = -1; - - if (lastClickedId) { - startIndex = items.findIndex((item) => { - return item.id === lastClickedId; - }); - } - - thisIndex = items.findIndex((item) => { - return item.id === id; - }); - - selectRange(startIndex, thisIndex); - } - - function onSelectChange(id: string, selected: boolean, shiftKey: boolean) { - if (shiftKey) { - multiSelect(id); - } else { - singleSelect(id, selected); - } - } - - function onSelectAll() { - const newSelectedIds: Set = new Set(); - items.forEach((item) => { - newSelectedIds.add(item.id); - }); - - setSelectedIds(newSelectedIds); - setLastClickedId(undefined); - } - - const onSelectNone = () => { - const newSelectedIds: Set = new Set(); - setSelectedIds(newSelectedIds); - setLastClickedId(undefined); - }; - - const onChangeZoom = (newZoomIndex: number) => { - const newFilter = cloneDeep(filter); - newFilter.zoomIndex = newZoomIndex; - updateFilter(newFilter); - }; - - const onOperationClicked = async (o: IListHookOperation) => { - await o.onClick(result, filter, selectedIds); - if (o.postRefetch) { - result.refetch(); - } - }; - - const operations = - otherOperations && - otherOperations.map((o) => ({ - text: o.text, - onClick: () => { - onOperationClicked(o); - }, - isDisplayed: () => { - if (o.isDisplayed) { - return o.isDisplayed(result, filter, selectedIds); - } - - return true; - }, - icon: o.icon, - buttonVariant: o.buttonVariant, - })); - - function onEdit() { - setIsEditDialogOpen(true); - } - - function onEditDialogClosed(applied: boolean) { - if (applied) { - onSelectNone(); - } - setIsEditDialogOpen(false); - - // refetch - result.refetch(); - } - - function onDelete() { - setIsDeleteDialogOpen(true); - } - - function onDeleteDialogClosed(deleted: boolean) { - if (deleted) { - onSelectNone(); - } - setIsDeleteDialogOpen(false); - - // refetch - result.refetch(); - } - - const renderPagination = () => ( - - ); - - const maybeRenderContent = () => { - if (result.loading || result.error) { - return; - } - - const pages = Math.ceil(totalCount / filter.itemsPerPage); - return ( - <> - {renderPagination()} - - {renderContent(result, filter, selectedIds, onChangePage, pages)} - - {renderPagination()} - - ); - }; - - const onChangeDisplayMode = (displayMode: DisplayMode) => { - const newFilter = cloneDeep(filter); - newFilter.displayMode = displayMode; - updateFilter(newFilter); - }; - - const onAddCriterion = ( - criterion: Criterion, - oldId?: string - ) => { - const newFilter = cloneDeep(filter); - - // Find if we are editing an existing criteria, then modify that. Or create a new one. - const existingIndex = newFilter.criteria.findIndex((c) => { - // If we modified an existing criterion, then look for the old id. - const id = oldId || criterion.getId(); - return c.getId() === id; - }); - if (existingIndex === -1) { - newFilter.criteria.push(criterion); - } else { - newFilter.criteria[existingIndex] = criterion; - } - - // Remove duplicate modifiers - newFilter.criteria = newFilter.criteria.filter((obj, pos, arr) => { - return arr.map((mapObj) => mapObj.getId()).indexOf(obj.getId()) === pos; - }); - - newFilter.currentPage = 1; - updateFilter(newFilter); - setEditingCriterion(undefined); - setNewCriterion(false); - }; - - const onRemoveCriterion = (removedCriterion: Criterion) => { - const newFilter = cloneDeep(filter); - newFilter.criteria = newFilter.criteria.filter( - (criterion) => criterion.getId() !== removedCriterion.getId() - ); - newFilter.currentPage = 1; - updateFilter(newFilter); - }; - - const updateCriteria = (c: Criterion[]) => { - const newFilter = cloneDeep(filter); - newFilter.criteria = c.slice(); - setNewCriterion(false); - }; - - function onCancelAddCriterion() { - setEditingCriterion(undefined); - setNewCriterion(false); - } - - const content = ( -
    - - setNewCriterion(true)} - filterDialogOpen={newCriterion ?? editingCriterion} - persistState={persistState} - /> - 0} - onEdit={renderEditDialog ? onEdit : undefined} - onDelete={renderDeleteDialog ? onDelete : undefined} - /> - - - setEditingCriterion(c)} - onRemoveCriterion={onRemoveCriterion} - /> - {(newCriterion || editingCriterion) && !filterDialog && ( - - )} - {newCriterion && - filterDialog && - filterDialog(filter.criteria, (c) => updateCriteria(c))} - {isEditDialogOpen && - renderEditDialog && - renderEditDialog( - getSelectedData(getData(result), selectedIds), - (applied) => onEditDialogClosed(applied) - )} - {isDeleteDialogOpen && - renderDeleteDialog && - renderDeleteDialog( - getSelectedData(getData(result), selectedIds), - (deleted) => onDeleteDialogClosed(deleted) - )} - {result.loading ? : undefined} - {result.error ?

    {result.error.message}

    : undefined} - {maybeRenderContent()} -
    - ); - - return { contentTemplate: content, onSelectChange }; -}; - -const useList = ( - options: IListHookOptions & - IQuery -): IListHookData => { - const filterOptions = getFilterOptions(options.filterMode); - - const history = useHistory(); - const location = useLocation(); - const [interfaceState, setInterfaceState] = useInterfaceLocalForage(); - const [filterInitialised, setFilterInitialised] = useState(false); - const { configuration: config } = useContext(ConfigurationContext); - // Store initial pathname to prevent hooks from operating outside this page - const originalPathName = useRef(location.pathname); - const persistanceKey = options.persistanceKey ?? options.filterMode; - - const defaultSort = options.defaultSort ?? filterOptions.defaultSortBy; - const defaultDisplayMode = filterOptions.displayModeOptions[0]; - const createNewFilter = useCallback(() => { - const filter = new ListFilterModel( - options.filterMode, - config, - defaultSort, - defaultDisplayMode, - options.defaultZoomIndex - ); - filter.configureFromQueryString(history.location.search); - return filter; - }, [ - options.filterMode, - config, - history, - defaultSort, - defaultDisplayMode, - options.defaultZoomIndex, - ]); - const [filter, setFilter] = useState(createNewFilter); - - const updateSavedFilter = useCallback( - (updatedFilter: ListFilterModel) => { - setInterfaceState((prevState) => { - if (!prevState.queryConfig) { - prevState.queryConfig = {}; - } - return { - ...prevState, - queryConfig: { - ...prevState.queryConfig, - [persistanceKey]: { - ...prevState.queryConfig[persistanceKey], - filter: queryString.stringify({ - ...queryString.parse( - prevState.queryConfig[persistanceKey]?.filter ?? "" - ), - disp: updatedFilter.displayMode, - }), - }, - }, - }; - }); - }, - [persistanceKey, setInterfaceState] - ); - - const { - data: defaultFilter, - loading: defaultFilterLoading, - } = useFindDefaultFilter(options.filterMode); - - const updateQueryParams = useCallback( - (newFilter: ListFilterModel) => { - const newParams = newFilter.makeQueryParameters(); - history.replace({ ...history.location, search: newParams }); - }, - [history] - ); - - const updateFilter = useCallback( - (newFilter: ListFilterModel) => { - setFilter(newFilter); - updateQueryParams(newFilter); - if (options.persistState === PersistanceLevel.VIEW) { - updateSavedFilter(newFilter); - } - }, - [options.persistState, updateSavedFilter, updateQueryParams] - ); - - // 'Startup' hook, initialises the filters - useEffect(() => { - // Only run once - if (filterInitialised) return; - - let newFilter = filter.clone(); - - if (options.persistState === PersistanceLevel.ALL) { - // only set default filter if query params are empty - if (!history.location.search) { - // wait until default filter is loaded - if (defaultFilterLoading) return; - - if (defaultFilter?.findDefaultFilter) { - newFilter.currentPage = 1; - try { - newFilter.configureFromJSON(defaultFilter.findDefaultFilter.filter); - } catch (err) { - console.log(err); - // ignore - } - // #1507 - reset random seed when loaded - newFilter.randomSeed = -1; - } - } - } else if (options.persistState === PersistanceLevel.VIEW) { - // wait until forage is initialised - if (interfaceState.loading) return; - - const storedQuery = interfaceState.data?.queryConfig?.[persistanceKey]; - if (options.persistState === PersistanceLevel.VIEW && storedQuery) { - const storedFilter = queryString.parse(storedQuery.filter); - if (storedFilter.disp !== undefined) { - const displayMode = Number.parseInt(storedFilter.disp as string, 10); - newFilter.displayMode = displayMode; - } - } - } - setFilter(newFilter); - updateQueryParams(newFilter); - - setFilterInitialised(true); - }, [ - filterInitialised, - filter, - history, - options.persistState, - updateQueryParams, - defaultFilter, - defaultFilterLoading, - interfaceState, - persistanceKey, - ]); - - // This hook runs on every page location change (ie navigation), - // and updates the filter accordingly. - useEffect(() => { - if (!filterInitialised) return; - - // Only update on page the hook was mounted on - if (location.pathname !== originalPathName.current) { - return; - } - - // Re-init filters on empty new query params - if (!location.search) { - setFilter(createNewFilter); - setFilterInitialised(false); - return; - } - - setFilter((prevFilter) => { - let newFilter = prevFilter.clone(); - newFilter.configureFromQueryString(location.search); - if (!isEqual(newFilter, prevFilter)) { - return newFilter; - } else { - return prevFilter; - } - }); - }, [filterInitialised, createNewFilter, location]); - - const onChangePage = useCallback( - (page: number) => { - const newFilter = cloneDeep(filter); - newFilter.currentPage = page; - updateFilter(newFilter); - window.scrollTo(0, 0); - }, - [filter, updateFilter] - ); - - const renderFilter = useMemo(() => { - if (filterInitialised) { - return options.filterHook - ? options.filterHook(cloneDeep(filter)) - : filter; - } - }, [filterInitialised, filter, options]); - - const renderList = useRenderList({ - ...options, - filter: renderFilter, - filterOptions, - onChangePage, - updateFilter, - }); - - const template = renderList ? ( - renderList.contentTemplate - ) : ( - - ); - - function onSelectChange(id: string, selected: boolean, shiftKey: boolean) { - if (renderList) { - renderList.onSelectChange(id, selected, shiftKey); - } - } - - return { - filter, - template, - onSelectChange, - onChangePage, - }; -}; - -export const useScenesList = ( - props: IListHookOptions -) => - useList({ - ...props, - filterMode: FilterMode.Scenes, - useData: useFindScenes, - getData: (result: FindScenesQueryResult) => - result?.data?.findScenes?.scenes ?? [], - getCount: (result: FindScenesQueryResult) => - result?.data?.findScenes?.count ?? 0, - getMetadataByline: (result: FindScenesQueryResult) => { - const duration = result?.data?.findScenes?.duration; - const size = result?.data?.findScenes?.filesize; - const filesize = size ? TextUtils.fileSize(size) : undefined; - - if (!duration && !size) { - return; - } - - const separator = duration && size ? " - " : ""; - - return ( - -  ( - {duration ? ( - - {TextUtils.secondsAsTimeString(duration, 3)} - - ) : undefined} - {separator} - {size && filesize ? ( - - - {` ${TextUtils.formatFileSizeUnit(filesize.unit)}`} - - ) : undefined} - ) - - ); - }, - }); - -export const useSceneMarkersList = ( - props: IListHookOptions -) => - useList({ - ...props, - filterMode: FilterMode.SceneMarkers, - useData: useFindSceneMarkers, - getData: (result: FindSceneMarkersQueryResult) => - result?.data?.findSceneMarkers?.scene_markers ?? [], - getCount: (result: FindSceneMarkersQueryResult) => - result?.data?.findSceneMarkers?.count ?? 0, - getMetadataByline: () => [], - }); - -export const useImagesList = ( - props: IListHookOptions -) => - useList({ - ...props, - filterMode: FilterMode.Images, - useData: useFindImages, - getData: (result: FindImagesQueryResult) => - result?.data?.findImages?.images ?? [], - getCount: (result: FindImagesQueryResult) => - result?.data?.findImages?.count ?? 0, - getMetadataByline: (result: FindImagesQueryResult) => { - const megapixels = result?.data?.findImages?.megapixels; - const size = result?.data?.findImages?.filesize; - const filesize = size ? TextUtils.fileSize(size) : undefined; - - if (!megapixels && !size) { - return; - } - - const separator = megapixels && size ? " - " : ""; - - return ( - -  ( - {megapixels ? ( - - Megapixels - - ) : undefined} - {separator} - {size && filesize ? ( - - - {` ${TextUtils.formatFileSizeUnit(filesize.unit)}`} - - ) : undefined} - ) - - ); - }, - }); - -export const useGalleriesList = ( - props: IListHookOptions -) => - useList({ - ...props, - filterMode: FilterMode.Galleries, - useData: useFindGalleries, - getData: (result: FindGalleriesQueryResult) => - result?.data?.findGalleries?.galleries ?? [], - getCount: (result: FindGalleriesQueryResult) => - result?.data?.findGalleries?.count ?? 0, - getMetadataByline: () => [], - }); - -export const useStudiosList = ( - props: IListHookOptions -) => - useList({ - ...props, - filterMode: FilterMode.Studios, - useData: useFindStudios, - getData: (result: FindStudiosQueryResult) => - result?.data?.findStudios?.studios ?? [], - getCount: (result: FindStudiosQueryResult) => - result?.data?.findStudios?.count ?? 0, - getMetadataByline: () => [], - }); - -export const usePerformersList = ( - props: IListHookOptions -) => - useList({ - ...props, - filterMode: FilterMode.Performers, - useData: useFindPerformers, - getData: (result: FindPerformersQueryResult) => - result?.data?.findPerformers?.performers ?? [], - getCount: (result: FindPerformersQueryResult) => - result?.data?.findPerformers?.count ?? 0, - getMetadataByline: () => [], - }); - -export const useMoviesList = ( - props: IListHookOptions -) => - useList({ - ...props, - filterMode: FilterMode.Movies, - useData: useFindMovies, - getData: (result: FindMoviesQueryResult) => - result?.data?.findMovies?.movies ?? [], - getCount: (result: FindMoviesQueryResult) => - result?.data?.findMovies?.count ?? 0, - getMetadataByline: () => [], - }); - -export const useTagsList = ( - props: IListHookOptions -) => - useList({ - ...props, - filterMode: FilterMode.Tags, - useData: useFindTags, - getData: (result: FindTagsQueryResult) => - result?.data?.findTags?.tags ?? [], - getCount: (result: FindTagsQueryResult) => - result?.data?.findTags?.count ?? 0, - getMetadataByline: () => [], - }); - -export const showWhenSelected = ( - _result: T, - _filter: ListFilterModel, - selectedIds: Set -) => { - return selectedIds.size > 0; -}; diff --git a/ui/v2.5/src/hooks/LocalForage.ts b/ui/v2.5/src/hooks/LocalForage.ts index 3632198f9..cf3d43ea0 100644 --- a/ui/v2.5/src/hooks/LocalForage.ts +++ b/ui/v2.5/src/hooks/LocalForage.ts @@ -29,7 +29,7 @@ interface ILocalForage { const Loading: Record = {}; const Cache: Record = {}; -export function useLocalForage( +export function useLocalForage( key: string, defaultValue: T = {} as T ): [ILocalForage, Dispatch>] { diff --git a/ui/v2.5/src/hooks/Toast.tsx b/ui/v2.5/src/hooks/Toast.tsx index 5489fa405..4ef8a7b12 100644 --- a/ui/v2.5/src/hooks/Toast.tsx +++ b/ui/v2.5/src/hooks/Toast.tsx @@ -77,12 +77,10 @@ function createHookObject(toastFunc: (toast: IToast) => void) { }; } -const useToasts = () => { +export const useToast = () => { const setToast = useContext(ToastContext); const [hookObject, setHookObject] = useState(createHookObject(setToast)); useEffect(() => setHookObject(createHookObject(setToast)), [setToast]); return hookObject; }; - -export default useToasts; diff --git a/ui/v2.5/src/hooks/debounce.ts b/ui/v2.5/src/hooks/debounce.ts new file mode 100644 index 000000000..236cbf35b --- /dev/null +++ b/ui/v2.5/src/hooks/debounce.ts @@ -0,0 +1,23 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable react-hooks/exhaustive-deps */ +import { DebounceSettings } from "lodash-es"; +import debounce, { DebouncedFunc } from "lodash-es/debounce"; +import React, { useCallback } from "react"; + +export function useDebounce any>( + fn: T, + deps: React.DependencyList, + wait?: number, + options?: DebounceSettings +): DebouncedFunc { + return useCallback(debounce(fn, wait, options), [...deps, wait, options]); +} + +// Convenience hook for use with state setters +export function useDebouncedSetState( + fn: React.Dispatch>, + wait?: number, + options?: DebounceSettings +): DebouncedFunc>> { + return useDebounce(fn, [], wait, options); +} diff --git a/ui/v2.5/src/hooks/index.ts b/ui/v2.5/src/hooks/index.ts deleted file mode 100644 index 457ce6cd0..000000000 --- a/ui/v2.5/src/hooks/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -export { default as useToast } from "./Toast"; -export { default as useInterval } from "./Interval"; -export { default as usePageVisibility } from "./PageVisibility"; -export { - useInterfaceLocalForage, - useChangelogStorage, - useLocalForage, -} from "./LocalForage"; -export { - useScenesList, - useSceneMarkersList, - useImagesList, - useGalleriesList, - useStudiosList, - usePerformersList, -} from "./ListHook"; -export { useLightbox, useGalleryLightbox } from "./Lightbox"; diff --git a/ui/v2.5/src/hooks/keybinds.ts b/ui/v2.5/src/hooks/keybinds.ts new file mode 100644 index 000000000..46c7e75c9 --- /dev/null +++ b/ui/v2.5/src/hooks/keybinds.ts @@ -0,0 +1,85 @@ +import Mousetrap from "mousetrap"; +import { useEffect, useRef } from "react"; +import { RatingSystemType } from "src/utils/rating"; + +export function useRatingKeybinds( + isVisible: boolean, + ratingSystem: RatingSystemType, + setRating: (v: number) => void +) { + const firstChar = useRef(undefined); + + const starRatingShortcuts: { [char: string]: number } = { + "0": NaN, + "1": 20, + "2": 40, + "3": 60, + "4": 80, + "5": 100, + }; + + function handleStarRatingKeybinds() { + for (const key in starRatingShortcuts) { + Mousetrap.bind(key, () => setRating(starRatingShortcuts[key])); + } + + setTimeout(() => { + for (const key in starRatingShortcuts) { + Mousetrap.unbind(key); + } + }, 1000); + } + + function handleDecimalKeybinds() { + Mousetrap.bind("`", () => { + setRating(NaN); + }); + + for (let i = 0; i <= 9; ++i) { + Mousetrap.bind(i.toString(), () => { + if (firstChar.current !== undefined) { + let combined = parseInt(firstChar.current + i.toString()); + if (combined === 0) { + combined = 100; + } + + setRating(combined); + firstChar.current = undefined; + } else { + firstChar.current = i.toString(); + } + }); + } + + setTimeout(() => { + firstChar.current = undefined; + + Mousetrap.unbind("`"); + for (let i = 0; i <= 9; ++i) { + Mousetrap.unbind(i.toString()); + } + }, 1000); + } + + useEffect(() => { + if (!isVisible) return; + + Mousetrap.bind("r", () => { + // numeric keypresses get caught by jwplayer, so blur the element + // if the rating sequence is started + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + + if (!ratingSystem || ratingSystem === RatingSystemType.Stars) { + return handleStarRatingKeybinds(); + } else { + return handleDecimalKeybinds(); + } + }); + + return () => { + Mousetrap.unbind("r"); + }; + }); +} diff --git a/ui/v2.5/src/hooks/state.ts b/ui/v2.5/src/hooks/state.ts index 00ee32a91..9c10523bd 100644 --- a/ui/v2.5/src/hooks/state.ts +++ b/ui/v2.5/src/hooks/state.ts @@ -32,3 +32,18 @@ export function useInitialState( return [value, setValue, setInitialValue]; } + +// useCompare is a hook that returns true if the value has changed since the last render. +export function useCompare(val: T) { + const prevVal = usePrevious(val); + return prevVal !== val; +} + +// usePrevious is a hook that returns the previous value of a variable. +export function usePrevious(value: T) { + const ref = React.useRef(); + React.useEffect(() => { + ref.current = value; + }, [value]); + return ref.current; +} diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index 3f5a8366f..5e4053684 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -25,7 +25,7 @@ @import "src/hooks/Interactive/interactive.scss"; @import "src/components/Dialogs/IdentifyDialog/styles.scss"; @import "src/components/Dialogs/styles.scss"; -@import "../node_modules/flag-icon-css/css/flag-icon.min.css"; +@import "flag-icons/css/flag-icons.min.css"; /* stylelint-disable */ #root { @@ -276,7 +276,7 @@ div.react-select__control { white-space: nowrap; .react-select__single-value, - .react-select__input { + .react-select__input-container { color: $text-color; } @@ -286,11 +286,15 @@ div.react-select__control { } } +div.react-select__menu-portal { + z-index: 1600; +} + div.react-select__menu, div.dropdown-menu { background-color: $secondary; color: $text-color; - z-index: 16; + z-index: 1600; .react-select__option, .dropdown-item { @@ -739,7 +743,7 @@ div.dropdown-menu { } .error-message { - white-space: "pre-wrap"; + white-space: pre-wrap; } .btn-toolbar .form-control { @@ -767,6 +771,79 @@ div.dropdown-menu { } } +$detailTabWidth: calc(100% / 3); + +.content-container, +.details-tab { + padding-left: 15px; + padding-right: 15px; + position: relative; + width: 100%; +} + +@media (min-width: 768px) { + .details-tab { + flex: 0 0 $detailTabWidth; + max-width: $detailTabWidth; + } + + .content-container { + flex: 0 0 calc(100% - #{$detailTabWidth}); + max-width: calc(100% - #{$detailTabWidth}); + } +} +@media (min-width: 1200px) { + .details-tab { + flex: 0 0 $detailTabWidth; + max-height: calc(100vh - 4rem); + max-width: $detailTabWidth; + overflow: auto; + + &.collapsed { + display: none; + } + } + + .details-divider { + flex: 0 0 15px; + height: calc(100vh - 4rem); + max-width: 15px; + + button { + background-color: transparent; + border: 0; + color: $link-color; + cursor: pointer; + font-size: 10px; + font-weight: 800; + height: 100%; + line-height: 100%; + padding: 0; + text-align: center; + width: 100%; + + &:active:not(:hover), + &:focus:not(:hover) { + background-color: transparent; + border: 0; + box-shadow: none; + } + } + } + + .content-container { + flex: 0 0 calc(100% - #{$detailTabWidth} - 15px); + max-height: calc(100vh - 4rem); + max-width: calc(100% - #{$detailTabWidth} - 15px); + overflow: auto; + + &.expanded { + flex: 0 0 calc(100% - 15px); + max-width: calc(100% - 15px); + } + } +} + .pre { white-space: pre-line; } diff --git a/ui/v2.5/src/index.tsx b/ui/v2.5/src/index.tsx index de26ef79c..ba718f75e 100755 --- a/ui/v2.5/src/index.tsx +++ b/ui/v2.5/src/index.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { ApolloProvider } from "@apollo/client"; import ReactDOM from "react-dom"; import { BrowserRouter } from "react-router-dom"; diff --git a/ui/v2.5/src/locales/bn-BD.json b/ui/v2.5/src/locales/bn-BD.json index e69142fad..2b2ca34f0 100644 --- a/ui/v2.5/src/locales/bn-BD.json +++ b/ui/v2.5/src/locales/bn-BD.json @@ -35,7 +35,7 @@ "download_backup": "জনানোগুলো ডাউনলোড করুন", "edit": "সম্পাদন করুন", "edit_entity": "{entityType} সম্পাদন করুন", - "export": "রপ্তানি…", + "export": "রপ্তানি", "export_all": "সব রপ্তানি করুন…", "find": "খোঁজ করুন", "finish": "শেষ করুন", diff --git a/ui/v2.5/src/locales/cs-CZ.json b/ui/v2.5/src/locales/cs-CZ.json index e069b5894..296d879a5 100644 --- a/ui/v2.5/src/locales/cs-CZ.json +++ b/ui/v2.5/src/locales/cs-CZ.json @@ -35,7 +35,7 @@ "download_backup": "Stáhnout zálohu", "edit": "Upravit", "edit_entity": "Upravit {entityType}", - "export": "Exportovat…", + "export": "Exportovat", "export_all": "Exportovat vše…", "find": "Hledat", "finish": "Dokončit", @@ -646,7 +646,6 @@ "details": "Detaily", "developmentVersion": "Vývojářská verze", "dialogs": { - "aliases_must_be_unique": "aliasy musí být unikátní", "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í.}}", @@ -696,5 +695,8 @@ "marker_image_previews_tooltip": "Animované WebP náhledy markerů, nezbytné pouze tehdy, pokud je Typ náhledu nastaven na \"Animovaný obrázek\".", "marker_screenshots": "Screenshoty markerů" } + }, + "validation": { + "aliases_must_be_unique": "aliasy musí být unikátní" } } diff --git a/ui/v2.5/src/locales/da-DK.json b/ui/v2.5/src/locales/da-DK.json index 8cdb5c5f5..d9698b74b 100644 --- a/ui/v2.5/src/locales/da-DK.json +++ b/ui/v2.5/src/locales/da-DK.json @@ -35,7 +35,7 @@ "download_backup": "Download Backup", "edit": "Ændre", "edit_entity": "Ændre {entityType}", - "export": "Eksportere…", + "export": "Eksportere", "export_all": "Exportere alt…", "find": "Finde", "finish": "Afslut", @@ -67,6 +67,7 @@ "play_selected": "Afspil valgte", "preview": "Forhåndsvisning", "previous_action": "Tilbage", + "reassign": "Gentildel", "refresh": "Opdater", "reload_plugins": "Genindlæs plugins", "reload_scrapers": "Genindlæs scrapers", @@ -99,10 +100,12 @@ "show": "Vis", "show_configuration": "Vis Konfiguration", "skip": "Spring over", + "split": "Opdel", "stop": "Stop", "submit": "Send", "submit_stash_box": "Send til Stash-Box", "submit_update": "Send opdatering", + "swap": "Ombyt", "tasks": { "clean_confirm_message": "Er du sikker på, at du vil Rense? Dette vil slette databaseinformation og genereret indhold for alle scener og gallerier, der ikke længere findes i filsystemet.", "dry_mode_selected": "Tør Tilstand valgt. Ingen egentlig sletning vil finde sted, kun logning.", @@ -121,6 +124,7 @@ "also_known_as": "Også kendt som", "ascending": "Stigende", "average_resolution": "Gennemsnitlig Opløsning", + "between_and": "og", "birth_year": "Fødselsår", "birthdate": "Fødselsdato", "bitrate": "Bithastighed", @@ -350,7 +354,7 @@ "auto_tagging": "Auto tagging", "backing_up_database": "Sikkerhedskopiering af database", "backup_and_download": "Udfører en sikkerhedskopi af databasen og downloader den resulterende fil.", - "backup_database": "Udfører en sikkerhedskopi af databasen til samme mappe som databasen med filnavnsformatet {filename_format}", + "backup_database": "Udfører en sikkerhedskopi af databasen til backup-mappen med filnavnsformatet {filename_format}", "cleanup_desc": "Tjek for manglende filer og fjern dem fra databasen. Dette er en destruktiv handling.", "data_management": "Datastyring", "defaults_set": "Standarder er blevet indstillet og vil blive brugt, når du klikker på knappen {action} på siden Opgaver.", @@ -427,7 +431,8 @@ }, "ui": { "abbreviate_counters": { - "description": "Forkort tællere i kort- og detaljebilleder, fx bliver \"1831\" forkortet til \"1.8K\"." + "description": "Forkort tællere i kort- og detaljebilleder, fx bliver \"1831\" forkortet til \"1.8K\".", + "heading": "Forkort tællere" }, "basic_settings": "Grundlæggende indstillinger", "custom_css": { @@ -435,6 +440,10 @@ "heading": "Brugerdefineret CSS", "option_label": "Brugerdefineret CSS aktiveret" }, + "custom_javascript": { + "description": "Siden skal genindlæses før ændringerne træder i kraft.", + "heading": "Brugerdefineret Javascript" + }, "custom_locales": { "description": "Ændre individuelle lokale strenge. Se https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/locales/en-GB.json for master-listen. Siden skal genindlæses for at aktivere ændringerne.", "heading": "Egen oversættelse", @@ -605,7 +614,6 @@ "details": "Detaljer", "developmentVersion": "Udviklingsversion", "dialogs": { - "aliases_must_be_unique": "aliaser skal være unikke", "delete_alert": "Følgende {count, plural, one {{singularEntity}} other {{pluralEntity}}} vil blive slettet permanent:", "delete_confirm": "Er du sikker på, at du vil slette {entityName}?", "delete_entity_desc": "{count, plural, one {Er du sikker på, at du vil slette denne {singularEntity}? Medmindre filen også slettes, vil denne {singularEntity} blive tilføjet igen, når scanningen udføres.} andet {Er du sikker på, at du vil slette disse {pluralEntity}? Medmindre filerne også slettes, vil disse {pluralEntity} blive tilføjet igen, når scanningen udføres.}}", @@ -893,7 +901,6 @@ "scenes": "Scener", "scenes_updated_at": "Scene opdateret den", "search_filter": { - "add_filter": "Tilføj filter", "name": "Filter", "saved_filters": "Gemte filtre", "update_filter": "Opdater filter" @@ -964,7 +971,7 @@ "your_system_has_been_created": "Succes! Dit system er blevet oprettet!" }, "welcome": { - "config_path_logic_explained": "Stash prøver først at finde sin konfigurationsfil (config.yml) fra den aktuelle arbejdsmappe, og hvis den ikke finder den der, falder den tilbage til $HOME/.stash/config. yml (på Windows vil dette være %USERPROFILE%\\.stash\\config.yml). Du kan også få Stash til at læse fra en specifik konfigurationsfil ved at køre den med -c eller --config muligheder.", + "config_path_logic_explained": "Stash prøver først at finde sin konfigurationsfil (config.yml) fra den aktuelle arbejdsmappe, og hvis den ikke finder den der, falder den tilbage til $HOME/.stash/config. yml (på Windows vil dette være %USERPROFILE%\\.stash\\config.yml). Du kan også få Stash til at læse fra en specifik konfigurationsfil ved at køre den med -c '' eller --config '' muligheder.", "in_current_stash_directory": "I mappen $HOME/.stash", "in_the_current_working_directory": "I den aktuelle arbejdsmappe", "next_step": "Med alt det ude af vejen, hvis du er klar til at fortsætte med at konfigurere et nyt system, skal du vælge, hvor du vil gemme din konfigurationsfil og klikke på Næste.", @@ -1030,6 +1037,9 @@ "type": "Type", "updated_at": "Opdateret Den", "url": "URL", + "validation": { + "aliases_must_be_unique": "aliaser skal være unikke" + }, "videos": "Videoer", "view_all": "Se alle", "weight": "Vægt", diff --git a/ui/v2.5/src/locales/de-DE.json b/ui/v2.5/src/locales/de-DE.json index b974233f2..b4dc9c864 100644 --- a/ui/v2.5/src/locales/de-DE.json +++ b/ui/v2.5/src/locales/de-DE.json @@ -6,6 +6,7 @@ "add_to_entity": "Hinzufügen zu {entityType}", "allow": "Erlauben", "allow_temporarily": "Vorübergehend erlauben", + "anonymise": "Anonymisieren", "apply": "Übernehmen", "auto_tag": "Auto-Tag", "backup": "Backup", @@ -20,6 +21,7 @@ "confirm": "Bestätigen", "continue": "Fortsetzen", "create": "Erstellen", + "create_chapters": "Kapitel erstellen", "create_entity": "Erstelle {entityType}", "create_marker": "Erstelle Markierung", "created_entity": "{entity_type} erstellt: {entity_name}", @@ -32,10 +34,11 @@ "delete_stashid": "StashID löschen", "disallow": "Nicht erlauben", "download": "Herunterladen", + "download_anonymised": "Anonymisiert herunterladen", "download_backup": "Lade Backup herunter", "edit": "Bearbeiten", "edit_entity": "Bearbeiten {entityType}", - "export": "Exportieren…", + "export": "Exportieren", "export_all": "Alle exportieren…", "find": "Suchen", "finish": "Fertig", @@ -58,6 +61,8 @@ "merge": "Zusammenführen", "merge_from": "Zusammenführen aus", "merge_into": "Zusammenführen in", + "migrate_blobs": "Blobs migrieren", + "migrate_scene_screenshots": "Szenen-Screenshots migrieren", "next_action": "Nächste", "not_running": "wird nicht ausgeführt", "open_in_external_player": "In externem Player öffnen", @@ -67,6 +72,7 @@ "play_selected": "Spiele ausgewählte", "preview": "Vorschau", "previous_action": "Zurück", + "reassign": "Neu zuordnen", "refresh": "Aktualisieren", "reload_plugins": "Plugins neu laden", "reload_scrapers": "Scraper neu laden", @@ -80,7 +86,7 @@ "save_delete_settings": "Verwende Option standardmäßig beim Löschen", "save_filter": "Filter speichern", "scan": "Scannen", - "scrape": "Durchsuchen", + "scrape": "Scrapen", "scrape_query": "Scrape Anfrage", "scrape_scene_fragment": "An Bruchstück scrapen", "scrape_with": "Scrape mit…", @@ -99,10 +105,12 @@ "show": "Anzeigen", "show_configuration": "Konfiguration anzeigen", "skip": "Überspringen", + "split": "Trennen", "stop": "Stopp", "submit": "Einreichen", "submit_stash_box": "Zu Stash-Box übermitteln", "submit_update": "Aktualisierung übermitteln", + "swap": "Tauschen", "tasks": { "clean_confirm_message": "Wollen Sie wirklich die Datenbank aufräumen? Dies wird alle Informationen und Hilfsdaten für Szenen und Galerien löschen, die nicht mehr auf dem Dateisystem vorhanden sind.", "dry_mode_selected": "Trockenmodus ausgewählt. Es findet keine Löschung der Daten statt, lediglich Protokollierung.", @@ -121,11 +129,17 @@ "also_known_as": "Auch bekannt unter", "ascending": "Aufsteigend", "average_resolution": "Durchschnittliche Auflösung", + "between_and": "und", "birth_year": "Geburtsjahr", "birthdate": "Geburtsdatum", "bitrate": "Bitrate", + "blobs_storage_type": { + "database": "Datenbank", + "filesystem": "Dateisystem" + }, "captions": "Untertitel", "career_length": "Karrierelänge", + "chapters": "Kapitel", "component_tagger": { "config": { "active_instance": "Aktive stash-box Instanz:", @@ -179,6 +193,7 @@ "latest_version": "Aktuellste Version", "latest_version_build_hash": "Neuester Build Hash:", "new_version_notice": "[NEU]", + "release_date": "Veröffentlichungsdatum:", "stash_discord": "Komm in unseren {url} Kanal", "stash_home": "Stash ist beheimatet auf {url}", "stash_open_collective": "Unterstütze uns über {url}", @@ -249,7 +264,15 @@ "description": "Verzeichnisspeicherort für SQLite-Datenbankdateisicherungen", "heading": "Backup-Verzeichnispfad" }, - "cache_location": "Verzeichnis für den Cache", + "blobs_path": { + "description": "Der Ort auf dem Dateisystem an dem die Binärdaten gespeichert werden. Wird nur angewendet, wenn der Binärdaten Speichertyp auf Dateisystem eingestellt ist. ACHTUNG: Eine Änderung des Pfades erfordert das manuelle Verschieben von bereits existierenden Daten.", + "heading": "Binärdaten Dateisystem-Pfad" + }, + "blobs_storage": { + "description": "Der Ort an dem Binärdaten wie Szenencover, Darsteller-, Studio- und Tag-Bilder bespeichert werden. Nach einer Änderung müssen die bereits existierenden Daten mit der Blobs migrieren Aufgabe migriert werden. Siehe Aufgaben-Seite für Migrierungen.", + "heading": "Binärdaten Speichertyp" + }, + "cache_location": "Verzeichnis für den Cache. Notwendig falls Streaming mit HLS (wie auf Apple Geräten üblich) oder DASH erfolgt.", "cache_path_head": "Cache Pfad", "calculate_md5_and_ohash_desc": "Berechne MD5 Prüfsumme zusätzlich zu oshash. Aktivierung führt dazu, dass erstmalige Scans mehr Zeit benötigen. Dateibenennungshash muss auf oshash gesetzt sein, um Berechnung des MD5 zu unterbinden.", "calculate_md5_and_ohash_label": "Berechne MD5 für Videodateien", @@ -259,12 +282,43 @@ "chrome_cdp_path_desc": "Dateipfad zur Chrome Executable oder einer externen Adresse (beginnend mit http:// oder https://, bspw. http://localhost:9222/json/version) die auf eine Chrome Instanz zeigt.", "create_galleries_from_folders_desc": "Wenn ausgewählt, erzeuge Galerien aus Verzeichnissen, welche Bilder enthalten.", "create_galleries_from_folders_label": "Erzeuge Galerien aus Verzeichnissen mit Bilder darin", + "database": "Datenbank", "db_path_head": "Datenbank Pfad", "directory_locations_to_your_content": "Verzeichnis zu Ihren Inhalten", "excluded_image_gallery_patterns_desc": "Reguläre Ausdrücke für Dateinamen/Pfade von Bildern/Galerien, welche von Scans ausgeschlossen werden und beim Aufräumen der Datenbank berücksichtigt werden sollen", "excluded_image_gallery_patterns_head": "Schema für ausgeschlossene Bilder/Galerien", "excluded_video_patterns_desc": "Reguläre Ausdrücke für Dateinamen/Pfade von Videos, welche von Scans ausgeschlossen werden und beim Aufräumen der Datenbank berücksichtigt werden sollen", "excluded_video_patterns_head": "Schema für ausgeschlossene Videos", + "ffmpeg": { + "hardware_acceleration": { + "desc": "Nutzt verfügbare Hardware zum Kodieren von Video für Live-Transkodierung.", + "heading": "FFmpeg Hardware-Kodierung" + }, + "live_transcode": { + "input_args": { + "desc": "Erweitert: Zusätzliche Parameter für die Live-Transkodierung mit ffmpeg, welche vor dem Eingabefeld übergeben werden können.", + "heading": "FFmpeg Live Transcode Eingangsparameter" + }, + "output_args": { + "desc": "Erweitert: Zusätzliche Parameter für die Live-Transkodierung mit ffmpeg, welche vor dem Ausgabefeld übergeben werden können.", + "heading": "FFmpeg Live-Transkodierung Ausgangsparameter" + } + }, + "transcode": { + "input_args": { + "desc": "Erweitert: Zusätzliche Parameter für die Live-Transkodierung mit ffmpeg, welche vor dem Eingabefeld übergeben werden können.", + "heading": "FFmpeg Live-Transkodierung Eingangsparameter" + }, + "output_args": { + "desc": "Erweitert: Zusätzliche Parameter für die Videogenerierung mit ffmpeg, welche vor dem Ausgabefeld übergeben werden können.", + "heading": "FFmpeg Transkodierung Ausgangsparameter" + } + } + }, + "funscript_heatmap_draw_range": "Reichweite in generierte Heatmaps einbeziehen", + "funscript_heatmap_draw_range_desc": "Zeichnet den Bewegungsbereich auf der y-Achse der erzeugten Heatmap. Vorhandene Heatmaps müssen nach der Änderung neu generiert werden.", + "gallery_cover_regex_desc": "Regulärer Ausdruck, verwendet um ein Bild als Galerietitelbild zu identifiziert", + "gallery_cover_regex_label": "Schema für Galerietitelbilder", "gallery_ext_desc": "Durch Kommas getrennte Liste von Dateiformaten, welche als Galeriecontainer gelesen werden sollen.", "gallery_ext_head": "Galeriecontainer Dateiformate", "generated_file_naming_hash_desc": "Verwende MD5 oder oshash für die Benennung der generierten Dateien. Um dies zu ändern, müssen für alle Szenen der entsprechende MD5/oshash berechnet werden. Nachdem dieser Wert geändert wurde, müssen vorhandene generierte Dateien migriert oder neu generiert werden. Siehe Aufgabenseite für die Migration.", @@ -272,6 +326,7 @@ "generated_files_location": "Verzeichnisspeicherort für die generierten Dateien (Markierungen, Vorschauen, Sprites usw.)", "generated_path_head": "Pfad für generierte Dateien", "hashing": "Hashwertberechnung", + "heatmap_generation": "Funscript Heatmap Erzeugung", "image_ext_desc": "Durch Kommas getrennte Liste von Dateierweiterungen, die als Bilder identifiziert werden.", "image_ext_head": "Bilderweiterungen", "include_audio_desc": "Binde Audiostream bei der Erstellung der Videovorschau ein.", @@ -300,7 +355,7 @@ "heading": "Scraper Pfad" }, "scraping": "Durchsuchen", - "sqlite_location": "Dateispeicherort für die SQLite-Datenbank (erfordert Neustart)", + "sqlite_location": "Dateispeicherort für die SQLite-Datenbank (erfordert Neustart). ACHTUNG: Ein Speicherort auf einem anderen System als dem Server auf dem Stash läuft (z.B. Netzwerkspeicher) wird nicht unterstützt!", "video_ext_desc": "Durch Kommas getrennte Liste von Dateierweiterungen, die als Videos identifiziert werden.", "video_ext_head": "Videodateiformate", "video_head": "Video" @@ -342,6 +397,9 @@ }, "tasks": { "added_job_to_queue": "{operation_name} zur Auftragswarteschlange hinzugefügt", + "anonymise_and_download": "Erstellt eine anonymisierte Kopie der Datenbank und lädt diese im Anschluss herunter.", + "anonymise_database": "Erstellt eine Kopie der Datenbank in das Backup-Verzeichnis und anonymisiert alle empfindlichen Daten. Diese kann dann für zur Fehlersuche und -behebung geteilt werden. Die ursprüngliche Datenbank wird dabei nicht verändert. Die anonymisierte Datenbank verwendet das Dateiformat {filename_format}.", + "anonymising_database": "Anonymisiere Datenbank", "auto_tag": { "auto_tagging_all_paths": "Automatisches Taggen aller Pfade", "auto_tagging_paths": "Automatisches Taggen der folgenden Pfade" @@ -368,6 +426,7 @@ "generate_previews_during_scan_tooltip": "Generiert animierte WebP-Vorschaubilder, nur erforderlich, wenn der Vorschautyp auf Animiertes Bild eingestellt ist.", "generate_sprites_during_scan": "Scrubber-Sprites generieren", "generate_thumbnails_during_scan": "Generiert Miniaturansichten für Bilder", + "generate_video_covers_during_scan": "Erzeuge Szenen-Cover", "generate_video_previews_during_scan": "Vorschaubilder generieren", "generate_video_previews_during_scan_tooltip": "Generiert Videovorschauen, die abgespielt werden, wenn man den Mauszeiger über eine Szene bewegt", "generated_content": "Generierter Inhalt", @@ -395,7 +454,16 @@ "incremental_import": "Inkrementeller Import aus einer Export-ZIP-Datei.", "job_queue": "Aufgabenwarteschlange", "maintenance": "Instandhaltung", + "migrate_blobs": { + "delete_old": "Ältere Daten löschen", + "description": "Migriere Blobs auf den aktuellen Binärdaten Speichertyp. Diese Migration sollte durchgeführt werden nachdem der Binärdaten Speichertyp geändert wurde. Optional können die alten Daten nach der Migration gelöscht werden." + }, "migrate_hash_files": "Wird nach dem Ändern des Dateinamen-Hashs für generierte Dateien verwendet, um vorhandene generierte Dateien in das neue Hash-Format umzubenennen.", + "migrate_scene_screenshots": { + "delete_files": "Screenshot-Dateien löschen", + "description": "Migriere Szenen-Screenshots in den neuen Binärdaten Speichertyp. Diese Migration sollte durchgeführt werden nachdem ein System auf die Version 0.20 geupdatet wurde. Optional können ältere Screenshot-Dateien gelöscht werden.", + "overwrite_existing": "Überschreibe existierende Binärblobs mit Screenshot-Dateien" + }, "migrations": "Migrationen", "only_dry_run": "Führt einen Probelauf durch. Es wird noch nichts entfernt", "plugin_tasks": "Plugin-Aufgaben", @@ -432,10 +500,15 @@ }, "basic_settings": "Grundeinstellungen", "custom_css": { - "description": "Die Seite muss neu geladen werden, damit die Änderungen wirksam werden.", + "description": "Die Seite muss neu geladen werden, damit die Änderungen wirksam werden. Es gibt keine Garantie für die Kompatibilität des benutzerdefinierten CSS und zukünftigen Versionen von Stash.", "heading": "Benutzerdefinierte CSS", "option_label": "Benutzerdefiniertes CSS aktiviert" }, + "custom_javascript": { + "description": "Seite muss neu geladen werden, damit die Änderungen wirksam werden. Es gibt keine Garantie für die Kompatibilität des benutzerdefinierten Javascript und zukünftigen Versionen von Stash.", + "heading": "Benutzerdefiniertes Javascript", + "option_label": "Benutzerdefiniertes Javascript aktiviert" + }, "custom_locales": { "description": "Überschreibe einzelne Locale-Strings. Siehe https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/locales/en-GB.json für die Hauptliste. Die Seite muss neu geladen werden, damit die Änderungen wirksam werden.", "heading": "Benutzerdefinierte Lokalisierung", @@ -461,7 +534,28 @@ "description": "Entferne die Möglichkeit der Erstellung neuer Objekte in der Dropdown-Auswahl", "heading": "Entferne Dropdown Erstellung" }, - "heading": "Editieren" + "heading": "Editieren", + "max_options_shown": { + "label": "Maximalanzahl der anzuzeigenden Objekte in Dropdown-Menüs" + }, + "rating_system": { + "star_precision": { + "label": "Präzision der Sternebewertung", + "options": { + "full": "Voll", + "half": "Halb", + "quarter": "Viertel", + "tenth": "Zehntel" + } + }, + "type": { + "label": "Art des Bewertungssystems", + "options": { + "decimal": "Dezimal", + "stars": "Sterne" + } + } + } }, "funscript_offset": { "description": "Zeitversatz in Millisekunden für interaktive Skriptwiedergabe.", @@ -484,6 +578,11 @@ "image_lightbox": { "heading": "Bild-Lightbox" }, + "image_wall": { + "direction": "Richtung", + "heading": "Bilderwand", + "margin": "Marge (Pixel)" + }, "images": { "heading": "Bilder", "options": { @@ -505,6 +604,10 @@ "description": "Anzeigen oder Ausblenden verschiedener Inhaltstypen in der Navigationsleiste", "heading": "Menüpunkte" }, + "minimum_play_percent": { + "description": "Der prozentuale Anteil der Zeit, in der eine Szene gespielt werden muss, bevor Abspielen gezählt wird ist erhöht worden.", + "heading": "Mindestabspieldauer (Prozent)" + }, "performers": { "options": { "image_location": { @@ -531,6 +634,7 @@ "scene_player": { "heading": "Szenenplayer", "options": { + "always_start_from_beginning": "Video immer von Anfang an starten", "auto_start_video": "Video automatisch starten", "auto_start_video_on_play_selected": { "description": "Automatischer Start von Videos aus der Warteschlange oder bei einer Wiedergabe von ausgewählten oder zufälligen Videos von der Szenen-Seite", @@ -540,7 +644,8 @@ "description": "Nächste Szene in der Warteschlange spielen", "heading": "Standardmäßig die Wiedergabeliste fortsetzen" }, - "show_scrubber": "Scrubber anzeigen" + "show_scrubber": "Scrubber anzeigen", + "track_activity": "Aktivität verfolgen" } }, "scene_wall": { @@ -621,6 +726,8 @@ }, "custom": "Benutzerdefiniert", "date": "Datum", + "date_format": "YYYY-MM-DD", + "datetime_format": "YYYY-MM-DD HH:MM", "death_date": "Todesdatum", "death_year": "Todesjahr", "descending": "Absteigend", @@ -629,10 +736,11 @@ "details": "Details", "developmentVersion": "Entwicklungsversion", "dialogs": { - "aliases_must_be_unique": "Aliase müssen einzigartig sein", + "create_new_entity": "Neues {entity} erstellen", "delete_alert": "Folgende {count, plural, one {{singularEntity}} other {{pluralEntity}}} werden dauerhaft gelöscht:", "delete_confirm": "Möchten Sie {entityName} wirklich löschen?", - "delete_entity_desc": "{count, plural, one {Möchten Sie {singularEntity} wirklich löschen? Sofern die Datei nicht ebenfalls gelöscht wird, wird diese {singularEntity} beim Scannen wieder hinzugefügt.} other {Möchten Sie {pluralEntity} wirklich löschen? Sofern die Dateien nicht ebenfalls gelöscht werden, werden diese {pluralEntity} beim Scannen wieder hinzugefügt.}}", + "delete_entity_desc": "{count, plural, one {Möchten Sie {singularEntity} wirklich löschen? Sofern die Datei nicht ebenfalls gelöscht werden soll, wird diese {singularEntity} beim Scannen wieder hinzugefügt.} other {Möchten Sie {pluralEntity} wirklich löschen? Sofern die Dateien nicht ebenfalls gelöscht werden sollen, werden diese {pluralEntity} beim Scannen wieder hinzugefügt.}}", + "delete_entity_simple_desc": "{count, plural, one {Möchten Sie {singularEntity} wirklich löschen?} other {Möchten Sie diese {pluralEntity} wirklich löschen?}}", "delete_entity_title": "{count, plural, one {Lösche {singularEntity}} other {Lösche {pluralEntity}}}", "delete_galleries_extra": "…plus allen Bilddateien, die keiner anderen Galerie angehängt sind.", "delete_gallery_files": "Lösche Galerieordner/zip Datei und alle Bilder, die keiner anderen Galerie angehängt sind.", @@ -643,6 +751,14 @@ "edit_entity_title": "Bearbeiten von {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "export_include_related_objects": "Zugehörige Objekte in den Export einbeziehen", "export_title": "Export", + "imagewall": { + "direction": { + "column": "Spalten", + "description": "Spalten- oder Reihenlayout.", + "row": "Zeilen" + }, + "margin_desc": "Anzahl der Marge (in Pixeln) um jedes Bild." + }, "lightbox": { "delay": "Verzögerung (Sek)", "display_mode": { @@ -652,6 +768,7 @@ "original": "Original" }, "options": "Optionen", + "page_header": "Seite {page} / {total}", "reset_zoom_on_nav": "Zoomstufe beim Bildwechsel zurücksetzen", "scale_up": { "description": "Skaliere kleinere Bilder auf Bildschirmgröße", @@ -664,12 +781,22 @@ "zoom": "Zoomen" } }, + "merge": { + "destination": "Ziel", + "empty_results": "Die Werte der Zielfelder bleiben unverändert.", + "source": "Quelle" + }, "merge_tags": { "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" + }, "scene_gen": { + "covers": "Szene-Cover", "force_transcodes": "Transcode Erzeugung erzwingen", "force_transcodes_tooltip": "Standardmäßig werden Transkodierungen nur erzeugt, wenn die Videodatei im Browser nicht unterstützt wird. Wenn diese Option aktiviert ist, werden Transkodierungen auch dann erstellt, wenn die Videodatei vom Browser unterstützt zu werden scheint.", "image_previews": "Animierte Bildvorschauen", @@ -714,6 +841,7 @@ }, "dimensions": "Maße", "director": "Regisseur", + "disambiguation": "Begriffsklärung", "display_mode": { "grid": "Gitter", "list": "Liste", @@ -758,6 +886,11 @@ "warmth": "Wärme" }, "empty_server": "Fügen Sie Ihrem Server einige Szenen hinzu, um Empfehlungen auf dieser Seite anzuzeigen.", + "errors": { + "image_index_greater_than_zero": "Bilderindex muss größer 0 sein", + "lazy_component_error_help": "Sollten Sie kürzlich ein Update für Stash durchgeführt haben, laden Sie bitte die Seite neu oder löschen Sie den Browser-Cache.", + "something_went_wrong": "Etwas ist schief gelaufen." + }, "ethnicity": "Ethnizität", "existing_value": "vorhandener Wert", "eye_color": "Augenfarbe", @@ -769,6 +902,7 @@ "file_info": "Datei", "file_mod_time": "Dateiänderungszeit", "files": "Dateien", + "files_amount": "{value} Dateien", "filesize": "Dateigröße", "filter": "Filter", "filter_name": "Filtername", @@ -804,12 +938,15 @@ "syncing": "Synchronisiert mit Server", "uploading": "Skript wird hochgeladen" }, + "hasChapters": "Hat Kapitel", "hasMarkers": "Hat Markierungen", "height": "Größe", + "height_cm": "Höhe (cm)", "help": "Hilfe", "ignore_auto_tag": "Auto-Tag ignorieren", "image": "Bild", "image_count": "Bilderanzahl", + "image_index": "Bild #", "images": "Bilder", "include_parent_tags": "Übergeordnete Tags einbeziehen", "include_sub_studios": "Untergeordnete Studios einbeziehen", @@ -818,6 +955,7 @@ "interactive": "Interaktiv", "interactive_speed": "Interaktive Geschwindigkeit", "isMissing": "Fehlt", + "last_played_at": "Zuletzt Abgespielt Am", "library": "Bibliothek", "loading": { "generic": "Wird geladen…" @@ -836,6 +974,8 @@ "age_context": "{age} {years_old} in dieser Szene" }, "phash": "PHashwert", + "play_count": "Anzahl Wiedergaben", + "play_duration": "Abspielzeit", "stream": "Stream", "video_codec": "Video-Codec" }, @@ -906,6 +1046,8 @@ }, "performers": "Darsteller", "piercings": "Piercings", + "play_count": "Anzahl der Videowiedergaben", + "play_duration": "Abspiellänge", "primary_file": "Primäre Datei", "queue": "Playlist", "random": "Zufällig", @@ -914,15 +1056,20 @@ "recently_released_objects": "Kürzlich erschienene {objects}", "release_notes": "Versionshinweise", "resolution": "Auflösung", + "resume_time": "Zeit fortsetzen", "scene": "Szene", "sceneTagger": "Szenen-Tagger", "sceneTags": "Szenen-Tags", + "scene_code": "Studio Code", "scene_count": "Szenenanzahl", + "scene_created_at": "Szene angelegt am", + "scene_date": "Datum der Szene", "scene_id": "Szenen-ID", + "scene_updated_at": "Szene geändert am", "scenes": "Szenen", "scenes_updated_at": "Szene aktualisiert am", "search_filter": { - "add_filter": "Filter hinzufügen", + "edit_filter": "Filter editieren", "name": "Filter", "saved_filters": "Gespeicherte Filter", "update_filter": "Filter aktualisieren" @@ -932,8 +1079,12 @@ "setup": { "confirm": { "almost_ready": "Wir sind fast bereit die Konfiguration abzuschließen. Bitte bestätige die folgenden Einstellungen. Du kannst auf Zurück klicken, um etwas Falsches zu ändern. Wenn alles gut aussieht, klicke auf Bestätigen, um dein System zu erstellen.", + "blobs_directory": "Binärdaten-Verzeichnis", + "cache_directory": "Cache-Verzeichnis", "configuration_file_location": "Ort der Konfigurationsdatei:", "database_file_path": "Dateipfad der Datenbank", + "default_blobs_location": "", + "default_cache_location": "/cache", "default_db_location": "/stash-go.sqlite", "default_generated_content_location": "/generated", "generated_directory": "Ordner der generierten Hilfsdateien", @@ -969,12 +1120,20 @@ }, "paths": { "database_filename_empty_for_default": "Datenbank-Dateiname (Leer für Standardwert)", - "description": "Als nächstes müssen wir festhalten wo für deine Porno-Kollektion finden können und wo wir unsere Datenbank und Hilfsdateien speichern dürfen. Diese Einstellungen lassen sich später auch noch ändern.", + "description": "Als nächstes müssen wir festhalten wo für deine Porno-Kollektion finden können und wo wir unsere Datenbank, generierten Hilfsdateien und Cache speichern dürfen. Diese Einstellungen lassen sich später auch noch ändern.", + "path_to_blobs_directory_empty_for_database": "Pfad zum Binärblob-Verzeichnis (leer um die Datenbank dafür zu nutzen)", + "path_to_cache_directory_empty_for_default": "Pfad zum Cache-Verzeichnis (leer für Voreinstellung)", "path_to_generated_directory_empty_for_default": "Pfad zum Ordner der Hilfsdateien (Leer für Standardwert)", "set_up_your_paths": "Setze die Dateipfade", "stash_alert": "Es wurde kein Bibliotheks-Pfad gesetzt. Somit werden keine Dateien in Stash eingescannt. Bist du dir sicher?", + "where_can_stash_store_blobs": "Wo darf Stash die Binärdaten-Blobs speichern?", + "where_can_stash_store_blobs_description": "Stash kann Binärdaten wie Szene-Cover, Darsteller-, Studio- und Tag-Bilder entweder in der Datenbank oder auf dem Dateisystem speichern. Als Voreinstellung wird Stash ein Verzeichnis blobs im Ordner erstellen in dem auch die Konfigurationsdatei gespeichert ist. Wenn Sie dies ändern möchten, geben Sie bitte einen absoluten oder relativen Pfad an. Stash wird dieses Verzeichnis für Sie erstellen, sollte es nicht bereits existieren.", + "where_can_stash_store_blobs_description_addendum": "Wenn Sie alternativ die Daten in der Datenbank speichern wollen, dann lassen Sie das Feld leer. Notiz: Dies wird die Datenbank start vergrößern und Migrierungsaufgaben werden länger dauern.", + "where_can_stash_store_cache_files": "Wo darf Stash Cache-Dateien zwischenspeichern?", + "where_can_stash_store_cache_files_description": "Um einige Funktionen wie HLS/DASH Live-Transkodierung zu nutzen, muss Stash über ein Cache-Verzeichnis als temporären Zwischenspeicher verfügen. Als Voreinstellung wird Stash ein Verzeichnis cache im Ordner erstellen in dem auch die Konfigurationsdatei gespeichert ist. Wenn Sie dies ändern möchten, geben Sie bitte einen absoluten oder relativen Pfad an. Stash wird dieses Verzeichnis für Sie erstellen, sollte es nicht bereits existieren.", "where_can_stash_store_its_database": "Wo darf Stash seine Datenbank abspeichern?", - "where_can_stash_store_its_database_description": "Stash nutzt eine sqlite-Datenbank, um Metadaten über deine Pornosammlung zu speichern. Standardmäßig wird diese als stash-go.sqlite in dem Ordner gespeichert, in dem auf deine Konfigurationsdatei liegt. Wenn du das ändern möchtest, gebe bitte einen absoluten oder relativen (gegenüber der aktuellen working directory) Pfad mit Dateinamen an.", + "where_can_stash_store_its_database_description": "Stash nutzt eine SQLite-Datenbank, um Metadaten über deine Pornosammlung zu speichern. Standardmäßig wird diese als stash-go.sqlite in dem Ordner gespeichert, in dem auf deine Konfigurationsdatei liegt. Wenn du das ändern möchtest, gebe bitte einen absoluten oder relativen (gegenüber der aktuellen working directory) Pfad mit Dateinamen an.", + "where_can_stash_store_its_database_warning": "ACHTUNG: Ein Speicherort abseits des Systems auf dem Stash ausgeführt wird (z.B. speichern der Datenbank auf einem Netzwerkspeicher während Stash auf einem anderen Computer ausgeführt wird) ist nicht unterstützt! SQLite ist nicht für Nutzung über das Netzwerk ausgelegt und der Versuch, dies zu tun, kann sehr leicht dazu führen, dass Ihre gesamte Datenbank beschädigt wird.", "where_can_stash_store_its_generated_content": "Wo darf Stash seine generierten Hilfsdateien abspeichern?", "where_can_stash_store_its_generated_content_description": "Um Thumbnails, Previews und Sprites zur Verfügung zu stellen, generiert Stash diese aus deinen Videos und Bildern. Das schließt auch Transkodierungen von nicht unterstützten Dateiformaten mit ein. Standardmäßig wird Stash diese im Ordner generated abspeichern, der sich am Ort der Konfigurationsdatei befindet. Wenn du das ändern möchtest, gebe bitte einen absoluten oder relativen (gegenüber der aktuellen working directory) Pfad an. Stash wird den Ordner erstellen, sollte er noch nicht existieren.", "where_is_your_porn_located": "Wo finden wir deine Porno-Kollektion?", @@ -994,7 +1153,7 @@ "your_system_has_been_created": "Geschafft! Dein System wurde erstellt!" }, "welcome": { - "config_path_logic_explained": "Stash versucht zunächst seine Konfigurationsdatei (config.yml) in dem aktuellen Arbeitsverzeichnis zu finden, wenn das nicht gelingt fällt es auf $HOME/.stash/config.yml (bei Windows ist das %USERPROFILE%\\.stash\\config.yml) zurück. Du kannst Stash auch einen Pfad beim Start durch die Kommandozeilen-Option -c or --config vorgeben.", + "config_path_logic_explained": "Stash versucht zunächst seine Konfigurationsdatei (config.yml) in dem aktuellen Arbeitsverzeichnis zu finden, wenn das nicht gelingt fällt es auf $HOME/.stash/config.yml (bei Windows ist das %USERPROFILE%\\.stash\\config.yml) zurück. Du kannst Stash auch einen Pfad beim Start durch die Kommandozeilen-Option -c '' or --config '' vorgeben.", "in_current_stash_directory": "Im Verzeichnis $HOME/.stash", "in_the_current_working_directory": "Im aktuellen Arbeitsverzeichnis", "next_step": "Nachdem das alles aus dem Weg ist, sind wir jetzt bereit ein neues System zu erstellen. Wähle dazu zunächst aus wo du die Konfigurationsdatei speichern möchtest und klicke auf Weiter.", @@ -1010,6 +1169,7 @@ "welcome_to_stash": "Willkommen zu Stash" }, "stash_id": "Stash-ID", + "stash_id_endpoint": "Stash ID Endpunkt", "stash_ids": "Stash IDs", "stashbox": { "go_review_draft": "Gehe zu {endpoint_name}, um Entwurf zu begutachten.", @@ -1045,7 +1205,10 @@ "default_filter_set": "Standardfiltersatz", "delete_past_tense": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} gelöscht", "generating_screenshot": "Screenshot wird erstellt…", + "image_index_too_large": "Fehler: Bild-Index ist größer als die Anzahl der Bilder der Gallerie", + "merged_scenes": "Zusammengefasste Szene", "merged_tags": "Zusammengeführte Tags", + "reassign_past_tense": "Datei neu zugewiesen", "removed_entity": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} gelöscht", "rescanning_entity": "Erneutes Scannen von {count, plural, one {{singularEntity}} other {{pluralEntity}}}…", "saved_entity": "{entity} gespeichert", @@ -1060,9 +1223,15 @@ "type": "Typ", "updated_at": "Aktualisiert am", "url": "URL", + "validation": { + "aliases_must_be_unique": "Aliase müssen einzigartig sein", + "date_invalid_form": "${path} muss die Form YYYY-MM-DD haben", + "required": "${path} ist ein notwendiges Feld" + }, "videos": "Videos", "view_all": "Alle ansehen", "weight": "Gewicht", + "weight_kg": "Gewicht (kg)", "years_old": "Jahre alt", "zip_file_count": "Anzahl der Zip-Dateien" } diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 52d19f2eb..858215286 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -6,6 +6,7 @@ "add_to_entity": "Add to {entityType}", "allow": "Allow", "allow_temporarily": "Allow temporarily", + "anonymise": "Anonymise", "apply": "Apply", "auto_tag": "Auto Tag", "backup": "Backup", @@ -20,6 +21,7 @@ "confirm": "Confirm", "continue": "Continue", "create": "Create", + "create_chapters": "Create Chapter", "create_entity": "Create {entityType}", "create_marker": "Create Marker", "created_entity": "Created {entity_type}: {entity_name}", @@ -32,10 +34,11 @@ "delete_stashid": "Delete StashID", "disallow": "Disallow", "download": "Download", + "download_anonymised": "Download anonymised", "download_backup": "Download Backup", "edit": "Edit", "edit_entity": "Edit {entityType}", - "export": "Export…", + "export": "Export", "export_all": "Export all…", "find": "Find", "finish": "Finish", @@ -58,6 +61,8 @@ "merge": "Merge", "merge_from": "Merge from", "merge_into": "Merge into", + "migrate_blobs": "Migrate Blobs", + "migrate_scene_screenshots": "Migrate Scene Screenshots", "next_action": "Next", "not_running": "not running", "open_in_external_player": "Open in external player", @@ -99,8 +104,8 @@ "set_image": "Set image…", "show": "Show", "show_configuration": "Show Configuration", - "split": "Split", "skip": "Skip", + "split": "Split", "stop": "Stop", "submit": "Submit", "submit_stash_box": "Submit to Stash-Box", @@ -124,12 +129,17 @@ "also_known_as": "Also known as", "ascending": "Ascending", "average_resolution": "Average Resolution", + "between_and": "and", "birth_year": "Birth Year", "birthdate": "Birthdate", "bitrate": "Bit Rate", - "between_and": "and", + "blobs_storage_type": { + "database": "Database", + "filesystem": "Filesystem" + }, "captions": "Captions", "career_length": "Career Length", + "chapters": "Chapters", "component_tagger": { "config": { "active_instance": "Active stash-box instance:", @@ -160,7 +170,7 @@ "duration_unknown": "Duration unknown", "fp_found": "{fpCount, plural, =0 {No new fingerprint matches found} other {# new fingerprint matches found}}", "fp_matches": "Duration is a match", - "fp_matches_multi": "Duration matches {matchCount}/{durationsLength} fingerprint(s)", + "fp_matches_multi": "Duration matches {matchCount}/{durationsLength} fingerprints", "hash_matches": "{hash_type} is a match", "match_failed_already_tagged": "Scene already tagged", "match_failed_no_result": "No results found", @@ -183,6 +193,7 @@ "latest_version": "Latest Version", "latest_version_build_hash": "Latest Version Build Hash:", "new_version_notice": "[NEW]", + "release_date": "Release date:", "stash_discord": "Join our {url} channel", "stash_home": "Stash home at {url}", "stash_open_collective": "Support us through {url}", @@ -253,7 +264,15 @@ "description": "Directory location for SQLite database file backups", "heading": "Backup Directory Path" }, - "cache_location": "Directory location of the cache", + "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" + }, + "blobs_storage": { + "description": "Where to store binary data such as scene covers, performer, studio and tag images. After changing this value, the existing data must be migrated using the Migrate Blobs tasks. See Tasks page for migration.", + "heading": "Binary data storage type" + }, + "cache_location": "Directory location of the cache. Required if streaming using HLS (such as on Apple devices) or DASH.", "cache_path_head": "Cache Path", "calculate_md5_and_ohash_desc": "Calculate MD5 checksum in addition to oshash. Enabling will cause initial scans to be slower. File naming hash must be set to oshash to disable MD5 calculation.", "calculate_md5_and_ohash_label": "Calculate MD5 for videos", @@ -263,12 +282,43 @@ "chrome_cdp_path_desc": "File path to the Chrome executable, or a remote address (starting with http:// or https://, for example http://localhost:9222/json/version) to a Chrome instance.", "create_galleries_from_folders_desc": "If true, creates galleries from folders containing images.", "create_galleries_from_folders_label": "Create galleries from folders containing images", + "database": "Database", "db_path_head": "Database Path", "directory_locations_to_your_content": "Directory locations to your content", "excluded_image_gallery_patterns_desc": "Regexps of image and gallery files/paths to exclude from Scan and add to Clean", "excluded_image_gallery_patterns_head": "Excluded Image/Gallery Patterns", "excluded_video_patterns_desc": "Regexps of video files/paths to exclude from Scan and add to Clean", "excluded_video_patterns_head": "Excluded Video Patterns", + "ffmpeg": { + "hardware_acceleration": { + "desc": "Uses available hardware to encode video for live transcoding.", + "heading": "FFmpeg hardware encoding" + }, + "live_transcode": { + "input_args": { + "desc": "Advanced: Additional arguments to pass to ffmpeg before the input field when live transcoding video.", + "heading": "FFmpeg Live Transcode Input Args" + }, + "output_args": { + "desc": "Advanced: Additional arguments to pass to ffmpeg before the output field when live transcoding video.", + "heading": "FFmpeg Live Transcode Output Args" + } + }, + "transcode": { + "input_args": { + "desc": "Advanced: Additional arguments to pass to ffmpeg before the input field when live transcoding video.", + "heading": "FFmpeg Live Transcode Input Args" + }, + "output_args": { + "desc": "Advanced: Additional arguments to pass to ffmpeg before the output field when generating video.", + "heading": "FFmpeg Transcode Output Args" + } + } + }, + "funscript_heatmap_draw_range": "Include range in generated heatmaps", + "funscript_heatmap_draw_range_desc": "Draw range of motion on the y-axis of the generated heatmap. Existing heatmaps will need to be regenerated after changing.", + "gallery_cover_regex_desc": "Regexp used to identify an image as gallery cover", + "gallery_cover_regex_label": "Gallery cover pattern", "gallery_ext_desc": "Comma-delimited list of file extensions that will be identified as gallery zip files.", "gallery_ext_head": "Gallery zip Extensions", "generated_file_naming_hash_desc": "Use MD5 or oshash for generated file naming. Changing this requires that all scenes have the applicable MD5/oshash value populated. After changing this value, existing generated files will need to be migrated or regenerated. See Tasks page for migration.", @@ -276,6 +326,7 @@ "generated_files_location": "Directory location for the generated files (scene markers, scene previews, sprites, etc)", "generated_path_head": "Generated Path", "hashing": "Hashing", + "heatmap_generation": "Funscript Heatmap Generation", "image_ext_desc": "Comma-delimited list of file extensions that will be identified as images.", "image_ext_head": "Image Extensions", "include_audio_desc": "Includes audio stream when generating previews.", @@ -304,7 +355,7 @@ "heading": "Scrapers Path" }, "scraping": "Scraping", - "sqlite_location": "File location for the SQLite database (requires restart)", + "sqlite_location": "File location for the SQLite database (requires restart). WARNING: storing the database on a different system to where the Stash server is run from (i.e. over the network) is unsupported!", "video_ext_desc": "Comma-delimited list of file extensions that will be identified as videos.", "video_ext_head": "Video Extensions", "video_head": "Video" @@ -346,6 +397,9 @@ }, "tasks": { "added_job_to_queue": "Added {operation_name} to job queue", + "anonymise_and_download": "Makes an anonymised copy of the database and downloads the resulting file.", + "anonymise_database": "Makes a copy of the database to the backups directory, anonymising all sensitive data. This can then be provided to others for troubleshooting and debugging purposes. The original database is not modified. Anonymised database uses the filename format {filename_format}.", + "anonymising_database": "Anonymising database", "auto_tag": { "auto_tagging_all_paths": "Auto Tagging all paths", "auto_tagging_paths": "Auto Tagging the following paths" @@ -372,6 +426,7 @@ "generate_previews_during_scan_tooltip": "Generate animated WebP previews, only required if Preview Type is set to Animated Image.", "generate_sprites_during_scan": "Generate scrubber sprites", "generate_thumbnails_during_scan": "Generate thumbnails for images", + "generate_video_covers_during_scan": "Generate scene covers", "generate_video_previews_during_scan": "Generate previews", "generate_video_previews_during_scan_tooltip": "Generate video previews which play when hovering over a scene", "generated_content": "Generated Content", @@ -399,7 +454,16 @@ "incremental_import": "Incremental import from a supplied export zip file.", "job_queue": "Task Queue", "maintenance": "Maintenance", + "migrate_blobs": { + "delete_old": "Delete old data", + "description": "Migrate blobs to the current blob storage system. This migration should be run after changing the blob storage system. Can optionally delete the old data after migration." + }, "migrate_hash_files": "Used after changing the Generated file naming hash to rename existing generated files to the new hash format.", + "migrate_scene_screenshots": { + "delete_files": "Delete screenshot files", + "description": "Migrate scene screenshots into the new blob storage system. This migration should be run after migrating an existing system to 0.20. Can optionally delete the old screenshots after migration.", + "overwrite_existing": "Overwrite existing blobs with screenshot data" + }, "migrations": "Migrations", "only_dry_run": "Only perform a dry run. Don't remove anything", "plugin_tasks": "Plugin Tasks", @@ -436,12 +500,12 @@ }, "basic_settings": "Basic Settings", "custom_css": { - "description": "Page must be reloaded for changes to take effect.", + "description": "Page must be reloaded for changes to take effect. There is no guarantee of compatibility between custom CSS and future releases of Stash.", "heading": "Custom CSS", "option_label": "Custom CSS enabled" }, "custom_javascript": { - "description": "Page must be reloaded for changes to take effect.", + "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", "option_label": "Custom Javascript enabled" }, @@ -470,24 +534,28 @@ "description": "Remove the ability to create new objects from the dropdown selectors", "heading": "Disable dropdown create" }, + "heading": "Editing", + "max_options_shown": { + "label": "Maximum number of items to show in select dropdowns" + }, "rating_system": { - "type": { - "label": "Rating System Type", - "options": { - "stars": "Stars", - "decimal": "Decimal" - } - }, "star_precision": { "label": "Rating Star Precision", "options": { "full": "Full", "half": "Half", - "quarter": "Quarter" + "quarter": "Quarter", + "tenth": "Tenth" + } + }, + "type": { + "label": "Rating System Type", + "options": { + "decimal": "Decimal", + "stars": "Stars" } } - }, - "heading": "Editing" + } }, "funscript_offset": { "description": "Time offset in milliseconds for interactive scripts playback.", @@ -510,6 +578,11 @@ "image_lightbox": { "heading": "Image Lightbox" }, + "image_wall": { + "direction": "Direction", + "heading": "Image Wall", + "margin": "Margin (pixels)" + }, "images": { "heading": "Images", "options": { @@ -533,7 +606,7 @@ }, "minimum_play_percent": { "description": "The percentage of time in which a scene must be played before its play count is incremented.", - "heading": "Minumum Play Percent" + "heading": "Minimum Play Percent" }, "performers": { "options": { @@ -616,7 +689,6 @@ } }, "configuration": "Configuration", - "resume_time": "Resume Time", "countables": { "files": "{count, plural, one {File} other {Files}}", "galleries": "{count, plural, one {Gallery} other {Galleries}}", @@ -654,6 +726,8 @@ }, "custom": "Custom", "date": "Date", + "date_format": "YYYY-MM-DD", + "datetime_format": "YYYY-MM-DD HH:MM", "death_date": "Death Date", "death_year": "Death Year", "descending": "Descending", @@ -662,7 +736,6 @@ "details": "Details", "developmentVersion": "Development Version", "dialogs": { - "aliases_must_be_unique": "aliases must be unique", "create_new_entity": "Create new {entity}", "delete_alert": "The following {count, plural, one {{singularEntity}} other {{pluralEntity}}} will be deleted permanently:", "delete_confirm": "Are you sure you want to delete {entityName}?", @@ -678,6 +751,14 @@ "edit_entity_title": "Edit {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "export_include_related_objects": "Include related objects in export", "export_title": "Export", + "imagewall": { + "direction": { + "column": "Column", + "description": "Column or row based layout.", + "row": "Row" + }, + "margin_desc": "Number of margin pixels around each entire image." + }, "lightbox": { "delay": "Delay (Sec)", "display_mode": { @@ -687,6 +768,7 @@ "original": "Original" }, "options": "Options", + "page_header": "Page {page} / {total}", "reset_zoom_on_nav": "Reset zoom level when changing image", "scale_up": { "description": "Scale smaller images up to fill screen", @@ -714,6 +796,7 @@ "destination": "Reassign to" }, "scene_gen": { + "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_previews": "Animated Image Previews", @@ -758,6 +841,7 @@ }, "dimensions": "Dimensions", "director": "Director", + "disambiguation": "Disambiguation", "display_mode": { "grid": "Grid", "list": "List", @@ -802,6 +886,11 @@ "warmth": "Warmth" }, "empty_server": "Add some scenes to your server to view recommendations on this page.", + "errors": { + "image_index_greater_than_zero": "Image index must be greater than 0", + "lazy_component_error_help": "If you recently upgraded Stash, please reload the page or clear your browser cache.", + "something_went_wrong": "Something went wrong." + }, "ethnicity": "Ethnicity", "existing_value": "existing value", "eye_color": "Eye Colour", @@ -849,6 +938,7 @@ "syncing": "Syncing with server", "uploading": "Uploading script" }, + "hasChapters": "Has Chapters", "hasMarkers": "Has Markers", "height": "Height", "height_cm": "Height (cm)", @@ -856,6 +946,7 @@ "ignore_auto_tag": "Ignore Auto Tag", "image": "Image", "image_count": "Image Count", + "image_index": "Image #", "images": "Images", "include_parent_tags": "Include parent tags", "include_sub_studios": "Include subsidiary studios", @@ -882,9 +973,9 @@ "age": "{age} {years_old}", "age_context": "{age} {years_old} in this scene" }, + "phash": "PHash", "play_count": "Play Count", "play_duration": "Play Duration", - "phash": "PHash", "stream": "Stream", "video_codec": "Video Codec" }, @@ -955,6 +1046,8 @@ }, "performers": "Performers", "piercings": "Piercings", + "play_count": "Play Count", + "play_duration": "Play Duration", "primary_file": "Primary file", "queue": "Queue", "random": "Random", @@ -963,19 +1056,20 @@ "recently_released_objects": "Recently Released {objects}", "release_notes": "Release Notes", "resolution": "Resolution", + "resume_time": "Resume Time", "scene": "Scene", - "scene_date": "Date of Scene", - "scene_created_at": "Scene Created At", - "scene_updated_at": "Scene Updated At", "sceneTagger": "Scene Tagger", "sceneTags": "Scene Tags", "scene_code": "Studio Code", "scene_count": "Scene Count", + "scene_created_at": "Scene Created At", + "scene_date": "Date of Scene", "scene_id": "Scene ID", + "scene_updated_at": "Scene Updated At", "scenes": "Scenes", "scenes_updated_at": "Scene Updated At", "search_filter": { - "add_filter": "Add Filter", + "edit_filter": "Edit Filter", "name": "Filter", "saved_filters": "Saved filters", "update_filter": "Update Filter" @@ -985,8 +1079,12 @@ "setup": { "confirm": { "almost_ready": "We're almost ready to complete the configuration. Please confirm the following settings. You can click back to change anything incorrect. If everything looks good, click Confirm to create your system.", + "blobs_directory": "Binary data directory", + "cache_directory": "Cache directory", "configuration_file_location": "Configuration file location:", "database_file_path": "Database file path", + "default_blobs_location": "", + "default_cache_location": "/cache", "default_db_location": "/stash-go.sqlite", "default_generated_content_location": "/generated", "generated_directory": "Generated directory", @@ -1022,12 +1120,20 @@ }, "paths": { "database_filename_empty_for_default": "database filename (empty for default)", - "description": "Next up, we need to determine where to find your porn collection, where to store the stash database and generated files. These settings can be changed later if needed.", + "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.", + "path_to_blobs_directory_empty_for_database": "path to blobs directory (empty to use database)", + "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", "stash_alert": "No library paths have been selected. No media will be able to be scanned into Stash. Are you sure?", + "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. 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, if you want to store this data in the database, you can leave this field blank. 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 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_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?", @@ -1047,7 +1153,7 @@ "your_system_has_been_created": "Success! Your system has been created!" }, "welcome": { - "config_path_logic_explained": "Stash tries to find its configuration file (config.yml) from the current working directory first, and if it does not find it there, it falls back to $HOME/.stash/config.yml (on Windows, this will be %USERPROFILE%\\.stash\\config.yml). You can also make Stash read from a specific configuration file by running it with the -c or --config options.", + "config_path_logic_explained": "Stash tries to find its configuration file (config.yml) from the current working directory first, and if it does not find it there, it falls back to $HOME/.stash/config.yml (on Windows, this will be %USERPROFILE%\\.stash\\config.yml). You can also make Stash read from a specific configuration file by running it with the -c '' or --config '' options.", "in_current_stash_directory": "In the $HOME/.stash directory", "in_the_current_working_directory": "In the current working directory", "next_step": "With all of that out of the way, if you're ready to proceed with setting up a new system, choose where you'd like to store your configuration file and click Next.", @@ -1063,6 +1169,7 @@ "welcome_to_stash": "Welcome to Stash" }, "stash_id": "Stash ID", + "stash_id_endpoint": "Stash ID Endpoint", "stash_ids": "Stash IDs", "stashbox": { "go_review_draft": "Go to {endpoint_name} to review draft.", @@ -1072,7 +1179,6 @@ "submit_update": "Already exists in {endpoint_name}" }, "statistics": "Statistics", - "stash_id_endpoint": "Stash ID Endpoint", "stats": { "image_size": "Images size", "scenes_duration": "Scenes duration", @@ -1099,6 +1205,7 @@ "default_filter_set": "Default filter set", "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_scenes": "Merged scenes", "merged_tags": "Merged tags", "reassign_past_tense": "File reassigned", @@ -1116,10 +1223,13 @@ "type": "Type", "updated_at": "Updated At", "url": "URL", + "validation": { + "aliases_must_be_unique": "aliases must be unique", + "date_invalid_form": "${path} must be in YYYY-MM-DD form", + "required": "${path} is a required field" + }, "videos": "Videos", "view_all": "View All", - "play_count": "Play Count", - "play_duration": "Play Duration", "weight": "Weight", "weight_kg": "Weight (kg)", "years_old": "years old", diff --git a/ui/v2.5/src/locales/en-US.json b/ui/v2.5/src/locales/en-US.json index 2d8240a4f..04883f94f 100644 --- a/ui/v2.5/src/locales/en-US.json +++ b/ui/v2.5/src/locales/en-US.json @@ -1,4 +1,7 @@ { + "actions": { + "customise": "Customize" + }, "eye_color": "Eye Color", "favourite": "Favorite", "hair_color": "Hair Color", diff --git a/ui/v2.5/src/locales/es-ES.json b/ui/v2.5/src/locales/es-ES.json index 2d797b242..2f115be11 100644 --- a/ui/v2.5/src/locales/es-ES.json +++ b/ui/v2.5/src/locales/es-ES.json @@ -35,7 +35,7 @@ "download_backup": "Descargar copia de seguridad", "edit": "Editar", "edit_entity": "Editar {entityType}", - "export": "Exportar…", + "export": "Exportar", "export_all": "Exportar todo…", "find": "Buscar", "finish": "Terminar", @@ -66,6 +66,7 @@ "play_selected": "Reproducir seleccionada", "preview": "Vista previa", "previous_action": "Atrás", + "reassign": "Reasignar", "refresh": "Actualizar", "reload_plugins": "Recargar plugins", "reload_scrapers": "Recargar rastreadores", @@ -98,10 +99,12 @@ "show": "Mostrar", "show_configuration": "Mostrar configuración", "skip": "Saltar", + "split": "Dividir", "stop": "Parar", "submit": "Enviar", "submit_stash_box": "Enviar a Stash-Box", "submit_update": "Enviar Actualización", + "swap": "Intercambia", "tasks": { "clean_confirm_message": "¿Estás seguro que quieres iniciar la limpieza? Esto eliminará la información en la base de datos, y el contenido generado para todas las escenas y galerías que ya no estén disponibles en el sistema de ficheros.", "dry_mode_selected": "Modo de simulación seleccionado. No se eliminará información, solo se guardarán registros de las acciones a realizar.", @@ -120,6 +123,7 @@ "also_known_as": "También conocida como", "ascending": "Ascendente", "average_resolution": "Resolución media", + "between_and": "y", "birth_year": "Año de nacimiento", "birthdate": "Cumpleaños", "bitrate": "Tasa de bits", @@ -189,6 +193,7 @@ }, "categories": { "about": "Acerca de", + "changelog": "Registro de cambios", "interface": "Diseño", "logs": "Registros", "metadata_providers": "Proveedores de Metadatos", @@ -243,6 +248,10 @@ "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" }, + "backup_directory_path": { + "description": "Ubicación del directorio para copias de seguridad de archivos de bases de datos SQLite", + "heading": "Ruta del directorio de la copia de seguridad" + }, "cache_location": "Ruta relativa del directorio donde se almacenarán los ficheros de la caché", "cache_path_head": "Ruta relative para la caché", "calculate_md5_and_ohash_desc": "Calcular comprobación MD5 en adición a oshash. Habilitar esta opción puede provocar que los escaneos iniciales resulten más lentos. El cálculo de hash del nombre del fichero debe establecerse en oshash para deshabilitar el cálculo MD5.", @@ -420,12 +429,21 @@ "scene_tools": "Herramientas de escenas" }, "ui": { + "abbreviate_counters": { + "description": "Abrevie los contadores en las tarjetas y en las páginas de vista de detalles, por ejemplo, \"1831\" tendrá el formato \"1.8K\".", + "heading": "Abreviar contadores" + }, "basic_settings": "Ajustes básicos", "custom_css": { "description": "La página debe ser recargada para que se lleven a cabo los cambios realizados.", "heading": "CSS personalizado", "option_label": "Habilitar CSS personalizado" }, + "custom_javascript": { + "description": "La página debe ser refrescada para que los cambios tomen efecto.", + "heading": "JavaScript personalizada", + "option_label": "JavaScript personalizada activada" + }, "delete_options": { "description": "Opciones por defecto al borrar escenas, imágenes y galerías.", "heading": "Opciones de eliminación", @@ -446,7 +464,15 @@ "description": "Eliminar la capacidad de crear nuevos recursos desde los selectores de formulario desplegables", "heading": "Deshabilitar creación en menú desplegable" }, - "heading": "Edición" + "heading": "Edición", + "rating_system": { + "type": { + "options": { + "decimal": "Decimal", + "stars": "Estrellas" + } + } + } }, "funscript_offset": { "description": "Tiempo de compensación en milisegundos para la reproducción de scripts interactivos.", @@ -581,7 +607,6 @@ "details": "Detalles", "developmentVersion": "Versión de desarrollo", "dialogs": { - "aliases_must_be_unique": "Los alias deben ser únicos", "delete_alert": "El/los siguiente/s {count, plural, one {{singularEntity}} other {{pluralEntity}}} se eliminará/n de forma permanente:", "delete_confirm": "¿Estás seguro que deseas eliminar {entityName}?", "delete_entity_desc": "{count, plural, one {¿Estás seguro que deseas eliminar esta {singularEntity}? Hasta que el archivo sea eliminado también, esta {singularEntity} se volverá a añadir cuando se lleve a cabo un escaneo.} other {¿Estás seguro que deseas eliminar {pluralEntity}? Hasta que los archivos sean eliminados del sistema de ficheros también, estas {pluralEntity} se volverán a añadir cuando se lleve a cabo un escaneo.}}", @@ -858,7 +883,6 @@ "scenes": "Escenas", "scenes_updated_at": "Fecha de actualización de la escena", "search_filter": { - "add_filter": "Añadir filtro", "name": "Filtro", "saved_filters": "Filtros guardados", "update_filter": "Actualizar filtro" @@ -929,7 +953,7 @@ "your_system_has_been_created": "¡Todo correcto! ¡Tu entorno ha sido creado!" }, "welcome": { - "config_path_logic_explained": "Stash intenta encontrar primero su archivo de configuración (config.yml) en el directorio actual de trabajo y, en caso de no encontrarlo, lo intenta en $HOME/.stash/config.yml (en Windows será %USERPROFILE%\\.stash\\config.yml). También puedes hacer que Stash obtenga su configuración de un archivo de configuración lanzando la aplicación con las opciones -c o --config .", + "config_path_logic_explained": "Stash intenta encontrar primero su archivo de configuración (config.yml) en el directorio actual de trabajo y, en caso de no encontrarlo, lo intenta en $HOME/.stash/config.yml (en Windows será %USERPROFILE%\\.stash\\config.yml). También puedes hacer que Stash obtenga su configuración de un archivo de configuración lanzando la aplicación con las opciones -c '' o --config ''.", "in_current_stash_directory": "En el directorio $HOME/.stash", "in_the_current_working_directory": "En el directorio de trabajo actual", "next_step": "Si estás listo para crear un nuevo entorno, por favor, selecciona donde quieres guardar tu archivo de configuración y haz clic en siguiente.", @@ -991,6 +1015,9 @@ "twitter": "Twitter", "updated_at": "Fecha de modificación", "url": "URL", + "validation": { + "aliases_must_be_unique": "Los alias deben ser únicos" + }, "videos": "Vídeos", "weight": "Peso", "years_old": "años" diff --git a/ui/v2.5/src/locales/et-EE.json b/ui/v2.5/src/locales/et-EE.json index db8c48734..c3a010350 100644 --- a/ui/v2.5/src/locales/et-EE.json +++ b/ui/v2.5/src/locales/et-EE.json @@ -6,6 +6,7 @@ "add_to_entity": "Lisa {entityType}-sse", "allow": "Luba", "allow_temporarily": "Luba ajutiselt", + "anonymise": "Anonüümseks Muutmine", "apply": "Rakenda", "auto_tag": "Märgi Automaatselt", "backup": "Varunda", @@ -20,6 +21,7 @@ "confirm": "Kinnita", "continue": "Jätka", "create": "Loo", + "create_chapters": "Loo Peatükk", "create_entity": "Loo {entityType}", "create_marker": "Loo Marker", "created_entity": "Loodud {entity_type}: {entity_name}", @@ -32,10 +34,11 @@ "delete_stashid": "Kustuta StashID", "disallow": "Keela", "download": "Lae alla", + "download_anonymised": "Lae alla anonümiseeritult", "download_backup": "Lae varundus alla", "edit": "Muuda", "edit_entity": "Muuda {entityType}", - "export": "Ekspordi…", + "export": "Ekspordi", "export_all": "Ekspordi kõik…", "find": "Otsi", "finish": "Lõpeta", @@ -58,6 +61,8 @@ "merge": "Liida", "merge_from": "Liida teisest", "merge_into": "Liida teise", + "migrate_blobs": "Migreeri Blobid", + "migrate_scene_screenshots": "Migreeri Stseenide Ekraanipildid", "next_action": "Järgmine", "not_running": "ei jookse", "open_in_external_player": "Ava välises mängijas", @@ -128,8 +133,13 @@ "birth_year": "Sünniaasta", "birthdate": "Sünnikuupäev", "bitrate": "Bitikiirus", + "blobs_storage_type": { + "database": "Andmebaas", + "filesystem": "Failisüsteem" + }, "captions": "Subtiitrid", "career_length": "Karjääri Pikkus", + "chapters": "Peatükid", "component_tagger": { "config": { "active_instance": "Aktiivne stash-kasti eksemplar:", @@ -160,7 +170,7 @@ "duration_unknown": "Kestus teadmata", "fp_found": "{fpCount, plural, =0 {Uusi sõrmejälje kattuvusi ei leitud} other {# uut sõrmejälje kattuvust leitud}}", "fp_matches": "Kestus klapib", - "fp_matches_multi": "Kestus klapib {matchCount}/{durationsLength} sõrmejäljel", + "fp_matches_multi": "Kestus klapib {matchCount}/{durationsLength} sõrmejälgedel", "hash_matches": "{hash_type} klapib", "match_failed_already_tagged": "Stseen juba sildistatud", "match_failed_no_result": "Vasteid ei leitud", @@ -183,6 +193,7 @@ "latest_version": "Uusim Versioon", "latest_version_build_hash": "Uusima Versiooni Ehituse Hash:", "new_version_notice": "[UUS]", + "release_date": "Väljalaskekuupäev:", "stash_discord": "Liitu meie {url}i kanaliga", "stash_home": "Stashi kodu {url}-is", "stash_open_collective": "Toeta meid läbi {url}-i", @@ -253,7 +264,15 @@ "description": "Failitee SQLite andmebaasi varundusfailide jaoks", "heading": "Varunduse Failitee" }, - "cache_location": "Failitee vahemäluni", + "blobs_path": { + "description": "Kus kohas hoida binaarseid andmeid failisüsteemis. Kehtib ainult kui kasutad Failisüsteem blob salvestustüüpi. HOIATUS: selle muutmine nõuab olemasolevate andmete manuaalset liigutamist.", + "heading": "Binaarseete andmete failisüsteemi tee" + }, + "blobs_storage": { + "description": "Kus hoida binaarseid andmeid nagu stseeni kaanepildid, näitlejate, stuudiote ja siltide pilte. Peale selle väärtuse muutmist tuleb olemasolevad andmed migreerida kasutades Migreeri Blobe ülesannet. Vaata Ülesannete lehele migreerimiseks.", + "heading": "Binaarsete andmete hoiustamistüüp" + }, + "cache_location": "Failitee vahemäluni. Nõutud kui striimimiseks kasutatakse HLSi (näiteks Apple seadetel) või DASHi.", "cache_path_head": "Vahemälu Failitee", "calculate_md5_and_ohash_desc": "Kalkuleeri MD5 checksum lisaks oshashile. Lubamine põhjustab aeglasemat esmast skaneerimist. Faili nimetuse hash peab olema sätitud oshashiks, et keelata MD5 kalkuleerimine.", "calculate_md5_and_ohash_label": "Kalkuleeri MD5 videote jaoks", @@ -263,12 +282,43 @@ "chrome_cdp_path_desc": "Failitee Chrome käivitajani, või kaugaadress (algab http:// või https:// -iga, näiteks http://localhost:9222/json/version) Chrome'i eksemplarini.", "create_galleries_from_folders_desc": "Kui lubatud, loob galeriisid pilte sisaldavatest kaustadest.", "create_galleries_from_folders_label": "Loo galeriisid kaustadest, mis sisaldavad pilte", + "database": "Andmebaas", "db_path_head": "Andmebaasi Failitee", "directory_locations_to_your_content": "Failitee asukohad sisule", "excluded_image_gallery_patterns_desc": "Pildi- ja galeriifailide/teede regexpid, mida skannimisest välja jätta ja Clean'i lisada", "excluded_image_gallery_patterns_head": "Välistatud Pildi/Galerii Mustrid", "excluded_video_patterns_desc": "Videofailide/teede regexpid, mida skannimisest välja jätta ja Clean'i lisada", "excluded_video_patterns_head": "Välistatud Video Mustrid", + "ffmpeg": { + "hardware_acceleration": { + "desc": "Kasutab olemasolevat riistvara reaalajas video transkodeerimiseks.", + "heading": "FFmpeg riistvara enkodeerimine" + }, + "live_transcode": { + "input_args": { + "desc": "Edasijõudnutele: Lisaargumendid mida edastada ffmpegi sisendväljale live video transkodeerimise ajal.", + "heading": "FFmpeg Live Transkodeerimise Sisendargumendid" + }, + "output_args": { + "desc": "Edasijõudnutele: Lisaargumendid mida edastada ffmpegi väljundväljale live video transkodeerimise ajal.", + "heading": "FFmpeg Live Transkodeerimise Väljundargumendid" + } + }, + "transcode": { + "input_args": { + "desc": "Edasijõudnutele: Lisaargumendid mida edastada ffmpegi sisendväljale video reaalajas transkodeerimisel.", + "heading": "FFmpeg Reaalajas Transkodeerimise Sisendargumendid" + }, + "output_args": { + "desc": "Edasijõudnutele: Lisaargumendid mida edastada ffmpegi väljundväljale video genereerimisel.", + "heading": "FFmpeg Transkodeerimise Väljundargumendid" + } + } + }, + "funscript_heatmap_draw_range": "Kaasa vahemik genereeritud kuumkaartidel", + "funscript_heatmap_draw_range_desc": "Joonista liikumisvahemik genereeritud kuumkaardi y-teljel. Olemasolevad kuumkaardid tuleb peale muutmist uuesti genereerida.", + "gallery_cover_regex_desc": "Regexp kasutakse, et tuvastada pilti kui galerii kaanepildina", + "gallery_cover_regex_label": "Galerii kaanepildi muster", "gallery_ext_desc": "Komadega eraldatud faililaiendite loend, mis tuvastatakse galerii ZIP-failidena.", "gallery_ext_head": "Galerii zip Laiendused", "generated_file_naming_hash_desc": "Kasutage failide nimetamiseks MD5 või oshashi. Selle muutmiseks on vaja, et kõikides stseenides oleks kohaldatav MD5/oshash väärtus täidetud. Pärast selle väärtuse muutmist tuleb olemasolevad loodud failid migreerida või uuesti genereerida. Vaadake üleviimise kohta lehekülge Ülesanded.", @@ -276,6 +326,7 @@ "generated_files_location": "Loodud failide (stseenimarkerid, stseeni eelvaated, spraidid jne) asukoht Failiteel", "generated_path_head": "Genereeritud Failitee", "hashing": "Hashimine", + "heatmap_generation": "Funscripti Kuumkaardi Genereerimine", "image_ext_desc": "Komadega eraldatud faililaiendite loend, mis tuvastatakse piltidena.", "image_ext_head": "Pildilaiendused", "include_audio_desc": "Kaasa eelvaadete loomisel helivoog.", @@ -304,7 +355,7 @@ "heading": "Kraapijate Failitee" }, "scraping": "Kraapimine", - "sqlite_location": "Failitee asukoht SQLite andmebaasi jaoks (vajab taaskäivitust)", + "sqlite_location": "Failitee asukoht SQLite andmebaasi jaoks (vajab taaskäivitust). HOIATUS: andmebaasi kasutamine, mis ei jookse samal süsteemil, kui Stash (nt üle võrgu) ei ole toetatud!", "video_ext_desc": "Komadega eraldatud faililaiendite loend, mis tuvastatakse videotena.", "video_ext_head": "Videolaiendused", "video_head": "Video" @@ -346,6 +397,9 @@ }, "tasks": { "added_job_to_queue": "{operation_name} lisatud tööde järjekorda", + "anonymise_and_download": "Loob anonüümse koopia andmebaasist ja laeb väljundfaili alla.", + "anonymise_database": "Loob andmebaasist tagavarakoopia tagavara koopiate kausta, muudab tundliku teabe anonüümseks. Seda saab jagada teistega abistamise ja probleemide analüüsimise eesmärgil. Originaalset andmebaasi ei muudeta. Anonüümne andmebaas kasutab failinime formaati {filename_format}.", + "anonymising_database": "Muudan andmebaasi anonüümseks", "auto_tag": { "auto_tagging_all_paths": "Automaatne Märkimine kõikidel failiteedel", "auto_tagging_paths": "Automaatne Märkimine järgnevatel failiteedel" @@ -372,6 +426,7 @@ "generate_previews_during_scan_tooltip": "Genereeri animeeritud WebP eelvaateid, nõutav ainult siis, kui eelvaate tüüp on seatud väärtusele Animeeritud Pilt.", "generate_sprites_during_scan": "Genereeri puhastusspriite", "generate_thumbnails_during_scan": "Genereeri piltide jaoks pisipilte", + "generate_video_covers_during_scan": "Genereeri stseeni kaanepidid", "generate_video_previews_during_scan": "Genereeri eelvaateid", "generate_video_previews_during_scan_tooltip": "Genereeri video eelvaateid, mis esitatakse kursorit stseeni kohal hoides", "generated_content": "Genereeritud Sisu", @@ -399,7 +454,16 @@ "incremental_import": "Järkjärguline import esitatud eksporditud ZIP-failist.", "job_queue": "Ülesannete Järjekord", "maintenance": "Hooldus", + "migrate_blobs": { + "delete_old": "Kustuta vanad andmed", + "description": "Migreeri blobid praegusele blobi andmesüsteemile. Seeda migratsiooni tuleb jooksutada peale blobi andmesalvestussüsteemi muutmist. Saab valikuliselt kustutada vanad andmed peale migratsiooni." + }, "migrate_hash_files": "Kasutatakse pärast genereeritud faili nimetamise hashi muutmist olemasolevate loodud failide ümbernimetamiseks uuele hashivormingule.", + "migrate_scene_screenshots": { + "delete_files": "Kustuta ekraanipiltide failid", + "description": "Migreeri stseeni ekranipildid uute blobi andmesüsteemi. Seda migratsuiooni peaks jooksutama peale olemasoleva süsteemi migreerimist 0.20le. Saab valikuselt kustutada vanu ekraanipilte peale migratsiooni.", + "overwrite_existing": "Kirjuta üle eksisteerivad blobid koos ekraanipiltide andmetega" + }, "migrations": "Migreerimised", "only_dry_run": "Tee ainult kuivjooks. Ära eemalda midagi", "plugin_tasks": "Plugina Ülesanded", @@ -436,12 +500,12 @@ }, "basic_settings": "Põhisätted", "custom_css": { - "description": "Muudatuste jõustumiseks tuleb leht uuesti laadida.", + "description": "Muudatuste jõustumiseks tuleb leht uuesti laadida. Pole garantiid, et kohandatud CSS töötab ka tuleviku Stashi uuendustes.", "heading": "Kohandatud CSS", "option_label": "Kohandatud CSS lubatud" }, "custom_javascript": { - "description": "Pead muudatuste nägemiseks lehe uuesti laadima.", + "description": "Pead muudatuste nägemiseks lehe uuesti laadima. Pole garantiid, et kohandatud Javascript töötab ka tuleviku Stashi uuendustes.", "heading": "Kohandatud Javascript", "option_label": "Kohandatud Javascript lubatud" }, @@ -471,13 +535,17 @@ "heading": "Keela rippmenüü loomine" }, "heading": "Redigeerimine", + "max_options_shown": { + "label": "Maksimaalne number esemeid mida näidata valikmenüüdes" + }, "rating_system": { "star_precision": { "label": "Tähtedega Hindamise Täpsus", "options": { "full": "Täis", "half": "Pool", - "quarter": "Neljandik" + "quarter": "Neljandik", + "tenth": "Kümme" } }, "type": { @@ -510,6 +578,11 @@ "image_lightbox": { "heading": "Pildi Valguskast" }, + "image_wall": { + "direction": "Direktsioon", + "heading": "Pildisein", + "margin": "Marginaalid (pikslid)" + }, "images": { "heading": "Pildid", "options": { @@ -653,6 +726,8 @@ }, "custom": "Kohandatud", "date": "Kuupäev", + "date_format": "AAAA-KK-PP", + "datetime_format": "AAAA-KK-PP TT:MM", "death_date": "Surmakuupäev", "death_year": "Surma-aasta", "descending": "Langev", @@ -661,7 +736,6 @@ "details": "Detailid", "developmentVersion": "Arendusversioon", "dialogs": { - "aliases_must_be_unique": "aliased peavad olema erilised", "create_new_entity": "Loo uus {entity}", "delete_alert": "Järgnev {count, plural, one {{singularEntity}} other {{pluralEntity}}} kustutatakse lõplikult:", "delete_confirm": "Kas oled kindel, et soovid kustutada {entityName}?", @@ -677,6 +751,14 @@ "edit_entity_title": "Redigeeri {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "export_include_related_objects": "Kaasa seotud objektid eksporti", "export_title": "Ekspordi", + "imagewall": { + "direction": { + "column": "Kolumn", + "description": "Kolumni või ridade põhine välimus.", + "row": "Rida" + }, + "margin_desc": "Marginaali pikslite arv ümber iga täispildi." + }, "lightbox": { "delay": "Viivitus (s)", "display_mode": { @@ -686,6 +768,7 @@ "original": "Originaal" }, "options": "Sätted", + "page_header": "Leht {page} / {total}", "reset_zoom_on_nav": "Pildi muutumisel lähtesta suumi tase", "scale_up": { "description": "Suurendage väiksemaid pilte ekraani täitmiseks", @@ -713,6 +796,7 @@ "destination": "Määra Ümber" }, "scene_gen": { + "covers": "Stseeni kaaned", "force_transcodes": "Sunni Ümbertöötlemise genereerimine", "force_transcodes_tooltip": "Vaikimisi genereeritakse ümbertöötlemisi ainult siis, kui brauser videofaili ei toeta. Kui see on lubatud, genereeritakse ümbertöötlemisi isegi siis, kui videofail näib olevat brauseris toetatud.", "image_previews": "Animeeritud Piltide Eelvaated", @@ -757,6 +841,7 @@ }, "dimensions": "Dimensioonid", "director": "Režissöör", + "disambiguation": "Ühesõnastamine", "display_mode": { "grid": "Võrgustik", "list": "Nimekiri", @@ -801,6 +886,11 @@ "warmth": "Soojus" }, "empty_server": "Sellel lehel soovituste nägemiseks lisage oma serverisse mõned stseenid.", + "errors": { + "image_index_greater_than_zero": "Pildi indeks peab olema suurem kui 0", + "lazy_component_error_help": "Kui uuendasid Stashi hiljuti, palun taaslae leht või tühjenda oma brauseri cache.", + "something_went_wrong": "Midagi läks valesti." + }, "ethnicity": "Rahvus", "existing_value": "eksisteeriv väärtus", "eye_color": "Silmavärv", @@ -848,6 +938,7 @@ "syncing": "Serveriga sünkroniseerimine", "uploading": "Skripti üleslaadimine" }, + "hasChapters": "Sisaldab Episoode", "hasMarkers": "On Markereid", "height": "Pikkus", "height_cm": "Pikkus (cm)", @@ -855,6 +946,7 @@ "ignore_auto_tag": "Ignoneeri Automaatset Märkimist", "image": "Pilt", "image_count": "Pildiarv", + "image_index": "Pilt #", "images": "Pildid", "include_parent_tags": "Kaasa vanem-silte", "include_sub_studios": "Kaasa tütarstuudioid", @@ -977,7 +1069,7 @@ "scenes": "Stseenid", "scenes_updated_at": "Stseen Uuendatud", "search_filter": { - "add_filter": "Lisa Filter", + "edit_filter": "Muuda Filtrit", "name": "Filter", "saved_filters": "Salvestatud filtrid", "update_filter": "Uuenda Filtrit" @@ -987,8 +1079,12 @@ "setup": { "confirm": { "almost_ready": "Oleme seadistamise lõpuleviimiseks peaaegu valmis. Vaata üle järgmised sätted. Valede väärtuste muutmiseks võite klõpsata tagasi. Kui kõik tundub õige, klõpsa süsteemi loomiseks nuppu Kinnita.", + "blobs_directory": "Binaarsete andmete kaust", + "cache_directory": "Cache kaust", "configuration_file_location": "Konfiguratsioonifaili asukoht:", "database_file_path": "Andmebaasi faili failitee", + "default_blobs_location": "", + "default_cache_location": "/cache", "default_db_location": "/stash-go.sqlite", "default_generated_content_location": "/generated", "generated_directory": "Genereeritud kaust", @@ -1024,12 +1120,20 @@ }, "paths": { "database_filename_empty_for_default": "andmebaasi failinimi (vaikimisi tühi)", - "description": "Järgmisena peame kindlaks määrama, kust leida su pornokogu, kuhu salvestada stashi andmebaas ja loodud failid. Neid sätteid saab hiljem vajadusel muuta.", + "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.", + "path_to_blobs_directory_empty_for_database": "tee blobide kaustani (tühi, et kasutada andmebaasi)", + "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", "stash_alert": "Ühtegi kogu teed pole valitud. Stash ei saa skannida mitte ühtegi meediumifaili. Oled sa kindel?", + "where_can_stash_store_blobs": "Kus saab Stash hoida oma andmebaasi binaarseid andmeid?", + "where_can_stash_store_blobs_description": "Stash saab hoida binaarseid andmeid nagu stseeni kaanepilte, esinejate, stuudiote ja siltide pilte kas andmebaasis või failisüsteemis. Vaikimisi salvestab Stash neid andmeid failsüsteemi alamkausta blobs. Kui tahad seda muuta, palun sisesta absoluutne või relatiivne (hetke töökaustaga) tee. Stash loob selle kausta, kui seda juba ei eksisteeri.", + "where_can_stash_store_blobs_description_addendum": "Alternatiivselt, kui tahad hoida neid anmeid andmebaasis, saad jätta selle välja tühjaks. NB: See suurendab su andmebaasi fail ja suurendab andmebaasi migratsiooni aega.", + "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 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_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?", @@ -1049,7 +1153,7 @@ "your_system_has_been_created": "Edukas! Su süsteem on loodud!" }, "welcome": { - "config_path_logic_explained": "Stash püüab esmalt leida oma konfiguratsioonifaili (config.yml) praegusest töökataloogist ja kui ta seda sealt ei leia, läheb tagasi kausta $HOME/.stash/config. yml (Windowsis on see %USERPROFILE%\\.stash\\config.yml). Samuti saad panna Stashi lugema konkreetsest konfiguratsioonifailist, käivitades selle suvanditega -c või --config .", + "config_path_logic_explained": "Stash püüab esmalt leida oma konfiguratsioonifaili (config.yml) praegusest töökataloogist ja kui ta seda sealt ei leia, läheb tagasi kausta $HOME/.stash/config. yml (Windowsis on see %USERPROFILE%\\.stash\\config.yml). Samuti saad panna Stashi lugema konkreetsest konfiguratsioonifailist, käivitades selle suvanditega -c '' või --config ''.", "in_current_stash_directory": "Kaustas $HOME/.stash", "in_the_current_working_directory": "Praeguses töökaustas", "next_step": "Kui oled valmis uue süsteemi seadistamisega alustama, vali, kuhu soovid oma konfiguratsioonifaili salvestada, ja klõpsa nuppu Edasi.", @@ -1101,6 +1205,7 @@ "default_filter_set": "Vaikimisi filtrikomplekt", "delete_past_tense": "Kustutatud {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "generating_screenshot": "Ekraanipildi genereerimine…", + "image_index_too_large": "Error: Pildi index on suurem kui Galeriis olevate piltide arv", "merged_scenes": "Ühendatud stseenid", "merged_tags": "Sildid ühendatud", "reassign_past_tense": "Fail ümbermääratud", @@ -1118,6 +1223,11 @@ "type": "Tüüp", "updated_at": "Viimati Uuendatud", "url": "URL", + "validation": { + "aliases_must_be_unique": "aliased peavad olema erilised", + "date_invalid_form": "${path} peab olema AAAA-KK-PP vormis", + "required": "${path} on nõutud väli" + }, "videos": "Videod", "view_all": "Vaata Kõiki", "weight": "Kaal", diff --git a/ui/v2.5/src/locales/fi-FI.json b/ui/v2.5/src/locales/fi-FI.json index 002c365ca..a7826e0e3 100644 --- a/ui/v2.5/src/locales/fi-FI.json +++ b/ui/v2.5/src/locales/fi-FI.json @@ -6,6 +6,7 @@ "add_to_entity": "Lisää kohteeseen {entityType}", "allow": "Salli", "allow_temporarily": "Salli väliaikaisesti", + "anonymise": "Anonymisoi", "apply": "Käytä", "auto_tag": "Lisää tunnisteet automaattisesti", "backup": "Varmuuskopioi", @@ -32,10 +33,11 @@ "delete_stashid": "Poista StashID", "disallow": "Kiellä", "download": "Lataa", + "download_anonymised": "Lataa anonymisoitu", "download_backup": "Lataa varmuuskopio", "edit": "Muokkaa", "edit_entity": "Muokkaa {entityType}", - "export": "Vie…", + "export": "Vie", "export_all": "Vie kaikki…", "find": "Etsi", "finish": "Valmis", @@ -183,6 +185,7 @@ "latest_version": "Viimeisin Versio", "latest_version_build_hash": "Uusimman version tiiviste:", "new_version_notice": "[UUSI]", + "release_date": "Julkaisupäivä:", "stash_discord": "Liity {url} kanavallemme", "stash_home": "Stashin kotisivut {url}", "stash_open_collective": "Tue meitä {url}", @@ -340,6 +343,7 @@ }, "tasks": { "added_job_to_queue": "Lisättiin {operation_name} tehtäväjonoon", + "anonymise_and_download": "Tekee anonymisoidun kopion tietokannasta ja lataa sen.", "auto_tag": { "auto_tagging_all_paths": "Asetetaan tunnisteet automaattisesti kaikkiin polkuihin", "auto_tagging_paths": "Asetetaan tunnisteet automaattisesti seuraaviin polkuihin" @@ -348,7 +352,7 @@ "auto_tagging": "Automaattinen tunnisteiden asetus", "backing_up_database": "Varmuuskopioidaan tietokantaa", "backup_and_download": "Suorittaa tietokannan varmuuskopioinnin ja lataa luodun tiedoston.", - "backup_database": "Suorittaa tietokannan varmuuskopioinnin ja tallentaa {filename_format} -tiedoston samaan kansioon kuin tietokanta", + "backup_database": "Suorittaa tietokannan varmuuskopioinnin ja tallentaa {filename_format} -tiedoston varmuuskopiokansioon", "cleanup_desc": "Tarkistaa puuttuvat tiedostot ja poistaa ne tietokannasta. Tämä on tuhoava toimi.", "data_management": "Datan hallinta", "defaults_set": "Oletukset on asetettu ja niitä käytetään kun painat {action} -painiketta Tehtävät -sivulla.", @@ -611,7 +615,6 @@ "details": "Lisätiedot", "developmentVersion": "Kehitysversio", "dialogs": { - "aliases_must_be_unique": "Aliaksien pitää olla uniikkeja", "create_new_entity": "Luo uus {entity}", "delete_alert": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} poistetaan pysyvästi:", "delete_confirm": "Haluatko varmasti poistaa {entityName}?", @@ -643,7 +646,7 @@ }, "scroll_mode": { "label": "Vieritystila", - "pan_y": "Pan Y", + "pan_y": "Panoroi Y", "zoom": "Zoomaus" } }, @@ -900,7 +903,6 @@ "scenes": "Kohtaukset", "scenes_updated_at": "Kohtaus päivitetty", "search_filter": { - "add_filter": "Aseta suodatin", "name": "Suodatin", "saved_filters": "Tallennetut suodattimet", "update_filter": "Päivitä suodatin" @@ -942,7 +944,7 @@ "migration_irreversible_warning": "Migraatio ei ole peruutettavissa. Kun migraatio on suoritettu, tietokanta ei ole enää yhteensopiva stashin vanhempien versioiden kanssa.", "migration_required": "Migraatio vaaditaan", "perform_schema_migration": "Suorita migraatio", - "schema_too_old": "Tämänhetkinen stash -tietokannan muodon versio on {databaseSchema} ja se pitää muuttaa versioon {appSchema}. Tämä versio Stashista ei toimi ilman tietokannan migraatiota." + "schema_too_old": "Tämänhetkinen stash -tietokannan muodon versio on {databaseSchema} ja se pitää muuttaa versioon {appSchema}. Tämä versio Stashista ei toimi ilman tietokannan migraatiota. Jos et halua tehdä tätä, sinun pitää palata aikaisempaan versioon stashista." }, "paths": { "database_filename_empty_for_default": "tietokannan tiedostonimi (oletus jos tyhjä)", @@ -1006,7 +1008,7 @@ "tattoos": "Tatuoinnit", "title": "Otsikko", "toast": { - "added_entity": "Lisätty {entity}", + "added_entity": "Lisätty {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "added_generation_job_to_queue": "Lisätty generointi työjonoon", "created_entity": "Luotiin {entity}", "default_filter_set": "Oletussuodattimet", @@ -1025,6 +1027,9 @@ "twitter": "Twitter", "updated_at": "Päivitetty", "url": "URL", + "validation": { + "aliases_must_be_unique": "Aliaksien pitää olla uniikkeja" + }, "videos": "Videot", "view_all": "Näytä kaikki", "weight": "Paino", diff --git a/ui/v2.5/src/locales/fr-FR.json b/ui/v2.5/src/locales/fr-FR.json index c1a0bbe5c..d1bff5c6f 100644 --- a/ui/v2.5/src/locales/fr-FR.json +++ b/ui/v2.5/src/locales/fr-FR.json @@ -6,9 +6,10 @@ "add_to_entity": "Ajouter à {entityType}", "allow": "Autoriser", "allow_temporarily": "Autoriser temporairement", + "anonymise": "Anonymiser", "apply": "Appliquer", "auto_tag": "Étiquetage automatique", - "backup": "Sauvegarde", + "backup": "Sauvegarder", "browse_for_image": "Sélectionner une image…", "cancel": "Annuler", "clean": "Nettoyer", @@ -20,6 +21,7 @@ "confirm": "Confirmer", "continue": "Continuer", "create": "Créer", + "create_chapters": "Créer un chapitre", "create_entity": "Créer {entityType}", "create_marker": "Créer un marqueur", "created_entity": "Créé {entity_type} : {entity_name}", @@ -32,10 +34,11 @@ "delete_stashid": "Supprimer StashID", "disallow": "Refuser", "download": "Télécharger", + "download_anonymised": "Téléchargement anonymisé", "download_backup": "Télécharger une sauvegarde", "edit": "Éditer", "edit_entity": "Éditer {entityType}", - "export": "Exporter…", + "export": "Exporter", "export_all": "Tout exporter…", "find": "Rechercher", "finish": "Terminer", @@ -58,6 +61,8 @@ "merge": "Fusionner", "merge_from": "Fusionner depuis", "merge_into": "Fusionner dans", + "migrate_blobs": "Migrer les blobs", + "migrate_scene_screenshots": "Migrer les vignettes de scène", "next_action": "Suivant", "not_running": "arrêt", "open_in_external_player": "Ouvrir dans un lecteur externe", @@ -128,8 +133,13 @@ "birth_year": "Année de naissance", "birthdate": "Date de naissance", "bitrate": "Débit", + "blobs_storage_type": { + "database": "Base de données", + "filesystem": "Système de fichier" + }, "captions": "Sous-titres", "career_length": "Durée de la carrière", + "chapters": "Chapitres", "component_tagger": { "config": { "active_instance": "Instance Stash-Box active :", @@ -160,7 +170,7 @@ "duration_unknown": "Durée inconnue", "fp_found": "{fpCount, plural, =0 {Aucune nouvelle correspondance d'empreinte trouvée} other {# nouvelles correspondances d'empreintes trouvées}}", "fp_matches": "La durée correspond", - "fp_matches_multi": "La durée correspond à {matchCount}/{durationsLength} empreinte·s", + "fp_matches_multi": "La durée correspond à {matchCount}/{durationsLength} empreintes", "hash_matches": "{hash_type} est une correspondance", "match_failed_already_tagged": "Scène déjà étiquetée", "match_failed_no_result": "Aucun résultat trouvé", @@ -183,11 +193,12 @@ "latest_version": "Dernière version", "latest_version_build_hash": "Empreinte de construction de la dernière version :", "new_version_notice": "[Nouveautés]", + "release_date": "Date de sortie :", "stash_discord": "Rejoignez notre chaine {url}", - "stash_home": "Accueil Stash sur {url}", + "stash_home": "Accueil de Stash sur {url}", "stash_open_collective": "Soutenez-nous via {url}", - "stash_wiki": "Page {url} Stash", - "version": "Version" + "stash_wiki": "{url} de Stash", + "version": "Version actuelle" }, "application_paths": { "heading": "Chemins de l'application" @@ -253,7 +264,15 @@ "description": "Emplacement de sauvegarde des bases de données SQLite", "heading": "Chemin du répertoire de sauvegarde" }, - "cache_location": "Emplacement du cache", + "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.", + "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.", + "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.", "cache_path_head": "Chemin du cache", "calculate_md5_and_ohash_desc": "Calculer la somme de contrôle MD5 en complément de oshash. Son activation entraîne un ralentissement des analyses initiales. Le hachage du nom de fichier doit être défini sur oshash pour désactiver le calcul MD5.", "calculate_md5_and_ohash_label": "Calculer le MD5 pour les vidéos", @@ -263,12 +282,43 @@ "chrome_cdp_path_desc": "Chemin de l'exécutable Chrome, ou adresse distante (commençant par http:// ou https://, par exemple http://localhost:9222/json/version) d'une instance de Chrome.", "create_galleries_from_folders_desc": "Coché, crée des galeries à partir de dossiers contenant des images.", "create_galleries_from_folders_label": "Créer des galeries à partir de dossiers contenant des images", + "database": "Base de données", "db_path_head": "Chemin de la base de données", "directory_locations_to_your_content": "Emplacements de votre contenu", - "excluded_image_gallery_patterns_desc": "Expression régulière de fichiers images et galeries ou de chemins d'accès à exclure de l'analyse et à ajouter au nettoyage", - "excluded_image_gallery_patterns_head": "Modèles d'image ou galerie exclués", + "excluded_image_gallery_patterns_desc": "Expressions régulières de fichiers images et galeries ou de chemins d'accès à exclure de l'analyse et à ajouter au nettoyage", + "excluded_image_gallery_patterns_head": "Modèles d'image ou galerie exclus", "excluded_video_patterns_desc": "Expressions régulières de fichiers vidéo ou de chemins d'accès à exclure de l'analyse et à ajouter au nettoyage", - "excluded_video_patterns_head": "Modèles de vidéo exclués", + "excluded_video_patterns_head": "Modèles de vidéo exclus", + "ffmpeg": { + "hardware_acceleration": { + "desc": "Utiliser le matériel disponible pour encoder la vidéo en vue d'un transcodage en temps réel.", + "heading": "Encodage matériel FFmpeg" + }, + "live_transcode": { + "input_args": { + "desc": "Avancé : Arguments supplémentaires à passer à FFmpeg avant le champ d'entrée lors du transcodage vidéo en temps réel.", + "heading": "Arguments d'entrée du transcodage FFmpeg en temps réel" + }, + "output_args": { + "desc": "Avancé : Arguments supplémentaires à passer à FFmpeg avant le champ de sortie lors du transcodage vidéo en temps réel.", + "heading": "Arguments de sortie du transcodage FFmpeg en temps réel" + } + }, + "transcode": { + "input_args": { + "desc": "Avancé : Arguments supplémentaires à passer à FFmpeg avant le champ d'entrée lors du transcodage vidéo en temps réel.", + "heading": "Arguments d'entrée pour le transcodage FFmpeg en temps réel" + }, + "output_args": { + "desc": "Avancé : Arguments supplémentaires à passer à FFmpeg avant le champ de sortie lors de la génération de la vidéo.", + "heading": "Arguments de sortie du transcodage FFmpeg" + } + } + }, + "funscript_heatmap_draw_range": "Inclure l'amplitude dans les cartes thermiques générées", + "funscript_heatmap_draw_range_desc": "Dessiner l'étendue du mouvement sur l'axe des ordonnées de la carte thermique générée. Les cartes thermiques existantes devront être régénérées après modifications.", + "gallery_cover_regex_desc": "Expression régulière utilisée pour identifier une image comme vignette de la galerie", + "gallery_cover_regex_label": "Modèle de vignette de la galerie", "gallery_ext_desc": "Liste d'extensions de fichiers séparées par des virgules qui seront reconnues comme des archives zip de la galerie.", "gallery_ext_head": "Extensions zip de la galerie", "generated_file_naming_hash_desc": "Utilisez MD5 ou oshash pour le nommage des fichiers générés. Le modifier exige que toutes les scènes soient renseignées avec une valeur MD5/oshash appropriée. Après avoir modifié cette valeur, les fichiers générés existants devront être migrés ou régénérés. Voir la page Tâches pour la migration.", @@ -276,6 +326,7 @@ "generated_files_location": "Emplacement des fichiers générés (marqueurs de scène, aperçus de scène, sprites, etc.)", "generated_path_head": "Chemin des fichiers générés", "hashing": "Hachage", + "heatmap_generation": "Génération de la carte thermique du script interactif", "image_ext_desc": "Liste d'extensions de fichiers séparées par des virgules qui seront reconnues comme des images.", "image_ext_head": "Extensions des images", "include_audio_desc": "Inclure le flux audio lors de la génération des aperçus.", @@ -304,7 +355,7 @@ "heading": "Chemin des extracteurs de contenu" }, "scraping": "Extraction de données", - "sqlite_location": "Emplacement du fichier de base de données SQLite (nécessite un redémarrage)", + "sqlite_location": "Emplacement du fichier de base de données SQLite (nécessite un redémarrage). AVERTISSEMENT : Le stockage de la base de données sur un système différent de celui qui exécute le serveur Stash (c'est-à-dire sur le réseau) n'est pas pris en charge !", "video_ext_desc": "Liste d'extensions de fichiers séparées par des virgules qui seront reconnues comme des vidéos.", "video_ext_head": "Extensions de fichiers vidéo", "video_head": "Vidéo" @@ -323,7 +374,7 @@ }, "scraping": { "entity_metadata": "Métadonnées {entityType}", - "entity_scrapers": "Extracteurs de {entityType}s", + "entity_scrapers": "Extracteurs de {entityType}", "excluded_tag_patterns_desc": "Expressions régulières de noms d'étiquettes à exclure des résultats de l'extraction", "excluded_tag_patterns_head": "Modèles d'étiquette excluse", "scraper": "Extracteur", @@ -346,6 +397,9 @@ }, "tasks": { "added_job_to_queue": "{operation_name} ajouté·e à la liste des tâches", + "anonymise_and_download": "Effectue une copie anonymisée de la base de données et télécharge le fichier résultant.", + "anonymise_database": "Faire une copie de la base de données dans le répertoire de sauvegarde, en anonymisant toutes données sensibles. Celle-ci peut être transmise aux autres pour diagnostic et débogage. La base de données originale n'est pas modifiée. La base de données anonymisée utilise le format de nom de fichier {filename_format}.", + "anonymising_database": "Anonymisation de la base de données", "auto_tag": { "auto_tagging_all_paths": "Étiquetage automatique de tous les chemins", "auto_tagging_paths": "Étiquetage automatique des chemins suivants" @@ -372,6 +426,7 @@ "generate_previews_during_scan_tooltip": "Générer des aperçus WebP animés, requis uniquement si le mode d'aperçu est défini sur Image animée.", "generate_sprites_during_scan": "Générer les sprites de progression", "generate_thumbnails_during_scan": "Générer des vignettes pour les images", + "generate_video_covers_during_scan": "Générer les vignettes de scène", "generate_video_previews_during_scan": "Générer les aperçus", "generate_video_previews_during_scan_tooltip": "Générer des aperçus vidéo joués lors du survol d'une scène", "generated_content": "Contenu généré", @@ -399,7 +454,16 @@ "incremental_import": "Importation incrémentielle à partir d'un fichier zip d'exportation fourni.", "job_queue": "File d'attente des tâches", "maintenance": "Maintenance", + "migrate_blobs": { + "delete_old": "Supprimer les anciennes données", + "description": "Migrer les blobs vers le système de stockage de blobs actuel. Cette migration doit être exécutée après avoir modifié le système de stockage des blobs. Il est possible de supprimer les anciennes données après migration." + }, "migrate_hash_files": "Utilisé après modification de l'empreinte des fichiers générés pour renommer les existants au nouveau format.", + "migrate_scene_screenshots": { + "delete_files": "Supprimer les fichiers de vignette", + "description": "Migrer les vignettes de scène dans le nouveau système de stockage blob. Cette migration doit être exécutée après avoir migré un système existant vers la version 0.20. Il est possible de supprimer les anciennes vignettes après migration.", + "overwrite_existing": "Remplacer les blobs existants par les données de vignettes" + }, "migrations": "Migrations", "only_dry_run": "Effectuer un essai à blanc. Ne supprime rien", "plugin_tasks": "Tâches de Plugin", @@ -436,12 +500,12 @@ }, "basic_settings": "Paramètres de base", "custom_css": { - "description": "La page doit être rafraichie pour que les changements prennent effet.", + "description": "La page doit être rafraichie pour que les changements prennent effet. La compatibilité entre le CSS personnalisé et les futures versions de Stash n'est pas garantie.", "heading": "CSS personnalisé", "option_label": "Activer le CSS personnalisé" }, "custom_javascript": { - "description": "La page doit être actualisée pour que les changements prennent effet.", + "description": "La page doit être actualisée pour que les changements prennent effet. La compatibilité entre le Javascript personnalisé et les futures versions de Stash n'est pas garantie.", "heading": "Javascript personnalisé", "option_label": "Activer le Javascript personnalisé" }, @@ -471,13 +535,17 @@ "heading": "Désactiver la création depuis la liste déroulante" }, "heading": "Édition", + "max_options_shown": { + "label": "Nombre maximal d'éléments à afficher dans les listes déroulantes de sélection" + }, "rating_system": { "star_precision": { "label": "Précision de la notation", "options": { "full": "Complète", "half": "Moitié", - "quarter": "Quart" + "quarter": "Quart", + "tenth": "Dixième" } }, "type": { @@ -510,6 +578,11 @@ "image_lightbox": { "heading": "Visionneuse d'images" }, + "image_wall": { + "direction": "Orientation", + "heading": "Mur d'images", + "margin": "Marge (pixels)" + }, "images": { "heading": "Images", "options": { @@ -653,6 +726,8 @@ }, "custom": "Personnalisé", "date": "Date", + "date_format": "AAAA-MM-JJ", + "datetime_format": "AAAA-MM-JJ HH:MM", "death_date": "Date du décès", "death_year": "Année du décès", "descending": "Descendant", @@ -661,7 +736,6 @@ "details": "Détails", "developmentVersion": "Version de développement", "dialogs": { - "aliases_must_be_unique": "Les alias doivent être uniques", "create_new_entity": "Créer un nouveau {entity}", "delete_alert": "{count, plural, one {Le {singularEntity} suivant sera supprimé} other {Les {pluralEntity} suivants seront supprimés}} définitivement :", "delete_confirm": "Êtes-vous sûr de vouloir supprimer {entityName} ?", @@ -677,6 +751,14 @@ "edit_entity_title": "Éditer {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "export_include_related_objects": "Inclure les objets liés dans l'exportation", "export_title": "Exporter", + "imagewall": { + "direction": { + "column": "Colonne", + "description": "Agencement en colonnes ou en lignes.", + "row": "Ligne" + }, + "margin_desc": "Nombre de pixels de marge autour de chaque image entière." + }, "lightbox": { "delay": "Délai (Secondes)", "display_mode": { @@ -686,6 +768,7 @@ "original": "Original" }, "options": "Options", + "page_header": "Page {page} / {total}", "reset_zoom_on_nav": "Réinitialisation du facteur de zoom lors d'un changement d'image", "scale_up": { "description": "Redimensionner les petites images pour les adapter à l'écran", @@ -713,11 +796,12 @@ "destination": "Réaffecter à" }, "scene_gen": { + "covers": "Vignettes de scène", "force_transcodes": "Forcer la génération du transcodage", "force_transcodes_tooltip": "Par défaut, les transcodes ne sont générés que lorsque le fichier vidéo n'est pas pris en charge par le navigateur. Activé, les transcodes seront générés même si le fichier vidéo semble être pris en charge par le navigateur.", "image_previews": "Aperçus d'images animées", "image_previews_tooltip": "Aperçu WebP animé, requis uniquement si le mode d'aperçu est défini sur Image animée.", - "interactive_heatmap_speed": "Générer des cartes de fréquentation et vitesses pour les scènes interactives", + "interactive_heatmap_speed": "Générer des cartes thermiques et vitesses pour les scènes interactives", "marker_image_previews": "Aperçus animés des marqueurs", "marker_image_previews_tooltip": "Aperçus WebP animés de marqueurs, requis uniquement si le mode d'aperçu est défini sur Image animée.", "marker_screenshots": "Captures d'écran des marqueurs", @@ -757,6 +841,7 @@ }, "dimensions": "Dimensions", "director": "Réalisateur", + "disambiguation": "Désambiguïsation", "display_mode": { "grid": "Grille", "list": "Liste", @@ -801,6 +886,11 @@ "warmth": "Température" }, "empty_server": "Ajoutez quelques scènes à votre serveur pour afficher les recommandations sur cette page.", + "errors": { + "image_index_greater_than_zero": "L'index de l'image doit être supérieur à 0", + "lazy_component_error_help": "Si vous avez récemment mis à jour Stash, merci de recharger la page ou de vider le cache de votre navigateur.", + "something_went_wrong": "Quelque chose n'a pas fonctionné." + }, "ethnicity": "Ethnicité", "existing_value": "valeur existante", "eye_color": "Couleur des yeux", @@ -848,6 +938,7 @@ "syncing": "Synchronisation avec le serveur", "uploading": "Script de chargement" }, + "hasChapters": "A des chapitres", "hasMarkers": "Dispose de marqueurs", "height": "Taille", "height_cm": "Taille (cm)", @@ -855,6 +946,7 @@ "ignore_auto_tag": "Ignorer l'étiquetage automatique", "image": "Image", "image_count": "Nombre d'Images", + "image_index": "Image #", "images": "Images", "include_parent_tags": "Inclure les étiquettes parentes", "include_sub_studios": "Inclure les studios affiliés", @@ -890,7 +982,7 @@ "megabits_per_second": "{value} mégabits par seconde", "metadata": "Métadonnées", "movie": "Film", - "movie_scene_number": "Numéro de scène du film", + "movie_scene_number": "Numéro de scène", "movies": "Films", "name": "Nom", "new": "Nouveau", @@ -953,7 +1045,7 @@ "updating_untagged_performers_description": "Une mise à jour des performeurs non étiquetés essaiera de faire correspondre tous les performeurs qui n'ont pas de StashID et actualisera les métadonnées." }, "performers": "Performeurs", - "piercings": "Perçages", + "piercings": "Piercings", "play_count": "Compteur de lecture", "play_duration": "Temps de lecture", "primary_file": "Fichier principal", @@ -967,7 +1059,7 @@ "resume_time": "Reprendre le temps", "scene": "Scène", "sceneTagger": "Étiqueteuse de scènes", - "sceneTags": "Étiquettes de scène", + "sceneTags": "Étiquettes de la scène", "scene_code": "Code studio", "scene_count": "Nombre de scènes", "scene_created_at": "Scène créée le", @@ -977,7 +1069,7 @@ "scenes": "Scènes", "scenes_updated_at": "Scène actualisée le", "search_filter": { - "add_filter": "Ajouter un filtre", + "edit_filter": "Modifier le filtre", "name": "Filtre", "saved_filters": "Filtres sauvegardés", "update_filter": "Filtre actualisé" @@ -987,8 +1079,12 @@ "setup": { "confirm": { "almost_ready": "Nous sommes presque prêts à terminer la configuration. Confirmez les paramètres suivants. Vous pouvez revenir en arrière pour modifier toute erreur. Si tout semble correct, cliquez sur Confirmer pour créer votre système.", + "blobs_directory": "Répertoire de données binaires", + "cache_directory": "Répertoire du cache", "configuration_file_location": "Emplacement du fichier de configuration :", "database_file_path": "Chemin du fichier de base de données", + "default_blobs_location": "", + "default_cache_location": "/cache", "default_db_location": "/stash-go.sqlite", "default_generated_content_location": "/generated", "generated_directory": "Répertoire généré", @@ -1020,16 +1116,24 @@ "migration_notes": "Notes de migration", "migration_required": "Migration requise", "perform_schema_migration": "Procéder à la migration du schéma", - "schema_too_old": "La version du schéma de votre base de données Stash actuelle est {databaseSchema} et doit être migrée vers la version {appSchema}. Cette version de Stash ne fonctionnera pas sans migration de la base de données. Si vous souhaitez ne pas migrer, vous devrez rétrograder vers une version qui correspond au schéma de votre base de données." + "schema_too_old": "La version du schéma de votre base de données Stash actuelle est {databaseSchema} et doit être migrée vers la version {appSchema}. Cette version de Stash ne fonctionnera pas sans migration de la base de données. Si vous ne souhaitez pas migrer, vous devrez rétrograder vers une version qui correspond au schéma de votre base de données." }, "paths": { "database_filename_empty_for_default": "Nom de fichier de la base de données (vide par défaut)", - "description": "Ensuite, nous devons déterminer où trouver votre collection pornographique, où stocker la base de données Stash et les fichiers générés. Ces paramètres peuvent être modifiés ultérieurement si nécessaire.", + "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.", + "path_to_blobs_directory_empty_for_database": "chemin vers le répertoire blobs (vide pour utiliser la base de données)", + "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)", "set_up_your_paths": "Configurez vos chemins", "stash_alert": "Aucun chemin de bibliothèque n'a été sélectionné. Aucun média ne pourra être analysé dans Stash. En êtes-vous sûr ?", + "where_can_stash_store_blobs": "Où Stash peut-il stocker les données binaires de la base de données ?", + "where_can_stash_store_blobs_description": "Stash peut stocker des données binaires telles que les vignettes de scènes, les images de performeurs, de studios et de tags, soit dans la base de données, soit dans le système de fichiers. Par défaut, ces données sont stockées dans le système de fichiers, dans le sous-répertoire blobs. Si vous souhaitez modifier cela, veuillez 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_blobs_description_addendum": "Alternativement, si vous souhaitez stocker ces données dans la base de données, vous pouvez laisser ce champ vide. Remarque : cela augmentera la taille de votre base de données et son temps de migration.", + "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 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_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 ?", @@ -1049,7 +1153,7 @@ "your_system_has_been_created": "Bravo ! Votre système a été créé !" }, "welcome": { - "config_path_logic_explained": "Stash essaie d'abord de trouver son fichier de configuration (config.yml) dans le répertoire de travail courant, et s'il ne le trouve pas, il se reporte sur $HOME/.stash/config. yml (sous Windows, ce sera %USERPROFILE%\\.stash\\config.yml). Vous pouvez également faire en sorte que Stash lise un fichier de configuration spécifique en le lançant avec les options -c ou --config .", + "config_path_logic_explained": "Stash essaie d'abord de trouver son fichier de configuration (config.yml) dans le répertoire de travail courant, et s'il ne le trouve pas, il se reporte sur $HOME/.stash/config. yml (sous Windows, ce sera %USERPROFILE%\\.stash\\config.yml). Vous pouvez également faire en sorte que Stash lise un fichier de configuration spécifique en le lançant avec les options -c '' ou --config ''.", "in_current_stash_directory": "Dans le répertoire $HOME/.stash", "in_the_current_working_directory": "Dans le répertoire de travail courant", "next_step": "Une fois que tout est réglé, si vous êtes prêt à procéder à la configuration d'un nouveau système, choisissez l'emplacement où vous souhaitez stocker votre fichier de configuration et cliquez sur Suivant.", @@ -1089,7 +1193,7 @@ "sub_tags": "Étiquettes affiliées", "subsidiary_studios": "Studios affiliés", "synopsis": "Résumé", - "tag": "Étiquettes", + "tag": "Étiquette", "tag_count": "Nombre d'étiquettes", "tags": "Étiquettes", "tattoos": "Tatouages", @@ -1101,6 +1205,7 @@ "default_filter_set": "Filtre par défaut défini", "delete_past_tense": "{count, plural, one {{singularEntity} supprimé} other {{pluralEntity} supprimés}}", "generating_screenshot": "Génération de la capture d'écran…", + "image_index_too_large": "Erreur : L'index de l'image est plus grand que le nombre d'images dans la galerie", "merged_scenes": "Scènes fusionnées", "merged_tags": "Étiquettes fusionnées", "reassign_past_tense": "Fichier réaffecté", @@ -1118,6 +1223,11 @@ "type": "Type", "updated_at": "Actualisé le", "url": "URL", + "validation": { + "aliases_must_be_unique": "Les alias doivent être uniques", + "date_invalid_form": "${path} doit être au format AAAA-MM-JJ", + "required": "${path} est un champ requis" + }, "videos": "Vidéos", "view_all": "Tout voir", "weight": "Poids", diff --git a/ui/v2.5/src/locales/hr-HR.json b/ui/v2.5/src/locales/hr-HR.json index 69ed36a16..af7508c40 100644 --- a/ui/v2.5/src/locales/hr-HR.json +++ b/ui/v2.5/src/locales/hr-HR.json @@ -16,6 +16,7 @@ "clear_image": "Očisti sliku", "close": "Zatvori", "confirm": "Prihvati", + "continue": "Nastavi", "create": "Napravi", "create_entity": "Napravi {entityType}", "create_marker": "Napravi Marker", @@ -24,11 +25,13 @@ "delete_entity": "Izbrišite {entityType}", "delete_file": "Izbrišite datoteku", "delete_generated_supporting_files": "Izbrišite generirane datoteke", + "delete_stashid": "Izbriši StashID", "disallow": "Zabrani", "download": "Preuzmi", "download_backup": "Preuzmi Sigurnostnu Kopiju", "edit": "Uredi", - "export": "Izvezi…", + "edit_entity": "Uredi {entityType}", + "export": "Izvezi", "export_all": "Izvezi sve…", "find": "Pronađi", "finish": "Završi", @@ -40,20 +43,60 @@ "generate_thumb_default": "Generiraj zadanu sliku", "generate_thumb_from_current": "Generiraj sliku iz trenutnog", "hide": "Sakrij", + "hide_configuration": "Sakrij konfiguraciju", "identify": "Identificiraj", "ignore": "Ignoriraj", "import": "Uvezi…", "import_from_file": "Uvezi iz datoteke", + "logout": "Odjava", + "merge": "Spoji", + "merge_from": "Spoji iz", + "merge_into": "Spoji u", "next_action": "Dalje", "not_running": "nije pokrenuto", + "open_in_external_player": "Otvori u vanjskom playeru", "open_random": "Otvori slučajnu stavku", "play_random": "Pokreni slučajnu stavku", "play_selected": "Pokreni odabrano", "preview": "Pretpregled", "previous_action": "Natrag", "refresh": "Osvježi", + "reload_plugins": "Ponovno učitaj dodatke", "remove": "Ukloni", + "remove_from_gallery": "Ukloni iz Galerije", + "rename_gen_files": "Preimenuj generirane datoteke", + "rescan": "Ponovno skeniraj", "running": "pokrenuto", - "save": "Spremi" + "save": "Spremi", + "save_delete_settings": "Zadano koristi ove opcije pri brisanju", + "save_filter": "Spremi filter", + "scan": "Skeniraj", + "search": "Traži", + "select_all": "Odaberi sve", + "select_entity": "Odaberi {entityType}", + "select_folders": "Odaberi mape", + "select_none": "Odznači sve", + "selective_auto_tag": "Djelomično auto-označavanje", + "selective_clean": "Djelomično čišćenje", + "selective_scan": "Djelomično skeniranje", + "set_as_default": "Postavi kao zadano", + "set_image": "Postavi sliku…", + "show": "Prikaži", + "show_configuration": "Prikaži konfiguraciju", + "skip": "Preskoči", + "split": "Razdjeli", + "stop": "Zaustavi", + "submit": "Potvrdi", + "submit_stash_box": "Prenesi na Stash-Box", + "submit_update": "Podnesi nadopunu", + "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." + }, + "temp_disable": "Privremeno isključi…", + "temp_enable": "Privremeno uključi…", + "use_default": "Koristi zadane vrijednosti", + "view_random": "Vidi nasumično" } } diff --git a/ui/v2.5/src/locales/hu-HU.json b/ui/v2.5/src/locales/hu-HU.json index 476be5ae9..7fff63874 100644 --- a/ui/v2.5/src/locales/hu-HU.json +++ b/ui/v2.5/src/locales/hu-HU.json @@ -30,7 +30,7 @@ "download_backup": "Biztonsági Mentés Letöltése", "edit": "Módosít", "edit_entity": "{entityType} Módosítása", - "export": "Exportálás…", + "export": "Exportálás", "export_all": "Összes exportálása…", "find": "Keresés", "finish": "Befejez", @@ -465,7 +465,6 @@ "scene_id": "Jelenet ID", "scenes": "Jelenetek", "search_filter": { - "add_filter": "Szűrő Hozzáadása", "name": "Szűrő", "saved_filters": "Mentett szűrők", "update_filter": "Szűrő Frissítése" @@ -495,7 +494,7 @@ "support_us": "Támogatás" }, "welcome": { - "config_path_logic_explained": "A Stash elöszőr a jelenlegi munkakönyvtárban próbálja a konfigurációs fájlját (config.yml) megkeresni. Ha ott nem találja, akkor a következő helyen próbálkozik: $HOME/.stash/config.yml (Windows rendszeren: %USERPROFILE%\\.stash\\config.yml). Meghatározhatja hogy a Stash egy bizonyos konfigurációs fájlt használjon, ammennyiben a -c or --config paraméterekkel indítja.", + "config_path_logic_explained": "A Stash elöszőr a jelenlegi munkakönyvtárban próbálja a konfigurációs fájlját (config.yml) megkeresni. Ha ott nem találja, akkor a következő helyen próbálkozik: $HOME/.stash/config.yml (Windows rendszeren: %USERPROFILE%\\.stash\\config.yml). Meghatározhatja hogy a Stash egy bizonyos konfigurációs fájlt használjon, ammennyiben a -c '' or --config '' paraméterekkel indítja.", "unexpected_explained": "Amennyiben ez a képernyő váratlanul bukkant fel, próbáld újraindítani a Stasht a megfelelő munkakönyvtárban, vagy a -c flag-gel." }, "welcome_specific_config": { diff --git a/ui/v2.5/src/locales/it-IT.json b/ui/v2.5/src/locales/it-IT.json index ce4a78385..3511c8e01 100644 --- a/ui/v2.5/src/locales/it-IT.json +++ b/ui/v2.5/src/locales/it-IT.json @@ -6,6 +6,7 @@ "add_to_entity": "Aggiungi a {entityType}", "allow": "Acconsenti", "allow_temporarily": "Acconsenti temporaneamente", + "anonymise": "Anonimizza", "apply": "Applica", "auto_tag": "Tag Automatico", "backup": "Backup", @@ -32,10 +33,11 @@ "delete_stashid": "Cancellare StashID", "disallow": "Non Acconsentire", "download": "Scarica", + "download_anonymised": "Scarica anonimamente", "download_backup": "Scarica Backup", "edit": "Edita", "edit_entity": "Modifica {entityType}", - "export": "Esporta…", + "export": "Esporta", "export_all": "Esporta tutto…", "find": "Trova", "finish": "Finire", @@ -137,7 +139,7 @@ "blacklist_label": "Lista Nera", "query_mode_auto": "Automatico", "query_mode_auto_desc": "Usa metadati se presenti, o nome file", - "query_mode_dir": "Dir", + "query_mode_dir": "Elenco", "query_mode_dir_desc": "Usa solo la cartella che contiene il file video", "query_mode_filename": "Nome file", "query_mode_filename_desc": "Usa solo il nome file", @@ -194,7 +196,7 @@ }, "categories": { "about": "Chi siamo", - "changelog": "Changelog", + "changelog": "Registro dei cambiamenti", "interface": "Interfaccia", "logs": "Log", "metadata_providers": "Provider dei Metadata", @@ -269,6 +271,8 @@ "excluded_image_gallery_patterns_head": "Schema Immagini/Gallerie Escluse", "excluded_video_patterns_desc": "Espressioni Regolari di file/percorsi di video per escluderli dalla Scansione e aggiungerli alla Pulizia", "excluded_video_patterns_head": "Schema Video Esclusi", + "gallery_cover_regex_desc": "Espressione regolare usata per identificare un immagine come copertina di galleria", + "gallery_cover_regex_label": "Schema copertina di galleria", "gallery_ext_desc": "Lista di estensioni delimitate da Virgola che saranno identificate come gallerie in file compressi/zip.", "gallery_ext_head": "Estensioni Gallerie zip", "generated_file_naming_hash_desc": "Usa l'MD5 o oshas per i nomi dei file creati. Cambiarlo richiede che tutte le scene abbiano il valore MD5/oshash ripopolato. Dopo la modifica, i file esistenti necessiteranno di essere migrati o ricreati. Vedere la pagina Attività per la migrazione.", @@ -346,6 +350,9 @@ }, "tasks": { "added_job_to_queue": "Aggiunto/a {operation_name} alla coda lavori", + "anonymise_and_download": "Crea una copia anonima del database e scarica il file creato.", + "anonymise_database": "Crea una copia del database nella cartella di backup, anonimizzando tutti i dati sensibili. Questo file può essere inviato ad altri come aiuto nella risoluzione dei problemi. Il database anonimizzato viene salvato in formato {filename_format}.", + "anonymising_database": "Anonimizzando il database", "auto_tag": { "auto_tagging_all_paths": "Taggare Automaticamente tutti i percorsi", "auto_tagging_paths": "Taggare Automaticamente i seguenti percorsi" @@ -661,7 +668,6 @@ "details": "Dettagli", "developmentVersion": "Versione Sviluppo", "dialogs": { - "aliases_must_be_unique": "gli alias devono essere univoci", "create_new_entity": "Crea nuovo/a {entity}", "delete_alert": "Il seguente/I seguenti {count, plural, one {{singularEntity}} other {{pluralEntity}}} sarà/saranno cancellati permanentemente:", "delete_confirm": "Sei sicuro di voler cancellare {entityName}?", @@ -757,6 +763,7 @@ }, "dimensions": "Dimensioni", "director": "Regista", + "disambiguation": "Disambiguità", "display_mode": { "grid": "Griglia", "list": "Lista", @@ -977,7 +984,6 @@ "scenes": "Scene", "scenes_updated_at": "Scena Aggiornata Al", "search_filter": { - "add_filter": "Aggiungi Filtro", "name": "Filtro", "saved_filters": "Filtri Salvati", "update_filter": "Aggiorna Filtro" @@ -1049,7 +1055,7 @@ "your_system_has_been_created": "Successo! Il vostro sistema è stato creato!" }, "welcome": { - "config_path_logic_explained": "Stash cerca di trovare il file di configurazione (config.yml) dall'attuale cartella di lavoro inizialmente e se non lo trova lì, cerca in $HOME/.stash/config.yml (sotto Windows, sarà %USERPROFILE%\\.stash\\config.yml). Potete far leggere a Stash un specifico file di configurazione lanciandolo con l'opzione -c o --config .", + "config_path_logic_explained": "Stash cerca di trovare il file di configurazione (config.yml) dall'attuale cartella di lavoro inizialmente e se non lo trova lì, cerca in $HOME/.stash/config.yml (sotto Windows, sarà %USERPROFILE%\\.stash\\config.yml). Potete far leggere a Stash un specifico file di configurazione lanciandolo con l'opzione -c '' o --config ''.", "in_current_stash_directory": "Nella cartella $HOME/.stash", "in_the_current_working_directory": "Nell'attuale cartella di lavoro", "next_step": "Dopo tutto questo, se siete pronti a procedere con la configurazione di un nuovo sistema, scegliete dove vorreste salvare il file di configurazione e premete Prossimo.", @@ -1118,6 +1124,9 @@ "type": "Tipo", "updated_at": "Aggiornato Al", "url": "URL", + "validation": { + "aliases_must_be_unique": "gli alias devono essere univoci" + }, "videos": "Video", "view_all": "Vedi Tutto", "weight": "Peso", diff --git a/ui/v2.5/src/locales/ja-JP.json b/ui/v2.5/src/locales/ja-JP.json index 9958dc1f6..27c74a3d8 100644 --- a/ui/v2.5/src/locales/ja-JP.json +++ b/ui/v2.5/src/locales/ja-JP.json @@ -6,6 +6,7 @@ "add_to_entity": "{entityType}に追加", "allow": "許可", "allow_temporarily": "一時的に許可", + "anonymise": "匿名にする", "apply": "適用", "auto_tag": "自動タグ付け", "backup": "バックアップ", @@ -32,10 +33,11 @@ "delete_stashid": "StashIDを削除", "disallow": "拒否", "download": "ダウンロード", + "download_anonymised": "匿名でダウンロード", "download_backup": "バックアップをダウンロード", "edit": "編集", "edit_entity": "{entityType}を編集", - "export": "エクスポート…", + "export": "エクスポート", "export_all": "全てエクスポート…", "find": "探す", "finish": "完了", @@ -105,6 +107,7 @@ "submit": "送信", "submit_stash_box": "Stash-Boxに送信", "submit_update": "更新を送信", + "swap": "入れ替える", "tasks": { "clean_confirm_message": "クリーニングを実行してもよろしいですか?この操作により、ファイルシステムで利用されていないすべてのシーンとギャラリーから生成されたコンテンツとデータベース情報が削除されます。", "dry_mode_selected": "ドライモードが選択されています。実際の削除は実施されず、ログ処理だけが実行されます。", @@ -182,6 +185,7 @@ "latest_version": "最新バージョン", "latest_version_build_hash": "最新バージョンのビルドハッシュ:", "new_version_notice": "[NEW]", + "release_date": "リリース日:", "stash_discord": "私たちの{url}チャンネルへ参加しませんか", "stash_home": "Stashのすべては{url}にあります", "stash_open_collective": "{url}からStashの開発を支援", @@ -268,6 +272,28 @@ "excluded_image_gallery_patterns_head": "除外する画像/ギャラリーの規則", "excluded_video_patterns_desc": "スキャンから除外し、クリーニングに追加する動画のファイル/パスの正規表現を指定できます", "excluded_video_patterns_head": "除外する動画の規則", + "ffmpeg": { + "live_transcode": { + "input_args": { + "desc": "詳細: 動画をライブ変換する際に、ffmpegに通す、input欄の前に付与する追加の引数を指定できます。", + "heading": "ffmpegライブ変換入力引数" + }, + "output_args": { + "desc": "詳細: 動画をライブ変換する際に、ffmpegに通す、output欄の前に付与する追加の引数を指定できます。", + "heading": "ffmpegライブ変換出力引数" + } + }, + "transcode": { + "input_args": { + "desc": "詳細: 動画を生成する際に、ffmpegに通す、input欄の前に付与する追加の引数を指定できます。", + "heading": "ffmpeg変換入力引数" + }, + "output_args": { + "desc": "詳細: 動画をライブ変換する際に、ffmpegに通す、output欄の前に付与する追加の引数を指定できます。", + "heading": "ffmpeg変換出力引数" + } + } + }, "gallery_ext_desc": "ギャラリーzipファイルとして認識させるファイル拡張子のコンマ区切りリストです。", "gallery_ext_head": "ギャラリーzipの拡張子", "generated_file_naming_hash_desc": "生成されたファイルの命名にMD5またはoshashを使用します。これを変更するには、すべてのシーンに該当するMD5/oshash値が適用されている必要があります。この値を変更後、既に存在する生成済みのファイルを移行または再生成する必要があります。遺構についてはタスクページをご確認ください。", @@ -345,6 +371,9 @@ }, "tasks": { "added_job_to_queue": "{operation_name}がジョブキューに追加されました", + "anonymise_and_download": "データベースを匿名化し、結果をダウンロードします。", + "anonymise_database": "センシティブな全データを匿名化して、バックアップ先にデータベースのコピーを保存します。これは、他者にトラブルシューティングを依頼したり、デバッグ目的の際に役立ちます。オリジナルのデータベースは一切変更されません。匿名化されたデータベースには次のファイル名規則が利用されます:{filename_format}", + "anonymising_database": "匿名データベース", "auto_tag": { "auto_tagging_all_paths": "すべてのパスを自動タグ付け", "auto_tagging_paths": "次のパスを自動タグ付け:" @@ -435,11 +464,12 @@ }, "basic_settings": "基本設定", "custom_css": { - "description": "変更を適用するにはページを更新する必要があります。", + "description": "変更を適用するにはページを更新する必要があります。カスタムCSSは、将来のStashリリースとの互換性を保証しません。", "heading": "カスタムCSS", "option_label": "カスタムCSSを有効にする" }, "custom_javascript": { + "description": "エフェクトの変更を反映させるためにはページをリロードしてください。カスタムJavaScriptは、将来のStashリリースとの互換性を保証しません。", "heading": "カスタムJavascript", "option_label": "カスタムJavascriptを有効化する" }, @@ -479,6 +509,7 @@ } }, "type": { + "label": "評価制度の方法", "options": { "decimal": "数値", "stars": "星" @@ -528,6 +559,10 @@ "description": "タイプナビゲーションバー上の異なるコンテンツタイプの表示または非表示を切り替えます", "heading": "メニューアイテム" }, + "minimum_play_percent": { + "description": "再生回数を増やすために再生しなければならないシーンの時間割合を指します。", + "heading": "最低再生割合" + }, "performers": { "options": { "image_location": { @@ -564,7 +599,8 @@ "description": "動画の再生が終了した際にキューに入っている次の動画を再生します", "heading": "デフォルトでプレイリストを続行" }, - "show_scrubber": "スクラバーを表示" + "show_scrubber": "スクラバーを表示", + "track_activity": "アクティビティを追跡" } }, "scene_wall": { @@ -653,7 +689,7 @@ "details": "詳細", "developmentVersion": "開発者バージョン", "dialogs": { - "aliases_must_be_unique": "別名は一意でなければいけません", + "create_new_entity": "{entity}を新規作成する", "delete_alert": "次の{count, plural, one {{singularEntity}} other {{pluralEntity}}}は完全に削除されます:", "delete_confirm": "本当に{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.}}", @@ -689,11 +725,20 @@ "zoom": "拡大" } }, + "merge": { + "destination": "宛先", + "empty_results": "宛先欄の値は変更されません。", + "source": "ソース" + }, "merge_tags": { "destination": "場所", "source": "ソース" }, "overwrite_filter_confirm": "本当に保存されているクエリ「{entityName}」を上書きしてもよろしいですか?", + "reassign_entity_title": "{count, plural, one {{singularEntity}を再割り当て} other {{pluralEntity}を再割り当て}}", + "reassign_files": { + "destination": "次に再割り当て:" + }, "scene_gen": { "force_transcodes": "強制的にトランスコード済みファイルを生成", "force_transcodes_tooltip": "初期設定では、トランスコード済みファイルはブラウザーがサポートしていない動画ファイルであった場合にのみ生成されます。有効にすると、ブラウザーがサポートする動画ファイルであった場合でもトランスコード済みファイルを生成します。", @@ -739,6 +784,7 @@ }, "dimensions": "寸法", "director": "監督", + "disambiguation": "用語解説", "display_mode": { "grid": "グリッド", "list": "リスト", @@ -794,6 +840,7 @@ "file_info": "ファイル情報", "file_mod_time": "ファイル変更日時", "files": "ファイル", + "files_amount": "{value}ファイル", "filesize": "ファイルサイズ", "filter": "フィルター", "filter_name": "フィルター名", @@ -863,6 +910,8 @@ "age_context": "{age} {years_old}(撮影当時)" }, "phash": "PHash", + "play_count": "再生回数", + "play_duration": "再生時間", "stream": "ストリーム", "video_codec": "動画コーデック" }, @@ -933,6 +982,8 @@ }, "performers": "出演者", "piercings": "ピアス", + "play_count": "再生回数", + "play_duration": "再生時間", "primary_file": "メインファイル", "queue": "キュー", "random": "ランダム", @@ -941,15 +992,19 @@ "recently_released_objects": "最近リリースされた{objects}", "release_notes": "リリースノート", "resolution": "解像度", + "resume_time": "レジューム時間", "scene": "シーン", "sceneTagger": "シーン一括タグ付け", "sceneTags": "シーンタグ", + "scene_code": "スタジオコード", "scene_count": "シーン数", + "scene_created_at": "シーンの作成日時", + "scene_date": "シーンの日付", "scene_id": "シーンID", + "scene_updated_at": "シーンの更新日時", "scenes": "シーン", "scenes_updated_at": "シーンの更新日:", "search_filter": { - "add_filter": "フィルターを追加", "name": "フィルター", "saved_filters": "保存済みのフィルター", "update_filter": "フィルターを更新" @@ -1021,7 +1076,7 @@ "your_system_has_been_created": "成功しました!システムの構築が完了しました!" }, "welcome": { - "config_path_logic_explained": "Stashは、まず初めに現在の作業ディレクトリに設定ファイル(config.yml)がないかどうか探し、存在しない場合は、$HOME/.stash/config.yml(Windowsは %USERPROFILE%\\.stash\\config.yml)にフォールバックします。-c <設定ファイルへのパス> または --config <設定ファイルへのパス>オプションをつけて起動させることで、特定の設定ファイルを読み込ませることもできます。", + "config_path_logic_explained": "Stashは、まず初めに現在の作業ディレクトリに設定ファイル(config.yml)がないかどうか探し、存在しない場合は、$HOME/.stash/config.yml(Windowsは %USERPROFILE%\\.stash\\config.yml)にフォールバックします。-c '<設定ファイルへのパス>' または --config '<設定ファイルへのパス>'オプションをつけて起動させることで、特定の設定ファイルを読み込ませることもできます。", "in_current_stash_directory": "$HOME/.stash ディレクトリ内", "in_the_current_working_directory": "現在の作業ディレクトリ内", "next_step": "これで、新しいシステムのセットアップを続行する準備ができました。構成ファイルを保存する場所を選択して、「次へ」をクリックしてください。", @@ -1037,6 +1092,7 @@ "welcome_to_stash": "Stashへようこそ" }, "stash_id": "Stash ID", + "stash_id_endpoint": "Stash IDエンドポイント", "stash_ids": "Stash ID", "stashbox": { "go_review_draft": "下書きを確認するには、{endpoint_name}に移動してください。", @@ -1074,6 +1130,7 @@ "generating_screenshot": "スクリーンショットを生成中…", "merged_scenes": "マージされたシーン", "merged_tags": "マージされたタグ", + "reassign_past_tense": "ファイルが再割り当てされました", "removed_entity": "{count, plural, one {{singularEntity}}と、その他{{pluralEntity}}}を削除しました", "rescanning_entity": "{count, plural, one {{singularEntity}} other {{pluralEntity}}}を再スキャン中…", "saved_entity": "{entity}が保存されました", @@ -1088,6 +1145,9 @@ "type": "タイプ", "updated_at": "更新日:", "url": "URL", + "validation": { + "aliases_must_be_unique": "別名は一意でなければいけません" + }, "videos": "動画", "view_all": "全て表示", "weight": "幅", diff --git a/ui/v2.5/src/locales/ko-KR.json b/ui/v2.5/src/locales/ko-KR.json index 2357c73a7..b37c1fab3 100644 --- a/ui/v2.5/src/locales/ko-KR.json +++ b/ui/v2.5/src/locales/ko-KR.json @@ -35,8 +35,8 @@ "download_backup": "백업 다운로드", "edit": "수정", "edit_entity": "{entityType} 수정", - "export": "내보내기…", - "export_all": "모두 내보내기…", + "export": "내보내기", + "export_all": "모두 내보내기", "find": "찾기", "finish": "완료", "from_file": "파일로 불러오기…", @@ -473,7 +473,12 @@ "heading": "수정하기", "rating_system": { "star_precision": { - "label": "평점별 정확도" + "label": "평점별 정확도", + "options": { + "full": "1점", + "half": "0.5점", + "quarter": "0.25점" + } }, "type": { "label": "평정 시스템 종류", @@ -526,6 +531,10 @@ "description": "탐색 바에 여러 종류의 컨텐츠들이 보여지게 하거나 숨깁니다", "heading": "메뉴 항목" }, + "minimum_play_percent": { + "description": "재생 횟수를 증가시키기 위해 재생되어야 하는 최소 영상 시간(백분율)입니다.", + "heading": "최소 재생 시간(%)" + }, "performers": { "options": { "image_location": { @@ -552,6 +561,7 @@ "scene_player": { "heading": "영상 플레이어", "options": { + "always_start_from_beginning": "항상 처음부터 비디오 시작하기", "auto_start_video": "비디오 자동 재생", "auto_start_video_on_play_selected": { "description": "대기열, 또는 '영상' 페이지에서 (랜덤)선택한 영상을 자동 재생", @@ -561,7 +571,8 @@ "description": "비디오가 끝나면 대기열에 있는 다음 영상을 재생합니다", "heading": "플레이리스트 이어보기" }, - "show_scrubber": "스크러버 보이기" + "show_scrubber": "스크러버 보이기", + "track_activity": "활동 트래킹" } }, "scene_wall": { @@ -650,7 +661,7 @@ "details": "세부사항", "developmentVersion": "개발 버전", "dialogs": { - "aliases_must_be_unique": "별칭은 유일해야 합니다", + "create_new_entity": "새로운 {entity} 생성", "delete_alert": "다음 {count, plural, one {{singularEntity}} other {{pluralEntity}}}이(가) 영구 삭제될 것입니다:", "delete_confirm": "정말 {entityName}을 삭제하시겠습니까?", "delete_entity_desc": "{정말로 {count, plural, one {singularEntity} other {pluralEntity}}을(를) 삭제하시겠습니까? 원본 파일 또한 삭제하지 않으면 스캔을 할 때 {count, plural, one {singularEntity} other {pluralEntity}}이(가) 다시 추가될 것입니다.}", @@ -944,15 +955,16 @@ "recently_released_objects": "최근 발매된 {objects}", "release_notes": "업데이트 내역", "resolution": "해상도", + "resume_time": "재시작 시간", "scene": "영상", "sceneTagger": "영상 태거", "sceneTags": "영상 태그", + "scene_code": "스튜디오 코드", "scene_count": "영상 개수", "scene_id": "영상 ID", "scenes": "영상", "scenes_updated_at": "영상 업데이트 날짜", "search_filter": { - "add_filter": "필터 추가", "name": "필터", "saved_filters": "저장된 필터", "update_filter": "필터 업데이트" @@ -1024,7 +1036,7 @@ "your_system_has_been_created": "성공했습니다! 시스템이 생성되었습니다!" }, "welcome": { - "config_path_logic_explained": "Stash에서는 설정 파일을 현재 폴더에서 먼저 찾아보고, 없다면 %USERPROFILE%\\.stash\\config.yml을 찾습니다 (윈도우가 아닌 운영체제에서는 $HOME/.stash/config.yml을 찾습니다). 또는 Stash를 -c <설정 파일 경로> 또는 --config <설정 파일 경로> 옵션을 사용해 실행시켜 특정한 설정 파일을 읽도록 할 수 있습니다.", + "config_path_logic_explained": "Stash에서는 설정 파일을 현재 폴더에서 먼저 찾아보고, 없다면 %USERPROFILE%\\.stash\\config.yml을 찾습니다 (윈도우가 아닌 운영체제에서는 $HOME/.stash/config.yml을 찾습니다). 또는 Stash를 -c '<설정 파일 경로>' 또는 --config '<설정 파일 경로>' 옵션을 사용해 실행시켜 특정한 설정 파일을 읽도록 할 수 있습니다.", "in_current_stash_directory": "$HOME/.stash 폴더 안", "in_the_current_working_directory": "현재 폴더 안", "next_step": "그 모든 것을 제외하고, 새로운 시스템 설정을 시작할 준비가 되었다면, 어디에 설정 파일을 저장할지 선택한 뒤 '다음' 버튼을 누르세요.", @@ -1040,6 +1052,7 @@ "welcome_to_stash": "Stash에 오신 것을 환영합니다" }, "stash_id": "Stash ID", + "stash_id_endpoint": "Stash ID 엔드포인트", "stash_ids": "Stash IDs", "stashbox": { "go_review_draft": "초안을 검토하려면 {endpoint_name}(으)로 이동하십시오.", @@ -1075,7 +1088,9 @@ "default_filter_set": "기본 필터 셋", "delete_past_tense": "{count, plural, one {{singularEntity}} other {{pluralEntity}}}이(가) 삭제되었습니다", "generating_screenshot": "스크린샷을 생성하는 중…", + "merged_scenes": "병합된 영상", "merged_tags": "병합된 태그", + "reassign_past_tense": "재할당된 파일", "removed_entity": "{singularEntity}을(를) 제거했습니다", "rescanning_entity": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} 다시 스캔하는 중…", "saved_entity": "{entity}를 저장했습니다", @@ -1090,6 +1105,9 @@ "type": "유형", "updated_at": "수정 날짜", "url": "URL", + "validation": { + "aliases_must_be_unique": "별칭은 유일해야 합니다" + }, "videos": "비디오", "view_all": "모두 보기", "weight": "몸무게", diff --git a/ui/v2.5/src/locales/nl-NL.json b/ui/v2.5/src/locales/nl-NL.json index cdbaa1c89..f2c708e5d 100644 --- a/ui/v2.5/src/locales/nl-NL.json +++ b/ui/v2.5/src/locales/nl-NL.json @@ -35,7 +35,7 @@ "download_backup": "Download Backup", "edit": "Bewerk", "edit_entity": "Wijzig {entityType}", - "export": "Exporteer…", + "export": "Exporteer", "export_all": "Exporteer Alles…", "find": "Zoek", "finish": "Klaar", @@ -590,7 +590,6 @@ "details": "Details", "developmentVersion": "Ontwikkelingsversie", "dialogs": { - "aliases_must_be_unique": "aliases moeten uniek zijn", "delete_alert": "De volgende {count, plural, one {{singularEntity}} other {{pluralEntity}}} zal permanent verwijderd worden:", "delete_confirm": "Weet je zeker dat je {entityName} wilt verwijderen?", "delete_entity_desc": "{count, plural, one {Weet u zeker dat u deze {singularEntity} wilt verwijderen? Tenzij het bestand ook wordt verwijderd, wordt deze {singularEntity} opnieuw toegevoegd wanneer de scan wordt uitgevoerd.} other {Weet u zeker dat u deze {pluralEntity} wilt verwijderen? Tenzij de bestanden ook worden verwijderd, worden deze {pluralEntity} opnieuw toegevoegd wanneer de scan wordt uitgevoerd.}}", @@ -862,7 +861,6 @@ "scenes": "Scènes", "scenes_updated_at": "Scène geüpdatet op", "search_filter": { - "add_filter": "Filter Toevoegen", "name": "Filter", "saved_filters": "Opgeslagen filters", "update_filter": "Filter Updaten" @@ -932,7 +930,7 @@ "your_system_has_been_created": "Succes! Uw systeem is aangemaakt!" }, "welcome": { - "config_path_logic_explained": "Stash probeert eerst zijn configuratiebestand (config.yml) uit de huidige werkdirectory te vinden, en als het daar niet gevonden wordt, valt het terug naar $HOME/.stash/config. yml (in Windows is dit %USERPROFILE%\\.stash\\config.yml). Je kunt Stash ook laten lezen uit een specifiek configuratiebestand door het uit te voeren met de opties -c of --config .", + "config_path_logic_explained": "Stash probeert eerst zijn configuratiebestand (config.yml) uit de huidige werkdirectory te vinden, en als het daar niet gevonden wordt, valt het terug naar $HOME/.stash/config. yml (in Windows is dit %USERPROFILE%\\.stash\\config.yml). Je kunt Stash ook laten lezen uit een specifiek configuratiebestand door het uit te voeren met de opties -c '' of --config ''.", "in_current_stash_directory": "In de $HOME/.stash map", "in_the_current_working_directory": "In de huidige werkdirectory", "next_step": "Met dat alles uit de weg, als u klaar bent om door te gaan met het opzetten van een nieuw systeem, kiest u waar u uw configuratiebestand wilt opslaan en klikt u op Volgende.", @@ -988,6 +986,9 @@ "twitter": "Twitter", "updated_at": "Bijgewerkt op", "url": "URL", + "validation": { + "aliases_must_be_unique": "aliases moeten uniek zijn" + }, "videos": "Video's", "view_all": "Alles weergeven", "weight": "Gewicht", diff --git a/ui/v2.5/src/locales/pl-PL.json b/ui/v2.5/src/locales/pl-PL.json index 0f9c8562e..0d7b05d21 100644 --- a/ui/v2.5/src/locales/pl-PL.json +++ b/ui/v2.5/src/locales/pl-PL.json @@ -6,6 +6,7 @@ "add_to_entity": "Dodaj do {entityType}", "allow": "Zezwól", "allow_temporarily": "Zezwól tymczasowo", + "anonymise": "Zanonimizuj", "apply": "Zastosuj", "auto_tag": "Automatyczne oznaczanie", "backup": "Kopia zapasowa", @@ -20,6 +21,7 @@ "confirm": "Zatwierdź", "continue": "Kontynuuj", "create": "Utwórz", + "create_chapters": "Utwórz rozdział", "create_entity": "Utwórz {entityType}", "create_marker": "Utwórz znacznik", "created_entity": "Utworzono {entity_type}: {entity_name}", @@ -32,10 +34,11 @@ "delete_stashid": "Usuń StashID", "disallow": "Nie zezwalaj", "download": "Pobierz", + "download_anonymised": "Pobierz zanonimizowane", "download_backup": "Pobierz kopię zapasową", "edit": "Edytuj", "edit_entity": "Edytuj {entityType}", - "export": "Eksportuj…", + "export": "Eksportuj", "export_all": "Eksportuj wszystko…", "find": "Znajdź", "finish": "Zakończ", @@ -58,6 +61,8 @@ "merge": "Scal", "merge_from": "Scal z", "merge_into": "Scal do", + "migrate_blobs": "Migruj bloby", + "migrate_scene_screenshots": "Migruj zrzuty ekranu ze scen", "next_action": "Dalej", "not_running": "nieuruchomiony", "open_in_external_player": "Otwórz w odtwarzaczu zewnętrznym", @@ -128,8 +133,13 @@ "birth_year": "Rok urodzenia", "birthdate": "Data urodzenia", "bitrate": "Szybkość transmisji", + "blobs_storage_type": { + "database": "Baza Danych", + "filesystem": "System plików" + }, "captions": "Napisy", "career_length": "Długość kariery", + "chapters": "Rozdziały", "component_tagger": { "config": { "active_instance": "Aktywna instancja stash-box:", @@ -183,6 +193,7 @@ "latest_version": "Najnowsza wersja", "latest_version_build_hash": "Hash kompilacji najnowszej wersji:", "new_version_notice": "[NOWA]", + "release_date": "Data wydania:", "stash_discord": "Dołącz do naszego kanału {url}", "stash_home": "Dom Stasha na stronie {url}", "stash_open_collective": "Wesprzyj nas poprzez {url}", @@ -253,7 +264,15 @@ "description": "Lokalizacja katalogu kopii zapasowych plików bazy danych SQLite", "heading": "Ścieżka katalogu kopii zapasowych" }, - "cache_location": "Lokalizacja katalogu pamięci podręcznej", + "blobs_path": { + "description": "Wskazuje miejsce na systemie plików w którym mają być przechowywane pliki binarne. Uwzględniane tylko jeśli wybrany został binarny tryb zapisu danych. UWAGA: zmiana wymaga ręcznego przeniesienia danych.", + "heading": "Ścieżka dla plików binarnych" + }, + "blobs_storage": { + "description": "Gdzie przechowywać dane binarne, takie jak okładki scen, obrazy wykonawców, studia i znaczników. Po zmianie tej wartości istniejące dane muszą zostać zmigrowane przy użyciu zadania Migruj bloby. Informacje na temat migracji znajdują się na stronie Zadania.", + "heading": "Tryb zapisu plików binarnych" + }, + "cache_location": "Lokalizacja katalogu pamięci podręcznej. Wymagane jeśli używany jest HLS (na przykład na urządzeniach Apple) lub DASH.", "cache_path_head": "Ścieżka pamięci podręcznej", "calculate_md5_and_ohash_desc": "Oblicz sumę kontrolną MD5 jako dodatek do oshash. Włączenie spowoduje, że początkowe skanowanie będzie wolniejsze. Aby wyłączyć obliczanie MD5, hash nazwy pliku musi być ustawiony na oshash.", "calculate_md5_and_ohash_label": "Obliczanie MD5 dla filmów", @@ -263,12 +282,43 @@ "chrome_cdp_path_desc": "Ścieżka do pliku wykonywalnego Chrome lub adres zdalny (zaczynający się od http:// lub https://, na przykład http://localhost:9222/json/version) do instancji Chrome.", "create_galleries_from_folders_desc": "Jeśli włączone, tworzy galerie z folderów zawierających obrazy.", "create_galleries_from_folders_label": "Tworzenie galerii z folderów zawierających obrazy", + "database": "Baza danych", "db_path_head": "Ścieżka bazy danych", "directory_locations_to_your_content": "Lokalizacje katalogów z Twoimi danymi", "excluded_image_gallery_patterns_desc": "Wyrażenia regularne dotyczące plików/ścieżek obrazów i galerii do wykluczenia ze skanowania i dodania do czyszczenia", "excluded_image_gallery_patterns_head": "Wykluczone wzorce obrazów/galerii", "excluded_video_patterns_desc": "Wyrażenia regularne plików/ścieżek wideo do wykluczenia ze skanowania i dodania do czyszczenia", "excluded_video_patterns_head": "Wykluczone wzorce wideo", + "ffmpeg": { + "hardware_acceleration": { + "desc": "Pozwala na transkodowanie wideo na żywo przy użyciu dostępnego sprzętu.", + "heading": "Kodowanie przy użyciu FFmpeg" + }, + "live_transcode": { + "input_args": { + "desc": "Zaawansowane: Dodatkowe argumenty do przekazania do FFmpeg przed polem wejściowym podczas transkodowania wideo na żywo.", + "heading": "Argumenty wejściowe dla transkodowani na żywo przy użyciou FFmpeg" + }, + "output_args": { + "desc": "Zaawansowane: Dodatkowe argumenty do przekazania do FFmpeg przed polem wyjściowym podczas transkodowania wideo na żywo.", + "heading": "Argumenty wyjścia dla transkodowania na żywo z użyciem FFmpeg" + } + }, + "transcode": { + "input_args": { + "desc": "Zaawansowane: Dodatkowe argumenty do przekazania do FFmpeg przed polem wejściowym podczas generowania wideo.", + "heading": "Argumenty wejściowe dla transkodowani na żywo przy użyciu FFmpeg" + }, + "output_args": { + "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" + } + } + }, + "funscript_heatmap_draw_range": "Bierz pod uwagę zakres dla wygenerowanych heatmap", + "funscript_heatmap_draw_range_desc": "Narysuj zakres ruchu na osi y generowanej heatmapy. Istniejące heatmapy będą musiały zostać ponownie wygenerowane po zmianie.", + "gallery_cover_regex_desc": "Regexp używany do identyfikacji obrazu jako okładki galerii", + "gallery_cover_regex_label": "Wzór dla okładki galerii", "gallery_ext_desc": "Rozdzielona przecinkami lista rozszerzeń plików, które będą identyfikowane jako pliki galerii zip.", "gallery_ext_head": "Rozszerzenia galerii zip", "generated_file_naming_hash_desc": "Użyj MD5 lub oshash dla wygenerowanych nazw plików. Zmiana tego ustawienia wymaga, aby wszystkie sceny miały uzupełnioną odpowiednią wartość MD5/oshash. Po zmianie tej wartości, istniejące wygenerowane pliki będą musiały zostać zmigrowane lub zregenerowane. Zobacz stronę Zadania, aby uzyskać informacje na temat migracji.", @@ -276,6 +326,7 @@ "generated_files_location": "Lokalizacja katalogu z wygenerowanymi plikami (znaczniki scen, podglądy scen, sprite'y itp.)", "generated_path_head": "Ścieżka wygenerowanych plików", "hashing": "Hashowanie", + "heatmap_generation": "Generowanie heatmap dla Funscript", "image_ext_desc": "Rozdzielona przecinkami lista rozszerzeń plików, które będą identyfikowane jako obrazy.", "image_ext_head": "Rozszerzenia obrazów", "include_audio_desc": "Uwzględnia strumień audio podczas generowania podglądu.", @@ -304,7 +355,7 @@ "heading": "Ścieżka do zbieraczy" }, "scraping": "Scrapowanie", - "sqlite_location": "Lokalizacja pliku dla bazy danych SQLite (wymaga ponownego uruchomienia)", + "sqlite_location": "Lokalizacja pliku dla bazy danych SQLite (wymaga ponownego uruchomienia). UWAGA: przechowywanie bazy danych w systemie innym niż ten na którym uruchomiony jest serwer Stash (np. w lokalizacji sieciowej) nie jest wspierane!", "video_ext_desc": "Rozdzielona przecinkami lista rozszerzeń plików, które będą identyfikowane jako pliki wideo.", "video_ext_head": "Rozszerzenia wideo", "video_head": "Wideo" @@ -346,6 +397,9 @@ }, "tasks": { "added_job_to_queue": "Dodano {operation_name} do kolejki zadań", + "anonymise_and_download": "Wykonuje zanonimizowaną kopię bazy danych i pobiera plik wynikowy.", + "anonymise_database": "Wykonuje kopię bazy danych do katalogu kopii zapasowych, anonimizując wszystkie wrażliwe dane. Kopia ta może być udostępniona innym osobom w celu rozwiązywania problemów i usuwania usterek. Oryginalna baza danych nie jest modyfikowana. Zanonimizowana baza danych używa formatu nazwy pliku {filename_format}.", + "anonymising_database": "Anonimizowanie bazy danych", "auto_tag": { "auto_tagging_all_paths": "Automatyczne tagowanie wszystkich ścieżek", "auto_tagging_paths": "Automatyczne tagowanie następujących ścieżek" @@ -372,6 +426,7 @@ "generate_previews_during_scan_tooltip": "Generowanie animowanych podglądów WebP, wymagane tylko wtedy, gdy opcja Typ podglądu jest ustawiona na Animowane obrazy.", "generate_sprites_during_scan": "Generowanie sprite'ów scrubberów", "generate_thumbnails_during_scan": "Generowanie miniatur dla obrazów", + "generate_video_covers_during_scan": "Generuj okładki dla scen", "generate_video_previews_during_scan": "Generowanie podglądów", "generate_video_previews_during_scan_tooltip": "Generowanie podglądów wideo, które są odtwarzane po najechaniu kursorem myszy na scenę", "generated_content": "Zawartość wygenerowana", @@ -399,7 +454,16 @@ "incremental_import": "Import przyrostowy z dostarczonego wyeksportowanego pliku zip.", "job_queue": "Kolejka zadań", "maintenance": "Konserwacja", + "migrate_blobs": { + "delete_old": "Usuń stare dane", + "description": "Migracja blobów do aktualnego systemu przechowywania blobów. Ta migracja powinna być uruchomiona po zmianie systemu przechowywania blobów. Może opcjonalnie usunąć stare dane po migracji." + }, "migrate_hash_files": "Używany po zmianie hasha nazw generowanych plików w celu zmiany nazwy istniejących wygenerowanych plików na nowy format hasha.", + "migrate_scene_screenshots": { + "delete_files": "Usuń pliki ze zrzutami ekranu ze scen", + "description": "Migracja zrzutów ekranu sceny do nowego systemu przechowywania blobów. Ta migracja powinna być uruchomiona po migracji istniejącego systemu do wersji 0.20. Opcjonalnie można usunąć stare zrzuty ekranu po migracji.", + "overwrite_existing": "Nadpisz istniejące BLOBy danymi ze zrzutów ekranu ze scen" + }, "migrations": "Migracje", "only_dry_run": "Wykonaj tylko próbę na sucho. Nie usuwa niczego", "plugin_tasks": "Zadania wtyczek", @@ -436,12 +500,12 @@ }, "basic_settings": "Ustawienia podstawowe", "custom_css": { - "description": "Strona musi zostać ponownie załadowana, aby zmiany zaczęły obowiązywać.", + "description": "Strona musi zostać ponownie załadowana, aby zmiany zaczęły obowiązywać. Nie ma gwarancji kompatybilności między niestandardowym CSS a przyszłymi wydaniami Stash.", "heading": "Własny CSS", "option_label": "Włączony własny CSS" }, "custom_javascript": { - "description": "Strona musi zostać ponownie załadowana, aby zmiany zaczęły obowiązywać.", + "description": "Strona musi zostać ponownie załadowana, aby zmiany zaczęły obowiązywać. Nie ma gwarancji zgodności między niestandardowym Javascriptem a przyszłymi wydaniami Stash.", "heading": "Własny Javascript", "option_label": "Własny Javascript włączony" }, @@ -471,19 +535,23 @@ "heading": "Wyłącz tworzenie za pomocą rozwijanej listy" }, "heading": "Edytowanie", + "max_options_shown": { + "label": "Maksymalna ilość obiektów pokazywanych w listach wyboru" + }, "rating_system": { "star_precision": { "label": "Precyzja oceniania gwiazdkami", "options": { "full": "Cała", "half": "Pół", - "quarter": "Ćwierć" + "quarter": "Ćwierć", + "tenth": "Części dziesiętne" } }, "type": { "label": "Rodzaj systemu oceny", "options": { - "decimal": "Liczby", + "decimal": "Liczba", "stars": "Gwiazdki" } } @@ -510,6 +578,11 @@ "image_lightbox": { "heading": "Pole podglądu obrazu" }, + "image_wall": { + "direction": "Kierunek", + "heading": "Ściana z obrazami", + "margin": "Margines (w pikselach)" + }, "images": { "heading": "Zdjęcia", "options": { @@ -653,6 +726,8 @@ }, "custom": "Własne", "date": "Data", + "date_format": "RRRR-MM-DD", + "datetime_format": "RRRR-MM-DD GG:MM", "death_date": "Data śmierci", "death_year": "Rok śmierci", "descending": "Malejąco", @@ -661,7 +736,6 @@ "details": "Szczegóły", "developmentVersion": "Wersja deweloperska", "dialogs": { - "aliases_must_be_unique": "aliasy muszą być unikatowe", "create_new_entity": "Dodaj {entity}", "delete_alert": "Następujące elementy {count, plural, one {{singularEntity}} other {{pluralEntity}}} zostaną trwale usunięte:", "delete_confirm": "Czy na pewno chcesz usunąć {entityName}?", @@ -677,6 +751,14 @@ "edit_entity_title": "Edytuj {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "export_include_related_objects": "Uwzględnij powiązane obiekty w eksporcie", "export_title": "Eksportuj", + "imagewall": { + "direction": { + "column": "Kolumna", + "description": "Układ oparty na kolumnach lub wierszach.", + "row": "Rząd" + }, + "margin_desc": "Liczba pikseli marginesu wokół całego obrazu." + }, "lightbox": { "delay": "Opóźnienie (s)", "display_mode": { @@ -686,6 +768,7 @@ "original": "Oryginalny" }, "options": "Opcje", + "page_header": "Strona {page} / {total}", "reset_zoom_on_nav": "Resetuj poziom powiększenia przy zmianie obrazu", "scale_up": { "description": "Skaluj mniejsze obrazy, aby wypełnić ekran", @@ -699,6 +782,8 @@ } }, "merge": { + "destination": "Cel", + "empty_results": "Wartości pola \"cel\" nie zostaną zmienione.", "source": "Źródło" }, "merge_tags": { @@ -706,7 +791,12 @@ "source": "Źródło" }, "overwrite_filter_confirm": "Czy na pewno chcesz nadpisać istniejące zapisane zapytanie {entityName}?", + "reassign_entity_title": "{count, plural, one {Przypisz {singularEntity}} other {Przypisz {pluralEntity}}}", + "reassign_files": { + "destination": "Przypisz ponownie do" + }, "scene_gen": { + "covers": "Okładki scen", "force_transcodes": "Wymuś generowanie transkodu", "force_transcodes_tooltip": "Domyślnie transkody są generowane tylko wtedy, gdy plik wideo nie jest obsługiwany przez przeglądarkę. Po włączeniu tej funkcji transkod będzie generowany nawet wtedy, gdy plik wideo wydaje się być obsługiwany przez przeglądarkę.", "image_previews": "Podglądy - Animowane obrazy", @@ -751,6 +841,7 @@ }, "dimensions": "Wymiary", "director": "Reżyser", + "disambiguation": "Ujednoznacznienie", "display_mode": { "grid": "Siatka", "list": "Lista", @@ -795,6 +886,11 @@ "warmth": "Ciepło" }, "empty_server": "Aby zobaczyć rekomendacje na tej stronie dodaj sceny do serwera.", + "errors": { + "image_index_greater_than_zero": "Indeks obrazu musi być większy niż 0", + "lazy_component_error_help": "Jeśli Stash został niedawno zaktualizowany, proszę przeładować stronę lub wyczyścić pamięć podręczną przeglądarki.", + "something_went_wrong": "Coś się zepsuło." + }, "ethnicity": "Pochodzenie etniczne", "existing_value": "istniejąca wartość", "eye_color": "Kolor oczu", @@ -842,6 +938,7 @@ "syncing": "Synchronizacja z serwerem", "uploading": "Wgrywanie skryptu" }, + "hasChapters": "Posiada Rozdziały", "hasMarkers": "Ma znaczniki", "height": "Wzrost", "height_cm": "Wzrost (cm)", @@ -849,6 +946,7 @@ "ignore_auto_tag": "Ignoruj automatyczne tagowanie", "image": "Obraz", "image_count": "Liczba obrazów", + "image_index": "Obraz nr", "images": "Zdjęcia", "include_parent_tags": "Uwzględnij tagi nadrzędne", "include_sub_studios": "Uwzględnić studia zależne", @@ -958,6 +1056,7 @@ "recently_released_objects": "Ostatnio wydane {objects}", "release_notes": "Informacje o wydaniu", "resolution": "Rozdzielczość", + "resume_time": "Rozpocznij od", "scene": "Scena", "sceneTagger": "Otagowywacz scen", "sceneTags": "Tagi sceny", @@ -970,7 +1069,7 @@ "scenes": "Sceny", "scenes_updated_at": "Scena aktualizowana", "search_filter": { - "add_filter": "Dodaj filtr", + "edit_filter": "Filtr Edycji", "name": "Filtr", "saved_filters": "Zapisane filtry", "update_filter": "Aktualizuj filtr" @@ -980,8 +1079,12 @@ "setup": { "confirm": { "almost_ready": "Jesteśmy prawie gotowi do zakończenia konfiguracji. Prosimy o potwierdzenie następujących ustawień. Możesz kliknąć przycisk Wstecz, aby zmienić wszystkie nieprawidłowe ustawienia. Jeśli wszystko wygląda dobrze, kliknij przycisk Zatwierdź, aby utworzyć system.", + "blobs_directory": "Katalog dla plików binarnych", + "cache_directory": "Katalog pamięci podręcznej", "configuration_file_location": "Lokalizacja pliku konfiguracyjnego:", "database_file_path": "Ścieżka do pliku bazy danych", + "default_blobs_location": "", + "default_cache_location": "<ścieżka zawierająca plik konfiguracyjny>/cache", "default_db_location": "<ścieżka zawierająca plik konfiguracyjny>/stash-go.sqlite", "default_generated_content_location": "<ścieżka zawierająca plik konfiguracyjny>/generated", "generated_directory": "Folder z wygenerowaną zawartością", @@ -1013,16 +1116,24 @@ "migration_notes": "Uwagi dotyczące migracji", "migration_required": "Wymagana migracja", "perform_schema_migration": "Wykonaj migrację schematów", - "schema_too_old": "Twoja obecna baza danych Stash to schemat w wersji {databaseSchema} i należy ją przenieść do wersji {appSchema}. Ta wersja Stash nie będzie działać bez migracji bazy danych. Jeśli nie chcesz migrować, musisz obniżyć wersję do wersji zgodnej ze schematem bazy danych." + "schema_too_old": "Twoja obecna baza danych Stash to schemat w wersji {databaseSchema} i należy ją zmigrować do wersji {appSchema}. Ta wersja Stash nie będzie działać bez migracji bazy danych. Jeśli nie chcesz migrować, musisz obniżyć wersję do wersji zgodnej ze schematem bazy danych." }, "paths": { "database_filename_empty_for_default": "nazwa pliku bazy danych (domyślnie puste)", - "description": "Następnie należy określić, gdzie ma się znajdować kolekcja porno, gdzie ma być przechowywana baza danych stash oraz wygenerowane pliki. Ustawienia te można zmienić później, jeśli zajdzie taka potrzeba.", + "description": "Następnie należy określić, gdzie ma się znajdować kolekcja porno oraz gdzie ma być przechowywana baza danych Stash, wygenerowane pliki i pliki pamięci podręcznej. Ustawienia te można zmienić później, jeśli zajdzie taka potrzeba.", + "path_to_blobs_directory_empty_for_database": "ścieżka do katalogu blobów (pusta, aby użyć bazy danych)", + "path_to_cache_directory_empty_for_default": "ścieżka do katalogu cache (domyślnie pusta)", "path_to_generated_directory_empty_for_default": "ścieżka do katalogu z wygenerowanymi plikami (domyślnie pusta)", "set_up_your_paths": "Ustaw ścieżki", "stash_alert": "Nie wybrano żadnych ścieżek biblioteki. Żaden materiał nie będzie mógł zostać zeskanowany przez aplikację Stash. Czy jesteś pewien?", + "where_can_stash_store_blobs": "Gdzie Stash może przechowywać dane binarne bazy danych?", + "where_can_stash_store_blobs_description": "Stash może przechowywać dane binarne, takie jak okładki scen, obrazy wykonawców, studia i znaczników, w bazie danych lub w systemie plików. Domyślnie przechowuje te dane w systemie plików w podkatalogu blobs. Jeśli chcesz to zmienić, podaj ścieżkę bezwzględną lub względną (do bieżącego katalogu roboczego). Stash utworzy ten katalog, jeśli jeszcze nie istnieje.", + "where_can_stash_store_blobs_description_addendum": "Alternatywnie, jeśli chcesz przechowywać te dane w bazie danych, możesz pozostawić to pole puste. Uwaga: Zwiększy to rozmiar pliku bazy danych i wydłuży czas migracji bazy danych.", + "where_can_stash_store_cache_files": "Gdzie Stash może przechowywać pliki pamięci podręcznej?", + "where_can_stash_store_cache_files_description": "Aby niektóre funkcjonalności takie jak transkodowanie na żywo HLS/DASH mogły działać, Stash wymaga katalogu cache dla plików tymczasowych. Domyślnie, Stash utworzy katalog cache w katalogu zawierającym twój plik konfiguracyjny. Jeśli chcesz to zmienić, podaj ścieżkę bezwzględną lub względną (do bieżącego katalogu roboczego). Stash utworzy ten katalog, jeśli jeszcze nie istnieje.", "where_can_stash_store_its_database": "Gdzie Stash może przechowywać swoją bazę danych?", - "where_can_stash_store_its_database_description": "Stash używa bazy danych sqlite do przechowywania metadanych porno. Domyślnie zostanie ona utworzona jako stash-go.sqlite w katalogu zawierającym twój plik konfiguracyjny. Jeśli chcesz to zmienić, podaj bezwzględną lub względną (względem bieżącego katalogu roboczego) nazwę pliku.", + "where_can_stash_store_its_database_description": "Stash używa bazy danych SQLite do przechowywania metadanych porno. Domyślnie zostanie ona utworzona jako stash-go.sqlite w katalogu zawierającym twój plik konfiguracyjny. Jeśli chcesz to zmienić, podaj bezwzględną lub względną (względem bieżącego katalogu roboczego) nazwę pliku.", + "where_can_stash_store_its_database_warning": "UWAGA: przechowywanie bazy danych w innym systemie niż ten, z którego uruchamiany jest Stash (np. przechowywanie bazy danych na dysku NAS przy jednoczesnym uruchomieniu serwera Stash na innym komputerze) jest nieobsługiwane! SQLite nie jest przeznaczony do pracy w sieci, a próba takiego działania może bardzo łatwo spowodować uszkodzenie całej bazy danych.", "where_can_stash_store_its_generated_content": "Gdzie Stash może przechowywać wygenerowaną zawartość?", "where_can_stash_store_its_generated_content_description": "Aby zapewnić miniatury, podglądy i sprite'y, Stash generuje obrazy i wideo. Obejmuje to również transkodowanie dla nieobsługiwanych formatów plików. Domyślnie Stash tworzy katalog generated w katalogu zawierającym plik konfiguracyjny. Jeśli chcesz zmienić miejsce przechowywania wygenerowanych multimediów, podaj ścieżkę bezwzględną lub względną (do bieżącego katalogu roboczego). Stash utworzy ten katalog, jeśli jeszcze nie istnieje.", "where_is_your_porn_located": "Gdzie znajduje się Twoje porno?", @@ -1042,7 +1153,7 @@ "your_system_has_been_created": "Sukces! Twój system został utworzony!" }, "welcome": { - "config_path_logic_explained": "Stash próbuje najpierw znaleźć swój plik konfiguracyjny (config.yml) w bieżącym katalogu roboczym, a jeśli go tam nie znajdzie, wraca do $HOME/.stash/config.yml (w systemie Windows będzie to %USERPROFILE%\\.stash\\config.yml). Można również sprawić, że Stash będzie odczytywał dane z określonego pliku konfiguracyjnego, uruchamiając go z opcją opcji -c <ścieżka do pliku konfiguracyjnego> lub --config <ścieżka do pliku konfiguracyjnego>.", + "config_path_logic_explained": "Stash próbuje najpierw znaleźć swój plik konfiguracyjny (config.yml) w bieżącym katalogu roboczym, a jeśli go tam nie znajdzie, wraca do $HOME/.stash/config.yml (w systemie Windows będzie to %USERPROFILE%\\.stash\\config.yml). Można również sprawić, że Stash będzie odczytywał dane z określonego pliku konfiguracyjnego, uruchamiając go z opcją opcji -c '<ścieżka do pliku konfiguracyjnego>' lub --config '<ścieżka do pliku konfiguracyjnego>'.", "in_current_stash_directory": "W katalogu $HOME/.stash", "in_the_current_working_directory": "W bieżącym katalogu roboczym", "next_step": "Jeśli jesteś gotowy do skonfigurowania nowego systemu, wybierz miejsce, w którym chcesz przechowywać plik konfiguracyjny, i kliknij przycisk Dalej.", @@ -1058,6 +1169,7 @@ "welcome_to_stash": "Witamy w Stashu" }, "stash_id": "ID Stasha", + "stash_id_endpoint": "Punkt końcowy Stash ID", "stash_ids": "ID Stasha", "stashbox": { "go_review_draft": "Przejdź do {endpoint_name}, aby zapoznać się z projektem.", @@ -1093,8 +1205,10 @@ "default_filter_set": "Domyślny zestaw filtrów", "delete_past_tense": "Usuń {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "generating_screenshot": "Generowanie zrzutu ekranu…", + "image_index_too_large": "Błąd: Indeks obrazu jest większy niż ilość obrazów w Galerii", "merged_scenes": "Połączone sceny", "merged_tags": "Połączone tagi", + "reassign_past_tense": "Plik przypisany ponownie", "removed_entity": "Usunięto {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "rescanning_entity": "Ponowne skanowanie {count, plural, one {{singularEntity}} other {{pluralEntity}}}…", "saved_entity": "Zapisano {entity}", @@ -1109,6 +1223,11 @@ "type": "Typ", "updated_at": "Zaktualizowano", "url": "URL", + "validation": { + "aliases_must_be_unique": "aliasy muszą być unikatowe", + "date_invalid_form": "${path} musi być w formacie RRRR-MM-DD", + "required": "${path} jest wymagana" + }, "videos": "Filmy wideo", "view_all": "Pokaż wszystko", "weight": "Waga", diff --git a/ui/v2.5/src/locales/pt-BR.json b/ui/v2.5/src/locales/pt-BR.json index c32f4dc60..17946e51b 100644 --- a/ui/v2.5/src/locales/pt-BR.json +++ b/ui/v2.5/src/locales/pt-BR.json @@ -35,7 +35,7 @@ "download_backup": "Download backup", "edit": "Editar", "edit_entity": "Editar {entityType}", - "export": "Exportar…", + "export": "Exportar", "export_all": "Exportar tudo…", "find": "Encontrar", "finish": "Finalizar", @@ -592,7 +592,6 @@ "details": "Detalhes", "developmentVersion": "Versão de desenvolvimento", "dialogs": { - "aliases_must_be_unique": "apelidos devem ser únicos", "delete_alert": "Os seguintes {count, plural, one {{singularEntity}} other {{pluralEntity}}} serão deletados permanentemente:", "delete_confirm": "Tem certeza de que deseja excluir {entityName}?", "delete_entity_desc": "{count, plural, one {Tem certeza de que deseja excluir este(a) {singularEntity}? A menos que o arquivo também esteja excluído, este(a) {singularEntity} será re-adicionado quando a escaneamento for executado.} other {Tem certeza de que deseja excluir estes(as) {pluralEntity}? A menos que os arquivos também estejam excluídos, estes(as) {pluralEntity} serão re-adicionados quando o escaneamento for executado.}}", @@ -884,7 +883,6 @@ "scenes": "Cenas", "scenes_updated_at": "Cena atualizada em", "search_filter": { - "add_filter": "Adicionar filtro", "name": "Filtro", "saved_filters": "Filtros salvos", "update_filter": "Atualizar filtro" @@ -956,7 +954,7 @@ "your_system_has_been_created": "Sucesso! Seu sistema foi criado!" }, "welcome": { - "config_path_logic_explained": "Stash tenta encontrar o arquivo de configuração (config.yml) a partir do diretório de trabalho atual, caso não o encontre lá, tentará encontra-lo em $HOME/.stash/config.yml (no Windows, será %USERPROFILE%\\.stash\\config.yml). Você também pode fazer o Stash ler um arquivo de configuração específico ao executar a aplicação com as opções -c ou --config .", + "config_path_logic_explained": "Stash tenta encontrar o arquivo de configuração (config.yml) a partir do diretório de trabalho atual, caso não o encontre lá, tentará encontra-lo em $HOME/.stash/config.yml (no Windows, será %USERPROFILE%\\.stash\\config.yml). Você também pode fazer o Stash ler um arquivo de configuração específico ao executar a aplicação com as opções -c '' ou --config ''.", "in_current_stash_directory": "No diretório $HOME/.stash", "in_the_current_working_directory": "No diretório de trabalho atual", "next_step": "Caso esteja pronto para criar um novo sistema, escolha onde você deseja armazenar seu arquivo de configuração e clique Próximo.", @@ -1021,6 +1019,9 @@ "type": "Tipo", "updated_at": "Atualizado em", "url": "URL", + "validation": { + "aliases_must_be_unique": "apelidos devem ser únicos" + }, "videos": "Vídeos", "view_all": "Ver todos", "weight": "Peso", diff --git a/ui/v2.5/src/locales/ro-RO.json b/ui/v2.5/src/locales/ro-RO.json index cdd229273..32b6b0cc4 100644 --- a/ui/v2.5/src/locales/ro-RO.json +++ b/ui/v2.5/src/locales/ro-RO.json @@ -32,7 +32,7 @@ "download_backup": "Descarcă Backup", "edit": "Editare", "edit_entity": "Editați {entityType}", - "export": "Export…", + "export": "Export", "export_all": "Exportați tot…", "find": "Găsire", "finish": "Terminare", @@ -112,7 +112,7 @@ "blacklist_label": "Lista neagră", "query_mode_auto": "Automat", "query_mode_auto_desc": "Folosește metadatele, dacă sunt prezente, sau numele fișierului", - "query_mode_dir": "Dir", + "query_mode_dir": "Director", "query_mode_dir_desc": "Folosește numai directorul părinte al fișierului video", "query_mode_filename": "Numele fișierului", "query_mode_filename_desc": "Folosește numai numele fișierului", @@ -388,7 +388,6 @@ "scenes": "Scene", "scenes_updated_at": "Scenă Actualizată La", "search_filter": { - "add_filter": "Adaugă Filtru", "name": "Filtru", "saved_filters": "Filtre Salvate", "update_filter": "Actualizează Filtru" diff --git a/ui/v2.5/src/locales/ru-RU.json b/ui/v2.5/src/locales/ru-RU.json index 5a8170859..650b6b1b5 100644 --- a/ui/v2.5/src/locales/ru-RU.json +++ b/ui/v2.5/src/locales/ru-RU.json @@ -6,6 +6,7 @@ "add_to_entity": "Добавить к {entityType}", "allow": "Разрешить", "allow_temporarily": "Временно разрешить", + "anonymise": "Анонимизировать", "apply": "Применить", "auto_tag": "Автоматически пометить тегом", "backup": "Резервная копия", @@ -20,6 +21,7 @@ "confirm": "Подтвердить", "continue": "Продолжить", "create": "Создать", + "create_chapters": "Создать раздел", "create_entity": "Создать {entityType}", "create_marker": "Создать Маркер", "created_entity": "Создано {entity_type}: {entity_name}", @@ -32,10 +34,11 @@ "delete_stashid": "Удалить StashID", "disallow": "Запретить", "download": "Скачать", + "download_anonymised": "Скачать анонимно", "download_backup": "Скачать резервную копию", "edit": "Изменить", "edit_entity": "Изменить {entityType}", - "export": "Экспортировать…", + "export": "Экспортировать", "export_all": "Экспортировать все…", "find": "Найти", "finish": "Завершить", @@ -58,6 +61,8 @@ "merge": "Слияние", "merge_from": "Слияние из", "merge_into": "Слияние в", + "migrate_blobs": "Перенос блоков", + "migrate_scene_screenshots": "Перенос скриншотов сцены", "next_action": "Вперёд", "not_running": "не запущен", "open_in_external_player": "Открыть во внешнем проигрывателе", @@ -128,8 +133,13 @@ "birth_year": "Год рождения", "birthdate": "Дата рождения", "bitrate": "Битрейт", + "blobs_storage_type": { + "database": "База данных", + "filesystem": "Файловая система" + }, "captions": "Субтитры", "career_length": "Продолжительность карьеры", + "chapters": "Разделы", "component_tagger": { "config": { "active_instance": "Активная инстанция Stash-устройства:", @@ -269,6 +279,17 @@ "excluded_image_gallery_patterns_head": "Шаблоны исключений изображений/галерей", "excluded_video_patterns_desc": "Регулярные выражения видеофайлов/путей которые будут исключены при Сканировании, и добавлены в Очистку", "excluded_video_patterns_head": "Шаблоны исключенные видео", + "ffmpeg": { + "transcode": { + "input_args": { + "desc": "Расширенные настройки: Дополнительные параметры для генерации видео при помощи ffmpeg.", + "heading": "Входные параметры транскодирования FFmpeg" + }, + "output_args": { + "heading": "Выходные параметры транскодирования FFmpeg" + } + } + }, "gallery_ext_desc": "Список расширений через запятую, которые будут распознаны как архивы с картинками.", "gallery_ext_head": "Расширения архивов галерей", "generated_file_naming_hash_desc": "Использовать MD5 или oshash для имен сгенерированных файлов. Меняя это, необходимо чтобы у всех сцен имелись соответствующие MD5/oshash значение. После изменения значения, существующие сгенерированные файлы нужно будет перенести или сгенерировать повторно. Подробнее о переносе на странице Задач.", @@ -649,7 +670,7 @@ "not_between": "не между", "not_equals": "является не", "not_matches_regex": "не соответствует регулярному выражению", - "not_null": "не равняется ничему" + "not_null": "не пуст" }, "custom": "Пользовательский", "date": "Дата", @@ -661,7 +682,6 @@ "details": "Подробности", "developmentVersion": "Версия разработки", "dialogs": { - "aliases_must_be_unique": "псевдонимы должны быть уникальными", "create_new_entity": "Создать новую запись в {entity}", "delete_alert": "Следующие {count, plural, one {{singularEntity}} other {{pluralEntity}}} будут удалены безвозвратно:", "delete_confirm": "Вы уверены что хотите удалить {entityName}?", @@ -757,6 +777,7 @@ }, "dimensions": "Размер", "director": "Режиссер", + "disambiguation": "Многозначность", "display_mode": { "grid": "Сетка", "list": "Список", @@ -977,7 +998,6 @@ "scenes": "Сцены", "scenes_updated_at": "Сцена обновлена в", "search_filter": { - "add_filter": "Добавить фильтр", "name": "Фильтр", "saved_filters": "Сохраненные фильтры", "update_filter": "Обновить фильтр" @@ -1049,7 +1069,7 @@ "your_system_has_been_created": "Готово! Ваша система создана!" }, "welcome": { - "config_path_logic_explained": "Stash сначала пытается найти свой файл конфигурации (config.yml) в текущем рабочем каталоге, и, если он не находит его там, возвращается к $HOME/.stash/config.yml (в Windows это будет %USERPROFILE%\\.stash\\config.yml). Вы также можете заставить Stash читать из определенного файла конфигурации, запустив его с параметрами -c <путь к файлу конфигурации> или --config <путь к файлу конфигурации>.", + "config_path_logic_explained": "Stash сначала пытается найти свой файл конфигурации (config.yml) в текущем рабочем каталоге, и, если он не находит его там, возвращается к $HOME/.stash/config.yml (в Windows это будет %USERPROFILE%\\.stash\\config.yml). Вы также можете заставить Stash читать из определенного файла конфигурации, запустив его с параметрами -c '<путь к файлу конфигурации>' или --config '<путь к файлу конфигурации>'.", "in_current_stash_directory": "В $HOME/.stash каталоге", "in_the_current_working_directory": "В текущем рабочем каталоге", "next_step": "После всего этого, если вы готовы приступить к настройке новой системы, выберите, где вы хотите сохранить файл конфигурации, и нажмите «Далее».", @@ -1118,6 +1138,9 @@ "type": "Тип", "updated_at": "Обновлено", "url": "Ссылка", + "validation": { + "aliases_must_be_unique": "псевдонимы должны быть уникальными" + }, "videos": "Видео", "view_all": "Показать все", "weight": "Вес", diff --git a/ui/v2.5/src/locales/sv-SE.json b/ui/v2.5/src/locales/sv-SE.json index 64f41fdea..c88229303 100644 --- a/ui/v2.5/src/locales/sv-SE.json +++ b/ui/v2.5/src/locales/sv-SE.json @@ -6,6 +6,7 @@ "add_to_entity": "Lägg till {entityType}", "allow": "Tillåt", "allow_temporarily": "Tillåt tillfälligt", + "anonymise": "Anonymisera", "apply": "Tillämpa", "auto_tag": "Tagga automatiskt", "backup": "Säkerhetskopiera", @@ -20,6 +21,7 @@ "confirm": "Bekräfta", "continue": "Fortsätt", "create": "Skapa", + "create_chapters": "Skapa Kapitel", "create_entity": "Skapa {entityType}", "create_marker": "Skapa markör", "created_entity": "Skapade {entity_type}: {entity_name}", @@ -32,10 +34,11 @@ "delete_stashid": "Radera StashID", "disallow": "Tillåt ej", "download": "Ladda ner", + "download_anonymised": "Ladda ner anonymiserad", "download_backup": "Ladda ner säkerhetskopia", "edit": "Redigera", "edit_entity": "Redigera {entityType}", - "export": "Exportera…", + "export": "Exportera", "export_all": "Exportera alla…", "find": "Sök", "finish": "Slutför", @@ -58,6 +61,8 @@ "merge": "Slå samman", "merge_from": "Slå samman från", "merge_into": "Slå samman till", + "migrate_blobs": "Migrera Blobbar", + "migrate_scene_screenshots": "Migrera Scenbilder", "next_action": "Nästa", "not_running": "körs ej", "open_in_external_player": "Öppna i extern spelare", @@ -67,6 +72,7 @@ "play_selected": "Spela vald", "preview": "Förhandsvisa", "previous_action": "Backa", + "reassign": "Omplacera", "refresh": "Uppdatera", "reload_plugins": "Ladda om tillägg", "reload_scrapers": "Ladda om skrapare", @@ -99,10 +105,12 @@ "show": "Visa", "show_configuration": "Visa konfigureringen", "skip": "Hoppa över", + "split": "Dela", "stop": "Stoppa", "submit": "Skicka", "submit_stash_box": "Skicka till Stash-Box", "submit_update": "Skicka uppdatering", + "swap": "Byt", "tasks": { "clean_confirm_message": "Är du säker att du vill rensa? Detta kommer radera databasinformation och genererade filer för alla scener och gallerier som inte längre finns på filsystemet.", "dry_mode_selected": "Torrt läge valt. Inget kommer raderas utan bara loggning kommer ske.", @@ -121,11 +129,17 @@ "also_known_as": "Även känd som", "ascending": "Stigande", "average_resolution": "Genomsnittlig upplösning", + "between_and": "och", "birth_year": "Födelseår", "birthdate": "Födelsedatum", "bitrate": "Bithastighet", + "blobs_storage_type": { + "database": "Databas", + "filesystem": "Filsystem" + }, "captions": "Undertexter", "career_length": "Karriärlängd", + "chapters": "Kapitel", "component_tagger": { "config": { "active_instance": "Aktiv stash-box instans:", @@ -179,6 +193,7 @@ "latest_version": "Senaste version", "latest_version_build_hash": "Senaste versionens bygghash:", "new_version_notice": "[Ny]", + "release_date": "Utgivningsdatum:", "stash_discord": "Gå med i vår {url}-kanal", "stash_home": "Stasha hemma vid {url}", "stash_open_collective": "Stötta oss genom {url}", @@ -249,7 +264,15 @@ "description": "Filsökväg för SQLite-databas backupfil", "heading": "Backup filsökväg" }, - "cache_location": "Mappsökväg till cache", + "blobs_path": { + "description": "Var i filsystemet som binär data ska lagras. Används bara när Blobbar lagras i Filsystemet. VARNING: ändring kräver manuell flytt av redan existerande data.", + "heading": "Binär data filsystemssökväg" + }, + "blobs_storage": { + "description": "Var binär data som scenomslag, stjärnor-, studio-, och taggbilder ska lagras. Efter ändring måste den redan existerande datan migreras genom Migrera Blobbar-uppgiften. Se Uppgifter-sidan för migrering.", + "heading": "Binär data lagringstyp" + }, + "cache_location": "Mappsökväg till cache. Krävs om strömning använder HLS (som på Apple-enheter) eller DASH.", "cache_path_head": "Cache-sökväg", "calculate_md5_and_ohash_desc": "Beräkna MD5 checksumma i tillägg med ohash. Aktivering kan sakta ner första skanningar. Hashen måste vara vald till ohash för att avaktivera MD5-beräkning.", "calculate_md5_and_ohash_label": "Beräkna MD5 för videor", @@ -259,12 +282,43 @@ "chrome_cdp_path_desc": "Sökväg till Chrome-programfilen, eller en fjärradress (börjar med http:// eller https://, till exempel http://localhost:9222/json/version) till en Chrome-instans.", "create_galleries_from_folders_desc": "Om sant, skapar gallerier från mappar som innehåller bilder.", "create_galleries_from_folders_label": "Skapa gallerier från mappar som innehåller bilder", + "database": "Databas", "db_path_head": "Databassökväg", "directory_locations_to_your_content": "Sökväg till ditt innehåll", "excluded_image_gallery_patterns_desc": "Regexps av bilder och gallery filer/sökväg att exkludera från Skanna och lägga till på Rensa", "excluded_image_gallery_patterns_head": "Mönster för Bild/Galleri exklusion", "excluded_video_patterns_desc": "Regexps av video filer/sökväg att exkludera från Skanna och lägga till på Rensa", "excluded_video_patterns_head": "Exluderade video-mönster", + "ffmpeg": { + "hardware_acceleration": { + "desc": "Använder tillgänglig hårdvara för liveomkodning av video.", + "heading": "FFmpeg hårdvaruomkodning" + }, + "live_transcode": { + "input_args": { + "desc": "Avancerat: Ytterligare argument att skicka till ffmpeg innan input-fältet vid live videogenerering.", + "heading": "FFmpeg Liveomkodning Input Argument" + }, + "output_args": { + "desc": "Avancerat: Ytterligare argument att skicka till ffmpeg innan output-fältet vid live videogenerering.", + "heading": "FFmpeg Liveomkodning Output Argument" + } + }, + "transcode": { + "input_args": { + "desc": "Avancerat: Ytterligare argument att skicka till ffmpeg innan input-fältet vid liveomkodning av video.", + "heading": "FFmpeg Liveomkodning Input Argument" + }, + "output_args": { + "desc": "Avancerat: Ytterligare argument att skicka till ffmpeg innan output-fältet vid videogenerering.", + "heading": "FFmpeg Omkodning Output Argument" + } + } + }, + "funscript_heatmap_draw_range": "Inkludera räckvidd i genererade värmekartor", + "funscript_heatmap_draw_range_desc": "Visa y-axelns rörelseräckvidd på den genererade värmekartan. Existerande värmekartor kommer behöva genereras om efter ändring.", + "gallery_cover_regex_desc": "Regexp som används för att identifiera en bild som galleriomslag", + "gallery_cover_regex_label": "Galleriomslagsmönster", "gallery_ext_desc": "Komma-avgränsad lista av filtillägg som kommer identifieras som galleri zip-filer.", "gallery_ext_head": "Galleri zip-tillägg", "generated_file_naming_hash_desc": "Använd MD5 eller ohash för att döpa genererade filer. Ett byte kräver att alla scener har ett värde för MD5/oshash. Efter ett byte måste existerande genererade filer migreras eller återgenereras. Se Job-sidan för migration.", @@ -272,6 +326,7 @@ "generated_files_location": "Mappsökväg för genererade filer (markörer, förhandsvisningar, sprites, m.m.)", "generated_path_head": "Genererad filsökväg", "hashing": "Hashande", + "heatmap_generation": "Funscript Värmekarta Generering", "image_ext_desc": "Kommaavgränsad lista av filändelser som kommer identifieras som bilder.", "image_ext_head": "Bildfiländelser", "include_audio_desc": "Inkluderar ljud vid förhandsvisningsgeneration.", @@ -300,7 +355,7 @@ "heading": "Skraparnas sökväg" }, "scraping": "Skrapning", - "sqlite_location": "Filsökväg för SQLite databasen (kräver omstart)", + "sqlite_location": "Filsökväg för SQLite databasen (kräver omstart). VARNING: lagring av databasen på ett annat system än det som Stash körs ifrån (t.ex. över nätvärket) är inte stöttat!", "video_ext_desc": "Kommaavgränsad lista av filändelser som kommer identifieras som videor.", "video_ext_head": "Videofiländelser", "video_head": "Video" @@ -342,6 +397,9 @@ }, "tasks": { "added_job_to_queue": "Köade {operation_name}", + "anonymise_and_download": "Skapar en anonymiserad kopia av databasen och laddar ner kopian.", + "anonymise_database": "Skapar en kopia av databasen till backup-sökvägen och anonymiserar all känslig data. Denna kan sedan skickas till andra för felsökning och problemlösning. Originaldatabasen påverkas ej. Anonymiserad databas använder filnamnsformatet {filename_format}.", + "anonymising_database": "Anonymiserar databas", "auto_tag": { "auto_tagging_all_paths": "Autotagga alla sökvägar", "auto_tagging_paths": "Autotagga följande sökvägar" @@ -368,6 +426,7 @@ "generate_previews_during_scan_tooltip": "Generera animera WebP-förhandsvisningar, krävs bara om Typ av Förhandsvisning är Animerad Bild.", "generate_sprites_during_scan": "Generera sprites för videoskrubbaren", "generate_thumbnails_during_scan": "Generera miniatyrer för bilder", + "generate_video_covers_during_scan": "Generera scenomslag", "generate_video_previews_during_scan": "Generera förhandsvisningar", "generate_video_previews_during_scan_tooltip": "Generera videoförhandsvisningar som spelas när man håller över en video", "generated_content": "Genererat Material", @@ -395,7 +454,16 @@ "incremental_import": "Stegvis import från en exporterad zip-fil.", "job_queue": "Uppgiftskö", "maintenance": "Underhåll", + "migrate_blobs": { + "delete_old": "Radera gammal data", + "description": "Migrera blobbar till det nuvarande bloblagringssystemet. Denna migrering borde köras efter ändring av bloblagringssystemet. Kan också radera den gamla datan efter migreringen." + }, "migrate_hash_files": "Används efter att genererade filers namn-hash har ändrats för att döpa om existerande filer till ny namnhash.", + "migrate_scene_screenshots": { + "delete_files": "Radera skärmbildsfiler", + "description": "Migrera skärmbilder till det nuvarande bloblagringssystemet. Denna migrering borde köras efter uppdatering av ett äldre system till 0.20. Kan också radera de gamla skärmbilderna efter migreringen.", + "overwrite_existing": "Skriv över redan existerande blobbar med skärmbildsdata" + }, "migrations": "Migration", "only_dry_run": "Kör bara en torr rensning. Radera ingenting", "plugin_tasks": "Tilläggsuppgifter", @@ -432,14 +500,19 @@ }, "basic_settings": "Grundinställningar", "custom_css": { - "description": "Sidan måste laddas om för att ändringar ska ta effekt.", + "description": "Sidan måste laddas om för att ändringar ska ta effekt. Det finns ingen garanti att anpassad CSS kommer vara kompatibel med framtida versioner av Stash.", "heading": "Anpassad CSS", "option_label": "Använd anpassad CSS" }, + "custom_javascript": { + "description": "Sidan måste laddas om för att ändringar ska ta effekt. Det finns ingen garanti att anpassad Javascript kommer vara kompatibel med framtida versioner av Stash.", + "heading": "Anpassad Javascript", + "option_label": "Använd anpassad Javascript" + }, "custom_locales": { "description": "Ändra individuella lokala strängar. Se https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/locales/en-GB.json för kompletta listan. Sidan måste laddas om för att ändringar ska ske.", - "heading": "Egen översättning", - "option_label": "Aktiverat egen översättning" + "heading": "Anpassad översättning", + "option_label": "Använd anpassad översättning" }, "delete_options": { "description": "Standardalternativ vid radering av bilder, gallerier och scener.", @@ -461,7 +534,28 @@ "description": "Ta bort möjligheten att skapa nya objekt från dropdown-menyer", "heading": "Stäng av skapande via dropdowns" }, - "heading": "Redigering" + "heading": "Redigering", + "max_options_shown": { + "label": "Maximalt antal objekt att visa i dropdowns" + }, + "rating_system": { + "star_precision": { + "label": "Betygstjärnors precision", + "options": { + "full": "Hel", + "half": "Halv", + "quarter": "Fjärdedel", + "tenth": "Tiondel" + } + }, + "type": { + "label": "Typ av betygsystem", + "options": { + "decimal": "Decimal", + "stars": "Stjärnor" + } + } + } }, "funscript_offset": { "description": "Tidsfördröjning i millisekunder för interaktiva skripter.", @@ -484,6 +578,11 @@ "image_lightbox": { "heading": "Bild Ljuslåda" }, + "image_wall": { + "direction": "Riktning", + "heading": "Bildvägg", + "margin": "Åtskiljare (pixlar)" + }, "images": { "heading": "Bilder", "options": { @@ -505,6 +604,10 @@ "description": "Visa eller göm olika flikar på navigationsremsan", "heading": "Menyobjekt" }, + "minimum_play_percent": { + "description": "I procent, hur mycket av scenen som måste spelas innan visningsräknaren ökar.", + "heading": "Minimal Visningsprocent" + }, "performers": { "options": { "image_location": { @@ -531,6 +634,7 @@ "scene_player": { "heading": "Scenspelaren", "options": { + "always_start_from_beginning": "Starta alltid video från början", "auto_start_video": "Starta videouppspelning automatiskt", "auto_start_video_on_play_selected": { "description": "Autostarta uppspelning från kön, valda eller slumpade från Scener-sidan", @@ -540,11 +644,12 @@ "description": "Spela nästa scen i kö efter scenens slut", "heading": "Fortsätt spellista som standard" }, - "show_scrubber": "Visa skrubbaren" + "show_scrubber": "Visa skrubbaren", + "track_activity": "Spåra Aktivitet" } }, "scene_wall": { - "heading": "Scen / Markör Vägg", + "heading": "Scen / Markörvägg", "options": { "display_title": "Visa titel och taggar", "toggle_sound": "Aktivera ljud" @@ -621,6 +726,8 @@ }, "custom": "Anpassad", "date": "Datum", + "date_format": "ÅÅÅÅ-MM-DD", + "datetime_format": "ÅÅÅÅ-MM-DD HH:MM", "death_date": "Dödsdatum", "death_year": "Dödsår", "descending": "Fallande", @@ -629,7 +736,7 @@ "details": "Beskrivningar", "developmentVersion": "Utvecklingsversion", "dialogs": { - "aliases_must_be_unique": "alias måste vara unik", + "create_new_entity": "Skapa ny {entity}", "delete_alert": "De följande {count, plural, one {{singularEntity}} andra {{pluralEntity}}} kommer raderas permanent:", "delete_confirm": "Är du säker på att du vill radera {entityName}?", "delete_entity_desc": "{count, plural, one {Är du säker att du vill radera detta {singularEntity}? Sålänge filen inte också raderas, kommer {singularEntity} att läggas till igen vid nästa skanning.} other {Är du säker att du vill radera dessa {pluralEntity}? Sålänge filen inte också raderas, kommer dessa {pluralEntity} att läggas till igen vid nästa skanning.}}", @@ -644,6 +751,14 @@ "edit_entity_title": "Redigera {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "export_include_related_objects": "Inkludera relaterade objekt i exporten", "export_title": "Exportera", + "imagewall": { + "direction": { + "column": "Kolumn", + "description": "Kolumn eller radbaserad layout.", + "row": "Rad" + }, + "margin_desc": "Antal åtskiljande pixlar runt varje bild." + }, "lightbox": { "delay": "Fördröjning (Sekund)", "display_mode": { @@ -653,6 +768,7 @@ "original": "Original" }, "options": "Alternativ", + "page_header": "Sida {page} / {total}", "reset_zoom_on_nav": "Återställ zoom vid bildbyte", "scale_up": { "description": "Skala mindre bilder så att de fyller skärmen", @@ -665,12 +781,22 @@ "zoom": "Zooma" } }, + "merge": { + "destination": "Destination", + "empty_results": "Destinationens värden kommer ej ändras.", + "source": "Källa" + }, "merge_tags": { "destination": "Mål", "source": "Källa" }, "overwrite_filter_confirm": "Är du säker på att du vill skriva över existerande sökning {entityName}?", + "reassign_entity_title": "{count, plural, one {Omplacera {singularEntity}} other {Omplacera {pluralEntity}}}", + "reassign_files": { + "destination": "Omplacera till" + }, "scene_gen": { + "covers": "Scenomslag", "force_transcodes": "Tvinga omkodning", "force_transcodes_tooltip": "Som standard blir videofiler bara omkodade om formatet inte stöds i webbläsaren. Vid aktivering kommer alla videofiler bli omkodande, även om de antas vara stöttade i webbläsaren.", "image_previews": "Animerad Bildförhandsvisning", @@ -715,6 +841,7 @@ }, "dimensions": "Mått", "director": "Regissör", + "disambiguation": "Särskiljning", "display_mode": { "grid": "Rutnät", "list": "Lista", @@ -759,6 +886,11 @@ "warmth": "Värme" }, "empty_server": "Lägg till några scener i Stash för att se rekommendationer på denna sida.", + "errors": { + "image_index_greater_than_zero": "Bildindex måste vara större än 0", + "lazy_component_error_help": "Om du nyligen uppdaterade Stash ladda om sidan eller rensa din webbläsares cache.", + "something_went_wrong": "Något gick fel." + }, "ethnicity": "Etnicitet", "existing_value": "existerande värde", "eye_color": "Ögonfärg", @@ -770,6 +902,7 @@ "file_info": "Filinfo", "file_mod_time": "Filens Modifikationstid", "files": "filer", + "files_amount": "{value} filer", "filesize": "Filstorlek", "filter": "Filter", "filter_name": "Filternamn", @@ -805,13 +938,15 @@ "syncing": "Synkar med server", "uploading": "Laddar upp skript" }, - "hasMarkers": "Har markörer", + "hasChapters": "Har Kapitel", + "hasMarkers": "Har Markörer", "height": "Längd", "height_cm": "Längd (cm)", "help": "Hjälp", "ignore_auto_tag": "Ignorera Autotagg", "image": "Bild", - "image_count": "Antal bilder", + "image_count": "Antal Bilder", + "image_index": "Bild #", "images": "Bilder", "include_parent_tags": "Inkludera överordnade taggar", "include_sub_studios": "Inkludera underordnade studior", @@ -820,6 +955,7 @@ "interactive": "Interaktiv", "interactive_speed": "Interaktiv hastighet", "isMissing": "Saknas", + "last_played_at": "Senast spelad", "library": "Bibliotek", "loading": { "generic": "Laddar…" @@ -838,6 +974,8 @@ "age_context": "{age} {years_old} i den här scenen" }, "phash": "PHash", + "play_count": "Visningar", + "play_duration": "Uppspelad tid", "stream": "Ström", "video_codec": "Videokodek" }, @@ -908,6 +1046,8 @@ }, "performers": "Stjärnor", "piercings": "Piercingar", + "play_count": "Visningar", + "play_duration": "Uppspelad tid", "primary_file": "Primär fil", "queue": "Kö", "random": "Slumpad", @@ -916,16 +1056,20 @@ "recently_released_objects": "Nyligen Släppta {objects}", "release_notes": "Versionsfakta", "resolution": "Upplösning", + "resume_time": "Återupptagningstid", "scene": "Scen", "sceneTagger": "Scentaggaren", "sceneTags": "Scentaggar", "scene_code": "Studiokod", "scene_count": "Antal scener", + "scene_created_at": "Scenen Skapad", + "scene_date": "Scenens Datum", "scene_id": "Scenens ID", + "scene_updated_at": "Scenen Uppdaterad", "scenes": "Scener", "scenes_updated_at": "Scen uppdaterad vid", "search_filter": { - "add_filter": "Lägg till filter", + "edit_filter": "Ändra Filter", "name": "Filter", "saved_filters": "Sparade filter", "update_filter": "Uppdatera filter" @@ -935,8 +1079,12 @@ "setup": { "confirm": { "almost_ready": "Vi är nästan redo att slutfärdiga konfigurationen. Bekräfta följande inställningar. Du kan trycka bakåt för att ändra något inkorrekt. Tryck på Bekräfta om allting ser korrekt ut.", + "blobs_directory": "Binär data mappsökväg", + "cache_directory": "Cache mappsökväg", "configuration_file_location": "Konfiguration filsökväg:", "database_file_path": "Databas filsökväg", + "default_blobs_location": "", + "default_cache_location": "/cache", "default_db_location": "/stash-go.sqlite", "default_generated_content_location": "/generated", "generated_directory": "Genererat sökväg", @@ -972,12 +1120,20 @@ }, "paths": { "database_filename_empty_for_default": "databasfilnamn (blank för standard)", - "description": "Härnäst, måste vi avgöra var Stash hittar din samling med porr och vart databasen och de genererade filerna 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 porrsamling, och vart 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_database": "sökväg till blobmappen (tomt för att använda databas)", + "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)", "set_up_your_paths": "Ställ in dina sökvägar", "stash_alert": "Inga bibliotekssökvägar har valts. Ingen media kommer kunna skannas in i Stash. Är du säker?", + "where_can_stash_store_blobs": "Var kan Stash lagra databasens binära data?", + "where_can_stash_store_blobs_description": "Stash kan lagra binär data som scenomslag, stjärn-, studio-, och taggbilder i antingen databasen eller på filsystemet. Som standard, kommer datan lagras på filsystemet i mappen Blobbar. 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_blobs_description_addendum": "Alternativt om du vill lagra denna data i databasen kan du lämna detta fält tomt. Notis: Detta kommer öka storleken på databasfilen och kommer förlänga databasmigrationstider.", + "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 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_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?", @@ -997,7 +1153,7 @@ "your_system_has_been_created": "Framgång! Ditt system har skapats!" }, "welcome": { - "config_path_logic_explained": "Stash försöker att hitta sin konfigurationsfil (config.yml) från den nuvarande arbetsmappen först, och om den inte finns där, letar den i $HOME/.stash/config.yml (för Windows är detta %USERPROFILE%\\.stash\\config.yml). Du kan också låta Stash läsa en specifik konfigurationsfil genom att starta den med -c eller --config inställningarna.", + "config_path_logic_explained": "Stash försöker att hitta sin konfigurationsfil (config.yml) från den nuvarande arbetsmappen först, och om den inte finns där, letar den i $HOME/.stash/config.yml (för Windows är detta %USERPROFILE%\\.stash\\config.yml). Du kan också låta Stash läsa en specifik konfigurationsfil genom att starta den med -c '' eller --config '' inställningarna.", "in_current_stash_directory": "I mappen $HOME/.stash", "in_the_current_working_directory": "I den nuvarande arbetsmappen", "next_step": "Med allt det färdigt, kan vi fortsätta med uppstarten om du är redo. Välj var du skulle vilja lagra din konfigurationsfil och tryck på Nästa.", @@ -1013,6 +1169,7 @@ "welcome_to_stash": "Välkommen till Stash" }, "stash_id": "Stash ID", + "stash_id_endpoint": "Stash ID Slutpunkt", "stash_ids": "Stash ID:er", "stashbox": { "go_review_draft": "Gå till {endpoint_name} för att granska utkast.", @@ -1048,7 +1205,10 @@ "default_filter_set": "Standardfilter valt", "delete_past_tense": "Raderade {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "generating_screenshot": "Genererar skärmbild…", + "image_index_too_large": "Fel: BIldindex är större än antalet bilder i Galleriet", + "merged_scenes": "Sammanslagna scener", "merged_tags": "Slog samman taggar", + "reassign_past_tense": "Fil omplacerad", "removed_entity": "Tog bort {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "rescanning_entity": "Återskannar {count, plural, one {{singularEntity}} other {{pluralEntity}}}…", "saved_entity": "Sparade {entity}", @@ -1063,6 +1223,11 @@ "type": "Typ", "updated_at": "Uppdaterad vid", "url": "URL", + "validation": { + "aliases_must_be_unique": "alias måste vara unik", + "date_invalid_form": "${path} måste vara i formatet ÅÅÅÅ-MM-DD", + "required": "${path} är ett obligatoriskt fält" + }, "videos": "Videor", "view_all": "Visa Allt", "weight": "Vikt", diff --git a/ui/v2.5/src/locales/th-TH.json b/ui/v2.5/src/locales/th-TH.json index 2b8085f7c..4e96c7c7b 100644 --- a/ui/v2.5/src/locales/th-TH.json +++ b/ui/v2.5/src/locales/th-TH.json @@ -35,7 +35,7 @@ "download_backup": "ดาวน์โหลดข้อมูลสำรอง", "edit": "แก้ไข", "edit_entity": "แก้ไข{entityType}", - "export": "ส่งออก…", + "export": "ส่งออก", "export_all": "ส่งออกทั้งหมด…", "find": "ค้นหา", "finish": "เสร็จ", diff --git a/ui/v2.5/src/locales/tr-TR.json b/ui/v2.5/src/locales/tr-TR.json index c7735e1ba..476b9fe17 100644 --- a/ui/v2.5/src/locales/tr-TR.json +++ b/ui/v2.5/src/locales/tr-TR.json @@ -31,7 +31,7 @@ "download": "İndir", "download_backup": "Yedekleme Dosyasını İndir", "edit": "Düzenle", - "export": "Dışa Aktar…", + "export": "Dışa Aktar", "export_all": "Tümünü dışa aktar…", "find": "Bul", "finish": "Bitir", @@ -547,7 +547,6 @@ "details": "Ayrıntılar", "developmentVersion": "Geliştirme Sürümü", "dialogs": { - "aliases_must_be_unique": "takma adlar benzersiz olmalıdır", "delete_alert": "Bu {count, plural, one {{singularEntity}} other {{pluralEntity}}} kalıcı olarak silinecektir:", "delete_confirm": "Bunu silmek istediğinizden emin misiniz: {entityName}?", "delete_entity_desc": "{count, plural, one {Bunu silmek istediğinizden emin misiniz: {entityName}? Dosyayı bilgisayarınızdan silmediğiniz sürece, yeniden tarama işleminde bu {singularEntity} tekrar veritabanına eklenecektir.} other {Bunları silmek istediğinizden emin misiniz: {pluralEntity}? Dosyayı bilgisayarınızdan silmediğiniz sürece, yeniden tarama işleminde bu {pluralEntity} tekrar veritabanına eklenecektir.}}", @@ -765,7 +764,6 @@ "scenes": "Sahneler", "scenes_updated_at": "Sahne Güncelleme Tarihi", "search_filter": { - "add_filter": "Filtre Ekle", "name": "Filtre", "saved_filters": "Kaydedilmiş filtreler", "update_filter": "Filtreyi Güncelle" @@ -835,7 +833,7 @@ "your_system_has_been_created": "Sistem oluşturma başarılı!" }, "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.", + "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": "$HOME/.stash dizini altında", "in_the_current_working_directory": "Mevcut dizinde", "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.", @@ -891,6 +889,9 @@ "twitter": "Twitter", "updated_at": "Güncellenme Zamanı", "url": "Internet Adresi (URL)", + "validation": { + "aliases_must_be_unique": "takma adlar benzersiz olmalıdır" + }, "videos": "Videolar", "weight": "Kilo", "years_old": "yaşında" diff --git a/ui/v2.5/src/locales/uk-UA.json b/ui/v2.5/src/locales/uk-UA.json index 5bfbd192e..af9e2bb3d 100644 --- a/ui/v2.5/src/locales/uk-UA.json +++ b/ui/v2.5/src/locales/uk-UA.json @@ -35,7 +35,7 @@ "download_backup": "Завантажити Резервну Копію", "edit": "Редагувати", "edit_entity": "Редагувати {entityType}", - "export": "Експортувати…", + "export": "Експортувати", "export_all": "Експортувати Все…", "find": "Знайти", "finish": "Завершити", @@ -159,7 +159,7 @@ "allowed_ip_addresses": "Дозволені IP-адреси", "allowed_ip_temporarily": "Тимчасово дозволені IP-адреси", "default_ip_whitelist": "Білий список IP-адрес за замовчуванням", - "default_ip_whitelist_desc": "IP-адреси за замовчуванням мають доступ до DLNA. Використовуйте {wildcard} щоб дозволити усі IP-адреси", + "default_ip_whitelist_desc": "IP-адреси за замовчуванням мають доступ до DLNA. Використовуйте {wildcard} щоб дозволити усі IP-адреси.", "disallowed_ip": "Заборонені IP-адреси", "enabled_by_default": "Вмикнено за замовчуванням", "network_interfaces": "Інтерфейси", @@ -176,7 +176,7 @@ "clear_api_key": "Видалити API-ключ", "generate_api_key": "Згенерувати API-ключ", "log_file": "Файл логів", - "log_file_desc": "Шлях до файлу, в який будуть записуватись логи. Залишити пустим, щоб відключити логування в файл. Потребує перезапуску", + "log_file_desc": "Шлях до файлу, в який будуть записуватись логи. Залишити пустим, щоб відключити логування в файл. Потребує перезапуску.", "log_http": "Логувати HTTP-доступ", "log_http_desc": "Логувати HTTP-дії до терміналу. Потребує перезапуску.", "log_to_terminal": "Логувати до терміналу", diff --git a/ui/v2.5/src/locales/zh-CN.json b/ui/v2.5/src/locales/zh-CN.json index 9683bce81..eb11b1ac5 100644 --- a/ui/v2.5/src/locales/zh-CN.json +++ b/ui/v2.5/src/locales/zh-CN.json @@ -6,12 +6,13 @@ "add_to_entity": "添加到 {entityType}", "allow": "允许", "allow_temporarily": "暂时允许", + "anonymise": "匿名化", "apply": "应用", "auto_tag": "自动添加标签", "backup": "备份", "browse_for_image": "浏览图片…", "cancel": "取消", - "clean": "清除", + "clean": "清理", "clear": "清除", "clear_back_image": "清除背面图片", "clear_front_image": "清除正面图片", @@ -32,10 +33,11 @@ "delete_stashid": "删除 StashID", "disallow": "不允许", "download": "下载", + "download_anonymised": "匿名下载", "download_backup": "下载备份", "edit": "编辑", "edit_entity": "编辑 {entityType}", - "export": "导出…", + "export": "导出", "export_all": "导出所有…", "find": "搜索", "finish": "结束", @@ -269,6 +271,28 @@ "excluded_image_gallery_patterns_head": "图片/图库排除规则", "excluded_video_patterns_desc": "要从扫描中排除并会被[清除]功能所移除的视频文件/路径的正则表达式", "excluded_video_patterns_head": "视频排除规则", + "ffmpeg": { + "live_transcode": { + "input_args": { + "desc": "高级:当直播转码的视频时,在输入参数前要传给ffmpeg用的附加参数。", + "heading": "FFmpeg直播转码用的输入参数" + }, + "output_args": { + "desc": "高级:当直播转码视频时,在输出视频参数前要传给ffmpeg的附加参数。", + "heading": "FFmpeg直播转码输出参数" + } + }, + "transcode": { + "input_args": { + "desc": "高级:当生成视频时,在输入视频参数前要传给ffmpeg的附加参数。", + "heading": "FFmpeg 转码用的输入参数" + }, + "output_args": { + "desc": "高级:当生成视频时,在输出视频参数前要传给ffmpeg的附加参数。", + "heading": "FFmpeg转码输出参数" + } + } + }, "gallery_ext_desc": "逗号(半角)分隔的文件扩展名列表,将被标识为图库或图包。", "gallery_ext_head": "图库压缩包扩展名", "generated_file_naming_hash_desc": "使用 MD5 或快搜码为生成的文件命名。 更改此设置要求所有短片都有对应的 MD5/快搜码 值。 更改此值后,之前生成的数据需要迁移或重新生成。 请参阅 [迁移] 页面。", @@ -346,6 +370,9 @@ }, "tasks": { "added_job_to_queue": "已经添加 {operation_name} 到工作队列", + "anonymise_and_download": "建立一个匿名的数据库拷贝然后下载其文件。", + "anonymise_database": "建立一个数据库的拷贝到备份目录,匿名化所有敏感数据。这可以为其他人提供查找问题和去虫的方法。原本的数据库是没有改动的。匿名化数据库使用文件名格式 {filename_format}.", + "anonymising_database": "数据库匿名化", "auto_tag": { "auto_tagging_all_paths": "自动标签所有路径", "auto_tagging_paths": "自动标签以下路径" @@ -436,12 +463,12 @@ }, "basic_settings": "基本设定", "custom_css": { - "description": "必须重新加载页面才能使更改生效。", + "description": "必须重新加载页面才能使更改生效。不保证未来的Stash会和客制的CSS兼容。", "heading": "自定义样式", "option_label": "自定义样式已启用" }, "custom_javascript": { - "description": "必须重新加载页面才能生效。", + "description": "必须重新加载页面才能生效。不保证未来的Stash和客制的Javascript会兼容。", "heading": "自定义 JavaScript", "option_label": "自定义 JavaScript 已启用" }, @@ -661,7 +688,6 @@ "details": "简介", "developmentVersion": "开发版本", "dialogs": { - "aliases_must_be_unique": "别名必须是唯一的", "create_new_entity": "创建新的 {entity}", "delete_alert": "以下 {count, plural, one {{singularEntity}} other {{pluralEntity}}} 会被永久删除:", "delete_confirm": "确定要删除 {entityName} 吗?", @@ -757,6 +783,7 @@ }, "dimensions": "大小", "director": "导演", + "disambiguation": "确定含义", "display_mode": { "grid": "格状显示", "list": "列表显示", @@ -767,7 +794,7 @@ "donate": "赞助", "dupe_check": { "description": "低于“精确”的准确度需要更长的时间来计算,但使用较低的准确度可能会产生误报。", - "found_sets": "{setCount, plural, one{# 发现的重复数据。} other {# 发现的重复数据。}}", + "found_sets": "{setCount, plural, one{# 个发现的重复数据。} other {# 个发现的重复数据。}}", "options": { "exact": "精确", "high": "高", @@ -785,7 +812,7 @@ "blur": "模糊", "brightness": "亮度", "contrast": "对比", - "gamma": "伽马", + "gamma": "伽马亮度", "green": "绿色", "hue": "色调", "name": "滤镜", @@ -977,7 +1004,6 @@ "scenes": "短片", "scenes_updated_at": "短片更新时间", "search_filter": { - "add_filter": "添加过滤器", "name": "过滤", "saved_filters": "保存过滤器", "update_filter": "更新过滤器" @@ -1049,7 +1075,7 @@ "your_system_has_been_created": "成功!你的系统建立了!" }, "welcome": { - "config_path_logic_explained": "Stash首先尝试从当前工作目录中找配置文件 (config.yml),如果没找到,会退回到 $HOME/.stash/config.yml (在微软视窗里,目录是%USERPROFILE%\\.stash\\config.yml). 你也可以让Stash读特定的配置文件,只要运行它时加上-c <配置文件目录>或者--config <配置文件目录>的选项。", + "config_path_logic_explained": "Stash首先尝试从当前工作目录中找配置文件 (config.yml),如果没找到,会退回到 $HOME/.stash/config.yml (在微软视窗里,目录是%USERPROFILE%\\.stash\\config.yml). 你也可以让Stash读特定的配置文件,只要运行它时加上-c '<配置文件目录>'或者--config '<配置文件目录>'的选项。", "in_current_stash_directory": "在 $HOMT/.stash目录里", "in_the_current_working_directory": "在当前工作目录里", "next_step": "之前的问题都解决了,如果你准备好建立一个新的系统,选择你想在哪里存放你的配置文件,然后点击“下一个”。", @@ -1118,6 +1144,9 @@ "type": "类别", "updated_at": "更新时间", "url": "链接", + "validation": { + "aliases_must_be_unique": "别名必须是唯一的" + }, "videos": "视频", "view_all": "查看全部", "weight": "体重", diff --git a/ui/v2.5/src/locales/zh-TW.json b/ui/v2.5/src/locales/zh-TW.json index 148d4d4c2..432b4dcf3 100644 --- a/ui/v2.5/src/locales/zh-TW.json +++ b/ui/v2.5/src/locales/zh-TW.json @@ -6,6 +6,7 @@ "add_to_entity": "新增至{entityType}", "allow": "允許", "allow_temporarily": "暫時允許", + "anonymise": "匿名化", "apply": "套用", "auto_tag": "自動套用標籤", "backup": "備份", @@ -20,6 +21,7 @@ "confirm": "確認", "continue": "繼續", "create": "建立", + "create_chapters": "建立章節", "create_entity": "建立{entityType}", "create_marker": "建立章節標記", "created_entity": "已建立{entity_type}:{entity_name}", @@ -32,10 +34,11 @@ "delete_stashid": "刪除 StashID", "disallow": "不允許", "download": "下載", + "download_anonymised": "下載匿名化版本資料庫", "download_backup": "下載備份", "edit": "編輯", "edit_entity": "編輯{entityType}", - "export": "匯出…", + "export": "匯出", "export_all": "匯出所有…", "find": "搜尋", "finish": "完成", @@ -58,6 +61,8 @@ "merge": "合併", "merge_from": "與其他項目合併", "merge_into": "合併至其他項目", + "migrate_blobs": "遷移物件檔案", + "migrate_scene_screenshots": "遷移短片截圖", "next_action": "下一步", "not_running": "尚未執行", "open_in_external_player": "透過外部播放器開啟", @@ -67,6 +72,7 @@ "play_selected": "播放所選", "preview": "預覽", "previous_action": "上一步", + "reassign": "重新指定", "refresh": "重新整理", "reload_plugins": "重新整理外掛程式", "reload_scrapers": "重新整理爬蟲", @@ -99,10 +105,12 @@ "show": "顯示", "show_configuration": "顯示設定", "skip": "跳過", + "split": "分開", "stop": "停止", "submit": "提交", "submit_stash_box": "提交至 Stash-Box", "submit_update": "提交更新", + "swap": "替換", "tasks": { "clean_confirm_message": "您確定要進行清理嗎?這將從資料庫及產生的文件中清除已不在的短片及圖庫。", "dry_mode_selected": "已選擇了模擬作業模式。不會進行任何實際刪除作業,只會進行模擬記錄。", @@ -121,11 +129,17 @@ "also_known_as": "又稱為", "ascending": "升序", "average_resolution": "平均解析度", + "between_and": "及", "birth_year": "出生年分", "birthdate": "出生日期", "bitrate": "位元率", + "blobs_storage_type": { + "database": "資料庫", + "filesystem": "檔案系統" + }, "captions": "字幕", "career_length": "活躍年代", + "chapters": "章節", "component_tagger": { "config": { "active_instance": "目前使用的 Stash-box:", @@ -179,6 +193,7 @@ "latest_version": "最新版本", "latest_version_build_hash": "最新版本的雜湊值:", "new_version_notice": "[新版本]", + "release_date": "上映日期:", "stash_discord": "加入我們的 {url} 頻道", "stash_home": "Stash 的 {url} 專案", "stash_open_collective": "透過 {url} 來支持本計畫的開發", @@ -249,7 +264,15 @@ "description": "SQLite 資料庫備份的檔案位置", "heading": "備份目錄位置" }, - "cache_location": "快取的檔案位置", + "blobs_path": { + "description": "存取物件檔案的檔案系統路徑。僅適用於使用檔案系統格式的物件檔案。警告:更改此選項後須手動遷移現有檔案。", + "heading": "物件檔案檔案系統路徑" + }, + "blobs_storage": { + "description": "選擇物件檔案存取路徑,這些檔案可能包括檔案如短片封面、演員、工作室、及標籤圖案等等。若您修改此選項,請記得透過『遷移物件檔案』來遷移所有相關檔案。詳情請見『排程』頁面。", + "heading": "物件檔案儲存類別" + }, + "cache_location": "快取的檔案位置。透過 HLS 或 DASH 格式串流時所需的必要選項。", "cache_path_head": "快取路徑", "calculate_md5_and_ohash_desc": "除 oshash 外,同時也計算 MD5 的雜湊值。開啟後,可能會影響初次掃描的速度。若要關閉 MD5 計算,請將『生成檔案名所使用的雜湊演算法』設為 oshash。", "calculate_md5_and_ohash_label": "計算影片 MD5", @@ -259,12 +282,34 @@ "chrome_cdp_path_desc": "Chrome 執行檔的檔案路徑,或 Chrome 的遠端地址(以 http:// 或 https:// 開頭,例如 http://localhost:9222/json/version)。", "create_galleries_from_folders_desc": "勾選後,則會從包含圖片的資料夾建立圖庫。", "create_galleries_from_folders_label": "從包含圖片的資料夾建立圖庫", + "database": "資料庫", "db_path_head": "資料庫路徑", "directory_locations_to_your_content": "多媒體的檔案位置", "excluded_image_gallery_patterns_desc": "要從掃描中排除,並會被『清理』功能所移除的圖片及圖庫檔案/路徑的正規表示式", "excluded_image_gallery_patterns_head": "圖片/圖庫排除規則", "excluded_video_patterns_desc": "要從掃描中排除,並會被『清理』功能所移除的影片檔案/路徑的正規表示式", "excluded_video_patterns_head": "影片排除規則", + "ffmpeg": { + "hardware_acceleration": { + "heading": "FFMpeg 硬體編碼" + }, + "live_transcode": { + "input_args": { + "heading": "FFMpeg 即時串流輸入選項" + }, + "output_args": { + "heading": "FFMpeg 即時串流輸出選項" + } + }, + "transcode": { + "input_args": { + "heading": "FFMpeg 即時串流輸入選項" + }, + "output_args": { + "heading": "FFMpeg 即時串流輸出選項" + } + } + }, "gallery_ext_desc": "以逗號分隔的副檔名名稱,這些檔案將視為圖庫或圖包。", "gallery_ext_head": "圖庫 ZIP 檔副檔名", "generated_file_naming_hash_desc": "使用 MD5 或 oshash 生成檔案命名。更改此設定後,所有短片則須有先對應的 MD5/oshash 雜湊值。因此,之前已經生成的檔案可能需要重新遷移或重新生成。請參閱『遷移』設定。", @@ -342,6 +387,9 @@ }, "tasks": { "added_job_to_queue": "已將『{operation_name}』加入至工作排程", + "anonymise_and_download": "產生並下載匿名化版本的資料庫。", + "anonymise_database": "建立一份去除敏感資訊且匿名化的資料庫檔案至備份資料夾中。此檔案可供其他人員幫助您排除相關問題以及除錯用。您現有的資料庫當案將不會被修改。匿名化版本的資料庫檔案命名為 {filename_format}。", + "anonymising_database": "匿名化資料庫中", "auto_tag": { "auto_tagging_all_paths": "為所有路徑自動套用標籤中", "auto_tagging_paths": "為以下路徑自動套用標籤中" @@ -436,6 +484,11 @@ "heading": "自訂 CSS", "option_label": "啟用自訂 CSS" }, + "custom_javascript": { + "description": "必須重新整理頁面才能使更改生效。", + "heading": "自定義 JavaScript", + "option_label": "已啟用自定義 JavaScript" + }, "custom_locales": { "description": "強制使用特定翻譯字串。主列表請參閱 https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/locales/en-GB.json。必須重新整理頁面才能使更改生效。", "heading": "自訂翻譯", @@ -461,7 +514,24 @@ "description": "關閉下拉選單建立物件功能", "heading": "關閉下拉選單建立" }, - "heading": "編輯" + "heading": "編輯", + "rating_system": { + "star_precision": { + "label": "星級精度", + "options": { + "full": "完整", + "half": "一半", + "quarter": "四分之一" + } + }, + "type": { + "label": "評分系統類別", + "options": { + "decimal": "數字", + "stars": "星級" + } + } + } }, "funscript_offset": { "description": "互動式腳本的時間偏移量 (毫秒)。", @@ -505,6 +575,10 @@ "description": "顯示或隱藏導覽列表中的項目", "heading": "選單項目" }, + "minimum_play_percent": { + "description": "在增加播放次數前必須播放的短片總長百分比。", + "heading": "最低播放百分比" + }, "performers": { "options": { "image_location": { @@ -531,6 +605,7 @@ "scene_player": { "heading": "短片播放器", "options": { + "always_start_from_beginning": "永遠從頭開始播放影片", "auto_start_video": "自動播放", "auto_start_video_on_play_selected": { "description": "開啟佇列中或所選短片、或隨機播放時,自動開始播放影片", @@ -540,7 +615,8 @@ "description": "當影片播放完畢時,自動跳至下一個短片", "heading": "持續播放播放清單" }, - "show_scrubber": "顯示預覽軸" + "show_scrubber": "顯示預覽軸", + "track_activity": "追蹤使用活動" } }, "scene_wall": { @@ -629,7 +705,7 @@ "details": "細節", "developmentVersion": "開發版本", "dialogs": { - "aliases_must_be_unique": "別名不可重複", + "create_new_entity": "建立新{entity}", "delete_alert": "以下{count, plural, one {{singularEntity}} other {{pluralEntity}}}將被永久刪除:", "delete_confirm": "你確定要刪除 {entityName} 嗎?", "delete_entity_desc": "{count, plural, one {你確定要刪除該{singularEntity}嗎?除非連同檔案一起刪除,否則,下次進行檔案掃描時,該{singularEntity}會被重新加到資料庫中。} other {你確定要刪除這些{pluralEntity}嗎?除非連同檔案一起刪除,否則,下次進行檔案掃描時,這些{pluralEntity}會被重新加到資料庫中。}}", @@ -665,11 +741,20 @@ "zoom": "放大" } }, + "merge": { + "destination": "目標", + "empty_results": "目標值將保持不變。", + "source": "源頭" + }, "merge_tags": { "destination": "目的地", "source": "來源" }, "overwrite_filter_confirm": "您確定要覆蓋現有的條件 {entityName} 嗎?", + "reassign_entity_title": "{count, plural, one {重新指定{singularEntity}}}", + "reassign_files": { + "destination": "重新指定至" + }, "scene_gen": { "force_transcodes": "強制產生轉檔檔案", "force_transcodes_tooltip": "預設情況下,只有無法正常於瀏覽器中播放的影片檔會被轉檔成可播放的格式。開啟此設定後,即使是可正常播放的影片檔,也會被視為需轉檔的檔案。", @@ -715,6 +800,7 @@ }, "dimensions": "解析度", "director": "導演", + "disambiguation": "消歧義", "display_mode": { "grid": "格狀顯示", "list": "條列顯示", @@ -770,6 +856,7 @@ "file_info": "檔案資訊", "file_mod_time": "檔案修改於", "files": "檔案", + "files_amount": "{value} 個檔案", "filesize": "檔案大小", "filter": "過濾", "filter_name": "過濾條件名稱", @@ -807,6 +894,7 @@ }, "hasMarkers": "含有章節標記", "height": "身高", + "height_cm": "高度 (cm)", "help": "說明", "ignore_auto_tag": "忽略自動標籤", "image": "圖片", @@ -819,6 +907,7 @@ "interactive": "互動性支援", "interactive_speed": "互動速度", "isMissing": "缺失", + "last_played_at": "上次播放於", "library": "收藏庫", "loading": { "generic": "載入中…" @@ -837,6 +926,8 @@ "age_context": "這齣戲裡面 {age} {years_old}" }, "phash": "PHash", + "play_count": "播放次數", + "play_duration": "播放長度", "stream": "串流連結", "video_codec": "影片編碼" }, @@ -907,6 +998,8 @@ }, "performers": "演員", "piercings": "穿洞", + "play_count": "播放次數", + "play_duration": "播放時數", "primary_file": "主檔案", "queue": "佇列", "random": "隨機", @@ -915,15 +1008,19 @@ "recently_released_objects": "最近釋出的{objects}", "release_notes": "更新日誌", "resolution": "解析度", + "resume_time": "恢復播放時間", "scene": "短片", "sceneTagger": "短片標籤器", "sceneTags": "短片標籤", + "scene_code": "番號", "scene_count": "短片數量", + "scene_created_at": "短片建立於", + "scene_date": "短片日期", "scene_id": "短片 ID", + "scene_updated_at": "短片更新於", "scenes": "短片", "scenes_updated_at": "短片更新時間", "search_filter": { - "add_filter": "新增篩選", "name": "篩選", "saved_filters": "已儲存的過濾條件", "update_filter": "更新篩選" @@ -995,7 +1092,7 @@ "your_system_has_been_created": "成功!您的系統已安裝完成!" }, "welcome": { - "config_path_logic_explained": "Stash 於執行時,會先在執行目錄中找尋其設定檔案 (config.yml),當找不到時,它將會再試著使用 $HOME/.stash/config.yml(於 Windows 中,此路徑為 %USERPROFILE%\\.stash\\config.yml)。您也可在執行 Stash 時透過 -c <設定檔路徑> 提供設定路徑,或者 --config 。", + "config_path_logic_explained": "Stash 於執行時,會先在執行目錄中找尋其設定檔案 (config.yml),當找不到時,它將會再試著使用 $HOME/.stash/config.yml(於 Windows 中,此路徑為 %USERPROFILE%\\.stash\\config.yml)。您也可在執行 Stash 時透過 -c '<設定檔路徑>' 提供設定路徑,或者 --config ''。", "in_current_stash_directory": "於 $HOME/.stash 資料夾中", "in_the_current_working_directory": "於目前的工作路徑", "next_step": "如果您已準備好繼續安裝本程式,請選擇您想要儲存設定檔案的位置,然後點擊「下一步」。", @@ -1011,6 +1108,7 @@ "welcome_to_stash": "歡迎使用 Stash" }, "stash_id": "Stash ID", + "stash_id_endpoint": "Stash ID 端點", "stash_ids": "Stash IDs", "stashbox": { "go_review_draft": "到 {endpoint_name} 預覽草稿。", @@ -1046,7 +1144,9 @@ "default_filter_set": "已設定預設過濾選項", "delete_past_tense": "已刪除{singularEntity}", "generating_screenshot": "產生截圖中…", + "merged_scenes": "合併的短片", "merged_tags": "已合併的標籤", + "reassign_past_tense": "已重新指定檔案", "removed_entity": "已刪除{singularEntity}", "rescanning_entity": "重新掃描{singularEntity}中…", "saved_entity": "已儲存{entity}", @@ -1061,9 +1161,13 @@ "type": "種類", "updated_at": "更新於", "url": "連結", + "validation": { + "aliases_must_be_unique": "別名不可重複" + }, "videos": "影片", "view_all": "顯示全部", "weight": "體重", + "weight_kg": "體重 (kg)", "years_old": "歲", "zip_file_count": "壓縮檔內容數量" } diff --git a/ui/v2.5/src/models/list-filter/criteria/country.ts b/ui/v2.5/src/models/list-filter/criteria/country.ts index 56b67f6bb..e35626ae8 100644 --- a/ui/v2.5/src/models/list-filter/criteria/country.ts +++ b/ui/v2.5/src/models/list-filter/criteria/country.ts @@ -1,6 +1,6 @@ import { IntlShape } from "react-intl"; import { CriterionModifier } from "src/core/generated-graphql"; -import { getCountryByISO } from "src/utils"; +import { getCountryByISO } from "src/utils/country"; import { StringCriterion, StringCriterionOption } from "./criterion"; const countryCriterionOption = new StringCriterionOption( 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 60d245f62..a4b53dec7 100644 --- a/ui/v2.5/src/models/list-filter/criteria/criterion.ts +++ b/ui/v2.5/src/models/list-filter/criteria/criterion.ts @@ -70,6 +70,10 @@ export abstract class Criterion { this._value = newValue; } + public isValid(): boolean { + return true; + } + public abstract getLabelValue(intl: IntlShape): string; constructor(type: CriterionOption, value: V) { @@ -227,6 +231,14 @@ export class StringCriterion extends Criterion { public getLabelValue(_intl: IntlShape) { return this.value; } + + public isValid(): boolean { + return ( + this.modifier === CriterionModifier.IsNull || + this.modifier === CriterionModifier.NotNull || + this.value.length > 0 + ); + } } export class MandatoryStringCriterionOption extends CriterionOption { @@ -267,6 +279,20 @@ export function createMandatoryStringCriterionOption( ); } +export class PathCriterionOption extends StringCriterionOption {} + +export function createPathCriterionOption( + value: CriterionType, + messageID?: string, + parameterName?: string +) { + return new PathCriterionOption( + messageID ?? value, + value, + parameterName ?? messageID ?? value + ); +} + export class BooleanCriterionOption extends CriterionOption { constructor(messageID: string, value: CriterionType, parameterName?: string) { super({ @@ -284,6 +310,10 @@ export class BooleanCriterion extends StringCriterion { protected toCriterionInput(): boolean { return this.value === "true"; } + + public isValid() { + return this.value === "true" || this.value === "false"; + } } export function createBooleanCriterionOption( @@ -375,7 +405,7 @@ export class NumberCriterion extends Criterion { protected toCriterionInput(): IntCriterionInput { return { modifier: this.modifier, - value: this.value.value, + value: this.value.value ?? 0, value2: this.value.value2, }; } @@ -392,8 +422,32 @@ export class NumberCriterion extends Criterion { } } + public isValid(): boolean { + if ( + this.modifier === CriterionModifier.IsNull || + this.modifier === CriterionModifier.NotNull + ) { + return true; + } + + const { value, value2 } = this.value; + if (value === undefined) { + return false; + } + + if ( + value2 === undefined && + (this.modifier === CriterionModifier.Between || + this.modifier === CriterionModifier.NotBetween) + ) { + return false; + } + + return true; + } + constructor(type: CriterionOption) { - super(type, { value: 0, value2: undefined }); + super(type, { value: undefined, value2: undefined }); } } @@ -439,6 +493,17 @@ export class ILabeledIdCriterion extends Criterion { }; } + public isValid(): boolean { + if ( + this.modifier === CriterionModifier.IsNull || + this.modifier === CriterionModifier.NotNull + ) { + return true; + } + + return this.value.length > 0; + } + constructor(type: CriterionOption) { super(type, []); } @@ -463,6 +528,17 @@ export class IHierarchicalLabeledIdCriterion extends Criterion 0 ? this.value.depth : "all"})`; } + public isValid(): boolean { + if ( + this.modifier === CriterionModifier.IsNull || + this.modifier === CriterionModifier.NotNull + ) { + return true; + } + + return this.value.items.length > 0; + } + constructor(type: CriterionOption) { const value: IHierarchicalLabelValue = { items: [], @@ -502,13 +578,13 @@ export function createMandatoryNumberCriterionOption( export class DurationCriterion extends Criterion { constructor(type: CriterionOption) { - super(type, { value: 0, value2: undefined }); + super(type, { value: undefined, value2: undefined }); } protected toCriterionInput(): IntCriterionInput { return { modifier: this.modifier, - value: this.value.value, + value: this.value.value ?? 0, value2: this.value.value2, }; } @@ -517,15 +593,39 @@ export class DurationCriterion extends Criterion { return this.modifier === CriterionModifier.Between || this.modifier === CriterionModifier.NotBetween ? `${DurationUtils.secondsToString( - this.value.value + this.value.value ?? 0 )} ${DurationUtils.secondsToString(this.value.value2 ?? 0)}` : this.modifier === CriterionModifier.GreaterThan || this.modifier === CriterionModifier.LessThan || this.modifier === CriterionModifier.Equals || this.modifier === CriterionModifier.NotEquals - ? DurationUtils.secondsToString(this.value.value) + ? DurationUtils.secondsToString(this.value.value ?? 0) : "?"; } + + public isValid(): boolean { + if ( + this.modifier === CriterionModifier.IsNull || + this.modifier === CriterionModifier.NotNull + ) { + return true; + } + + const { value, value2 } = this.value; + if (value === undefined) { + return false; + } + + if ( + value2 === undefined && + (this.modifier === CriterionModifier.Between || + this.modifier === CriterionModifier.NotBetween) + ) { + return false; + } + + return true; + } } export class PhashDuplicateCriterion extends StringCriterion { @@ -592,6 +692,30 @@ export class DateCriterion extends Criterion { : `${value}`; } + public isValid(): boolean { + if ( + this.modifier === CriterionModifier.IsNull || + this.modifier === CriterionModifier.NotNull + ) { + return true; + } + + const { value, value2 } = this.value; + if (!value) { + return false; + } + + if ( + !value2 && + (this.modifier === CriterionModifier.Between || + this.modifier === CriterionModifier.NotBetween) + ) { + return false; + } + + return true; + } + constructor(type: CriterionOption) { super(type, { value: "", value2: undefined }); } @@ -662,6 +786,30 @@ export class TimestampCriterion extends Criterion { return ""; } + public isValid(): boolean { + if ( + this.modifier === CriterionModifier.IsNull || + this.modifier === CriterionModifier.NotNull + ) { + return true; + } + + const { value, value2 } = this.value; + if (!value) { + return false; + } + + if ( + !value2 && + (this.modifier === CriterionModifier.Between || + this.modifier === CriterionModifier.NotBetween) + ) { + return false; + } + + return true; + } + constructor(type: CriterionOption) { super(type, { value: "", value2: undefined }); } diff --git a/ui/v2.5/src/models/list-filter/criteria/factory.ts b/ui/v2.5/src/models/list-filter/criteria/factory.ts index bbc870543..28bec371b 100644 --- a/ui/v2.5/src/models/list-filter/criteria/factory.ts +++ b/ui/v2.5/src/models/list-filter/criteria/factory.ts @@ -15,10 +15,12 @@ import { DateCriterionOption, TimestampCriterion, MandatoryTimestampCriterionOption, + PathCriterionOption, } from "./criterion"; import { OrganizedCriterion } from "./organized"; import { FavoriteCriterion, PerformerFavoriteCriterion } from "./favorite"; import { HasMarkersCriterion } from "./has-markers"; +import { HasChaptersCriterion } from "./has-chapters"; import { PerformerIsMissingCriterionOption, ImageIsMissingCriterionOption, @@ -64,9 +66,7 @@ export function makeCriteria( return new NoneCriterion(); case "name": case "path": - return new StringCriterion( - new MandatoryStringCriterionOption(type, type) - ); + return new StringCriterion(new PathCriterionOption(type, type)); case "checksum": return new StringCriterion( new MandatoryStringCriterionOption("media_info.checksum", type, type) @@ -106,11 +106,15 @@ export function makeCriteria( case "resume_time": case "duration": case "play_duration": - return new DurationCriterion(new NumberCriterionOption(type, type)); + return new DurationCriterion( + new MandatoryNumberCriterionOption(type, type) + ); case "favorite": return new FavoriteCriterion(); case "hasMarkers": return new HasMarkersCriterion(); + case "hasChapters": + return new HasChaptersCriterion(); case "sceneIsMissing": return new IsMissingCriterion(SceneIsMissingCriterionOption); case "imageIsMissing": @@ -192,6 +196,7 @@ export function makeCriteria( case "director": case "synopsis": case "description": + case "disambiguation": return new StringCriterion(new StringCriterionOption(type, type)); case "scene_code": return new StringCriterion(new StringCriterionOption(type, type, "code")); diff --git a/ui/v2.5/src/models/list-filter/criteria/has-chapters.ts b/ui/v2.5/src/models/list-filter/criteria/has-chapters.ts new file mode 100644 index 000000000..12d74cbbb --- /dev/null +++ b/ui/v2.5/src/models/list-filter/criteria/has-chapters.ts @@ -0,0 +1,18 @@ +import { CriterionOption, StringCriterion } from "./criterion"; + +export const HasChaptersCriterionOption = new CriterionOption({ + messageID: "hasChapters", + type: "hasChapters", + parameterName: "has_chapters", + options: [true.toString(), false.toString()], +}); + +export class HasChaptersCriterion extends StringCriterion { + constructor() { + super(HasChaptersCriterionOption); + } + + protected toCriterionInput(): string { + return this.value; + } +} 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 1cc71d168..5ea971d2c 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 @@ -33,6 +33,7 @@ export const SceneIsMissingCriterionOption = new IsMissingCriterionOptionClass( "is_missing", [ "title", + "cover", "details", "url", "date", @@ -52,39 +53,50 @@ export const ImageIsMissingCriterionOption = new IsMissingCriterionOptionClass( ["title", "galleries", "studio", "performers", "tags"] ); -export const PerformerIsMissingCriterionOption = new IsMissingCriterionOptionClass( - "isMissing", - "performerIsMissing", - "is_missing", - [ - "url", - "twitter", - "instagram", - "ethnicity", - "country", - "hair_color", - "eye_color", - "height", - "weight", - "measurements", - "fake_tits", - "career_length", - "tattoos", - "piercings", - "aliases", - "gender", - "image", - "details", - "stash_id", - ] -); +export const PerformerIsMissingCriterionOption = + new IsMissingCriterionOptionClass( + "isMissing", + "performerIsMissing", + "is_missing", + [ + "url", + "twitter", + "instagram", + "ethnicity", + "country", + "hair_color", + "eye_color", + "height", + "weight", + "measurements", + "fake_tits", + "career_length", + "tattoos", + "piercings", + "aliases", + "gender", + "image", + "details", + "stash_id", + ] + ); -export const GalleryIsMissingCriterionOption = new IsMissingCriterionOptionClass( - "isMissing", - "galleryIsMissing", - "is_missing", - ["title", "details", "url", "date", "studio", "performers", "tags", "scenes"] -); +export const GalleryIsMissingCriterionOption = + new IsMissingCriterionOptionClass( + "isMissing", + "galleryIsMissing", + "is_missing", + [ + "title", + "details", + "url", + "date", + "studio", + "performers", + "tags", + "scenes", + ] + ); export const TagIsMissingCriterionOption = new IsMissingCriterionOptionClass( "isMissing", diff --git a/ui/v2.5/src/models/list-filter/criteria/rating.ts b/ui/v2.5/src/models/list-filter/criteria/rating.ts index ffacdaf3f..56265fb9d 100644 --- a/ui/v2.5/src/models/list-filter/criteria/rating.ts +++ b/ui/v2.5/src/models/list-filter/criteria/rating.ts @@ -31,7 +31,7 @@ export class RatingCriterion extends Criterion { protected toCriterionInput(): IntCriterionInput { return { modifier: this.modifier, - value: this.value.value, + value: this.value.value ?? 0, value2: this.value.value2, }; } diff --git a/ui/v2.5/src/models/list-filter/criteria/resolution.ts b/ui/v2.5/src/models/list-filter/criteria/resolution.ts index a669a6f86..5ee0b7254 100644 --- a/ui/v2.5/src/models/list-filter/criteria/resolution.ts +++ b/ui/v2.5/src/models/list-filter/criteria/resolution.ts @@ -45,9 +45,8 @@ export class ResolutionCriterion extends AbstractResolutionCriterion { } } -export const AverageResolutionCriterionOption = new ResolutionCriterionOptionType( - "average_resolution" -); +export const AverageResolutionCriterionOption = + new ResolutionCriterionOptionType("average_resolution"); export class AverageResolutionCriterion extends AbstractResolutionCriterion { constructor() { diff --git a/ui/v2.5/src/models/list-filter/criteria/stash-ids.ts b/ui/v2.5/src/models/list-filter/criteria/stash-ids.ts index 6467c50ea..c4db4d596 100644 --- a/ui/v2.5/src/models/list-filter/criteria/stash-ids.ts +++ b/ui/v2.5/src/models/list-filter/criteria/stash-ids.ts @@ -103,4 +103,12 @@ export class StashIDCriterion extends Criterion { } return JSON.stringify(encodedCriterion); } + + public isValid(): boolean { + return ( + this.modifier === CriterionModifier.IsNull || + this.modifier === CriterionModifier.NotNull || + this.value.stashID.length > 0 + ); + } } diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts index 0a938eb07..e20fd5935 100644 --- a/ui/v2.5/src/models/list-filter/filter.ts +++ b/ui/v2.5/src/models/list-filter/filter.ts @@ -1,5 +1,3 @@ -import queryString, { ParsedQuery } from "query-string"; -import clone from "lodash-es/clone"; import { ConfigDataFragment, FilterMode, @@ -10,15 +8,26 @@ import { Criterion, CriterionValue } from "./criteria/criterion"; import { makeCriteria } from "./criteria/factory"; import { DisplayMode } from "./types"; -interface IQueryParameters { - perPage?: string; +interface IDecodedParams { + perPage?: number; sortby?: string; sortdir?: string; - disp?: string; + disp?: DisplayMode; q?: string; - p?: string; + p?: number; + z?: number; + c?: string[]; +} + +interface IEncodedParams { + perPage?: string | null; + sortby?: string | null; + sortdir?: string | null; + disp?: string | null; + q?: string | null; + p?: string | null; + z?: string | null; c?: string[]; - z?: string; } const DEFAULT_PARAMS = { @@ -31,8 +40,8 @@ const DEFAULT_PARAMS = { // TODO: handle customCriteria export class ListFilterModel { public mode: FilterMode; - private config: ConfigDataFragment | undefined; - public searchTerm?: string; + private config?: ConfigDataFragment; + public searchTerm: string = ""; public currentPage = DEFAULT_PARAMS.currentPage; public itemsPerPage = DEFAULT_PARAMS.itemsPerPage; public sortDirection: SortDirectionEnum = SortDirectionEnum.Asc; @@ -45,7 +54,7 @@ export class ListFilterModel { public constructor( mode: FilterMode, - config: ConfigDataFragment | undefined, + config?: ConfigDataFragment, defaultSort?: string, defaultDisplayMode?: DisplayMode, defaultZoomIndex?: number @@ -53,7 +62,9 @@ export class ListFilterModel { this.mode = mode; this.config = config; this.sortBy = defaultSort; - if (defaultDisplayMode !== undefined) this.displayMode = defaultDisplayMode; + if (defaultDisplayMode !== undefined) { + this.displayMode = defaultDisplayMode; + } if (defaultZoomIndex !== undefined) { this.defaultZoomIndex = defaultZoomIndex; this.zoomIndex = defaultZoomIndex; @@ -64,22 +75,24 @@ export class ListFilterModel { return Object.assign(new ListFilterModel(this.mode, this.config), this); } - // Does not decode any URL-encoding in parameters - public configureFromQueryParameters(params: IQueryParameters) { + // returns the number of filters applied + public count() { + // don't include search term + return this.criteria.length; + } + + public configureFromDecodedParams(params: IDecodedParams) { + if (params.perPage !== undefined) { + this.itemsPerPage = params.perPage; + } if (params.sortby !== undefined) { this.sortBy = params.sortby; // parse the random seed if provided - const randomPrefix = "random_"; - if (this.sortBy && this.sortBy.startsWith(randomPrefix)) { - const seedStr = this.sortBy.substring(randomPrefix.length); - + const match = this.sortBy.match(/^random_(\d+)$/); + if (match) { this.sortBy = "random"; - try { - this.randomSeed = Number.parseInt(seedStr, 10); - } catch (err) { - // ignore - } + this.randomSeed = Number.parseInt(match[1], 10); } } // #3193 - sortdir undefined means asc @@ -89,23 +102,19 @@ export class ListFilterModel { : SortDirectionEnum.Asc; if (params.disp !== undefined) { - this.displayMode = Number.parseInt(params.disp, 10); + this.displayMode = params.disp; } - if (params.q) { - this.searchTerm = params.q.trim(); + if (params.q !== undefined) { + this.searchTerm = params.q; } - this.currentPage = params.p ? Number.parseInt(params.p, 10) : 1; - if (params.perPage) this.itemsPerPage = Number.parseInt(params.perPage, 10); + this.currentPage = params.p ?? 1; if (params.z !== undefined) { - const zoomIndex = Number.parseInt(params.z, 10); - if (zoomIndex >= 0 && !Number.isNaN(zoomIndex)) { - this.zoomIndex = zoomIndex; - } + this.zoomIndex = params.z; } this.criteria = []; if (params.c !== undefined) { - params.c.forEach((jsonString) => { + for (const jsonString of params.c) { try { const encodedCriterion = JSON.parse(jsonString); const criterion = makeCriteria(this.config, encodedCriterion.type); @@ -121,48 +130,52 @@ export class ListFilterModel { // eslint-disable-next-line no-console console.error("Failed to parse encoded criterion:", err); } - }); + } } } - public static decodeQueryParameters( - parsedQuery: ParsedQuery - ): IQueryParameters { - const params = clone(parsedQuery); + // Does not decode any URL-encoding, only type conversions + public static decodeParams(params: IEncodedParams): IDecodedParams { + const ret: IDecodedParams = {}; + + if (params.perPage) { + ret.perPage = Number.parseInt(params.perPage, 10); + } + if (params.sortby) { + ret.sortby = params.sortby; + } + if (params.sortdir) { + ret.sortdir = params.sortdir; + } + if (params.disp) { + ret.disp = Number.parseInt(params.disp, 10); + } if (params.q) { - let searchTerm: string; - if (params.q instanceof Array) { - searchTerm = params.q[0]; - } else { - searchTerm = params.q; + ret.q = params.q.trim(); + } + if (params.p) { + ret.p = Number.parseInt(params.p, 10); + } + if (params.z) { + const zoomIndex = Number.parseInt(params.z, 10); + if (zoomIndex >= 0) { + ret.z = zoomIndex; } + } - // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#decoding_query_parameters_from_a_url - searchTerm = searchTerm.replaceAll("+", " "); - params.q = decodeURIComponent(searchTerm); + if (params.c && params.c.length !== 0) { + ret.c = params.c.map((jsonString) => + ListFilterModel.translateJSON(jsonString, true) + ); } - if (params.c !== undefined) { - let jsonParameters: string[]; - if (params.c instanceof Array) { - jsonParameters = params.c; - } else { - jsonParameters = [params.c!]; - } - params.c = jsonParameters.map((jsonString) => { - const decoding = true; - return ListFilterModel.translateSpecialCharacters( - decodeURIComponent(jsonString), - decoding - ); - }); - } - return params; + + return ret; } - private static translateSpecialCharacters(input: string, decoding: boolean) { + private static translateJSON(jsonString: string, decoding: boolean) { let inString = false; let escape = false; - return [...input] + return [...jsonString] .map((c) => { if (escape) { // this character has been escaped, skip @@ -212,14 +225,24 @@ export class ListFilterModel { .join(""); } - public configureFromQueryString(query: string) { - const parsed = queryString.parse(query, { decode: false }); - const decoded = ListFilterModel.decodeQueryParameters(parsed); - this.configureFromQueryParameters(decoded); + public configureFromQueryString(queryString: string) { + const query = new URLSearchParams(queryString); + const params = { + perPage: query.get("perPage"), + sortby: query.get("sortby"), + sortdir: query.get("sortdir"), + disp: query.get("disp"), + q: query.get("q"), + p: query.get("p"), + z: query.get("z"), + c: query.getAll("c"), + }; + const decoded = ListFilterModel.decodeParams(params); + this.configureFromDecodedParams(decoded); } public configureFromJSON(json: string) { - this.configureFromQueryParameters(JSON.parse(json)); + this.configureFromDecodedParams(JSON.parse(json)); } private setRandomSeed() { @@ -238,20 +261,16 @@ export class ListFilterModel { this.setRandomSeed(); if (this.sortBy === "random") { - return `${this.sortBy}_${this.randomSeed.toString()}`; + return `random_${this.randomSeed.toString()}`; } return this.sortBy; } - // Returns query parameters with necessary parts encoded - public getQueryParameters(): IQueryParameters { + // Returns query parameters with necessary parts URL-encoded + public getEncodedParams(): IEncodedParams { const encodedCriteria: string[] = this.criteria.map((criterion) => { - const decoding = false; - let str = ListFilterModel.translateSpecialCharacters( - criterion.toJSON(), - decoding - ); + let str = ListFilterModel.translateJSON(criterion.toJSON(), false); // URL-encode other characters str = encodeURI(str); @@ -273,7 +292,7 @@ export class ListFilterModel { this.itemsPerPage !== DEFAULT_PARAMS.itemsPerPage ? String(this.itemsPerPage) : undefined, - sortby: this.getSortBy() ?? undefined, + sortby: this.getSortBy(), sortdir: this.sortDirection === SortDirectionEnum.Desc ? "desc" : undefined, disp: @@ -300,11 +319,11 @@ export class ListFilterModel { const result = { perPage: this.itemsPerPage, - sortby: this.getSortBy() ?? undefined, + sortby: this.getSortBy(), sortdir: this.sortDirection === SortDirectionEnum.Desc ? "desc" : undefined, disp: this.displayMode, - q: this.searchTerm, + q: this.searchTerm || undefined, z: this.zoomIndex, c: encodedCriteria, }; @@ -313,7 +332,37 @@ export class ListFilterModel { } public makeQueryParameters(): string { - return queryString.stringify(this.getQueryParameters(), { encode: false }); + const query: string[] = []; + const params = this.getEncodedParams(); + + if (params.q) { + query.push(`q=${params.q}`); + } + if (params.c) { + for (const c of params.c) { + query.push(`c=${c}`); + } + } + if (params.sortby) { + query.push(`sortby=${params.sortby}`); + } + if (params.sortdir) { + query.push(`sortdir=${params.sortdir}`); + } + if (params.perPage) { + query.push(`perPage=${params.perPage}`); + } + if (params.disp) { + query.push(`disp=${params.disp}`); + } + if (params.z) { + query.push(`z=${params.z}`); + } + if (params.p) { + query.push(`p=${params.p}`); + } + + return query.join("&"); } // TODO: These don't support multiple of the same criteria, only the last one set is used. diff --git a/ui/v2.5/src/models/list-filter/galleries.ts b/ui/v2.5/src/models/list-filter/galleries.ts index 3f1e7aa7e..36bb65de6 100644 --- a/ui/v2.5/src/models/list-filter/galleries.ts +++ b/ui/v2.5/src/models/list-filter/galleries.ts @@ -4,10 +4,12 @@ import { NullNumberCriterionOption, createDateCriterionOption, createMandatoryTimestampCriterionOption, + createPathCriterionOption, } from "./criteria/criterion"; import { PerformerFavoriteCriterionOption } from "./criteria/favorite"; import { GalleryIsMissingCriterionOption } from "./criteria/is-missing"; import { OrganizedCriterionOption } from "./criteria/organized"; +import { HasChaptersCriterionOption } from "./criteria/has-chapters"; import { PerformersCriterionOption } from "./criteria/performers"; import { AverageResolutionCriterionOption } from "./criteria/resolution"; import { StudiosCriterionOption } from "./criteria/studios"; @@ -42,7 +44,7 @@ const displayModeOptions = [ const criterionOptions = [ createStringCriterionOption("title"), createStringCriterionOption("details"), - createStringCriterionOption("path"), + createPathCriterionOption("path"), createStringCriterionOption( "galleryChecksum", "media_info.checksum", @@ -53,6 +55,7 @@ const criterionOptions = [ AverageResolutionCriterionOption, GalleryIsMissingCriterionOption, TagsCriterionOption, + HasChaptersCriterionOption, createStringCriterionOption("tag_count"), PerformerTagsCriterionOption, PerformersCriterionOption, diff --git a/ui/v2.5/src/models/list-filter/images.ts b/ui/v2.5/src/models/list-filter/images.ts index 72c9e30d7..372ca21ca 100644 --- a/ui/v2.5/src/models/list-filter/images.ts +++ b/ui/v2.5/src/models/list-filter/images.ts @@ -4,6 +4,8 @@ import { createStringCriterionOption, NullNumberCriterionOption, createMandatoryTimestampCriterionOption, + createDateCriterionOption, + createPathCriterionOption, } from "./criteria/criterion"; import { PerformerFavoriteCriterionOption } from "./criteria/favorite"; import { ImageIsMissingCriterionOption } from "./criteria/is-missing"; @@ -24,6 +26,7 @@ const sortByOptions = [ "o_counter", "filesize", "file_count", + "date", ...MediaSortByOptions, ].map(ListFilterOptions.createSortBy); @@ -31,7 +34,7 @@ const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall]; const criterionOptions = [ createStringCriterionOption("title"), createMandatoryStringCriterionOption("checksum", "media_info.checksum"), - createMandatoryStringCriterionOption("path"), + createPathCriterionOption("path"), OrganizedCriterionOption, createMandatoryNumberCriterionOption("o_counter"), ResolutionCriterionOption, @@ -44,6 +47,8 @@ const criterionOptions = [ createMandatoryNumberCriterionOption("performer_count"), PerformerFavoriteCriterionOption, StudiosCriterionOption, + createStringCriterionOption("url"), + createDateCriterionOption("date"), createMandatoryNumberCriterionOption("file_count"), createMandatoryTimestampCriterionOption("created_at"), createMandatoryTimestampCriterionOption("updated_at"), diff --git a/ui/v2.5/src/models/list-filter/performers.ts b/ui/v2.5/src/models/list-filter/performers.ts index 30d1d7316..4028209f9 100644 --- a/ui/v2.5/src/models/list-filter/performers.ts +++ b/ui/v2.5/src/models/list-filter/performers.ts @@ -57,6 +57,7 @@ const numberCriteria: CriterionType[] = [ const stringCriteria: CriterionType[] = [ "name", + "disambiguation", "details", "ethnicity", "country", diff --git a/ui/v2.5/src/models/list-filter/scenes.ts b/ui/v2.5/src/models/list-filter/scenes.ts index b894628d8..f4be9f78a 100644 --- a/ui/v2.5/src/models/list-filter/scenes.ts +++ b/ui/v2.5/src/models/list-filter/scenes.ts @@ -5,6 +5,7 @@ import { NullNumberCriterionOption, createDateCriterionOption, createMandatoryTimestampCriterionOption, + createPathCriterionOption, } from "./criteria/criterion"; import { HasMarkersCriterionOption } from "./criteria/has-markers"; import { SceneIsMissingCriterionOption } from "./criteria/is-missing"; @@ -59,7 +60,7 @@ const displayModeOptions = [ const criterionOptions = [ createStringCriterionOption("title"), createStringCriterionOption("scene_code"), - createMandatoryStringCriterionOption("path"), + createPathCriterionOption("path"), createStringCriterionOption("details"), createStringCriterionOption("director"), createMandatoryStringCriterionOption("oshash", "media_info.hash"), diff --git a/ui/v2.5/src/models/list-filter/types.ts b/ui/v2.5/src/models/list-filter/types.ts index eb671d37e..3dd9e589c 100644 --- a/ui/v2.5/src/models/list-filter/types.ts +++ b/ui/v2.5/src/models/list-filter/types.ts @@ -24,7 +24,7 @@ export interface IHierarchicalLabelValue { } export interface INumberValue { - value: number; + value: number | undefined; value2: number | undefined; } @@ -176,4 +176,6 @@ export type CriterionType = | "scene_created_at" | "scene_updated_at" | "description" - | "scene_code"; + | "scene_code" + | "disambiguation" + | "hasChapters"; diff --git a/ui/v2.5/src/models/sceneQueue.ts b/ui/v2.5/src/models/sceneQueue.ts index a43e547ad..de9cf2bbe 100644 --- a/ui/v2.5/src/models/sceneQueue.ts +++ b/ui/v2.5/src/models/sceneQueue.ts @@ -1,4 +1,3 @@ -import queryString, { ParsedQuery } from "query-string"; import { FilterMode, Scene } from "src/core/generated-graphql"; import { ListFilterModel } from "./list-filter/filter"; import { SceneListFilterOptions } from "./list-filter/scenes"; @@ -38,18 +37,27 @@ export class SceneQueue { } private makeQueryParameters(sceneIndex?: number, page?: number) { - if (this.query) { - const queryParams = this.query.getQueryParameters(); - const translatedParams = { - qfp: queryParams.p ?? 1, - qfc: queryParams.c, - qfq: queryParams.q, - qsort: queryParams.sortby, - qsortd: queryParams.sortdir, - }; + const ret: string[] = []; + if (this.query) { + const queryParams = this.query.getEncodedParams(); + + if (queryParams.sortby) { + ret.push(`qsort=${queryParams.sortby}`); + } + if (queryParams.sortdir) { + ret.push(`qsortd=${queryParams.sortdir}`); + } + if (queryParams.q) { + ret.push(`qfq=${queryParams.q}`); + } + for (const c of queryParams.c ?? []) { + ret.push(`qfc=${c}`); + } + + let qfp = queryParams.p ?? "1"; if (page !== undefined) { - translatedParams.qfp = page; + qfp = String(page); } else if ( sceneIndex !== undefined && this.originalQueryPage !== undefined && @@ -60,46 +68,40 @@ export class SceneQueue { sceneIndex + (this.originalQueryPage - 1) * this.originalQueryPageSize; const newPage = Math.floor(filterIndex / this.query.itemsPerPage) + 1; - translatedParams.qfp = newPage; + qfp = String(newPage); + } + ret.push(`qfp=${qfp}`); + } else if (this.sceneIDs && this.sceneIDs.length > 0) { + for (const id of this.sceneIDs) { + ret.push(`qs=${id}`); } - - return queryString.stringify(translatedParams, { encode: false }); } - if (this.sceneIDs && this.sceneIDs.length > 0) { - const params = { - qs: this.sceneIDs, - }; - return queryString.stringify(params, { encode: false }); - } - - return ""; + return ret.join("&"); } - public static fromQueryParameters(params: ParsedQuery) { + public static fromQueryParameters(params: URLSearchParams) { const ret = new SceneQueue(); - const translated = { - sortby: params.qsort, - sortdir: params.qsortd, - q: params.qfq, - p: params.qfp, - c: params.qfc, - }; - if (params.qfp) { - const decoded = ListFilterModel.decodeQueryParameters(translated); + if (params.has("qfp")) { + const translated = { + sortby: params.get("qsort"), + sortdir: params.get("qsortd"), + q: params.get("qfq"), + p: params.get("qfp"), + c: params.getAll("qfc"), + }; + const decoded = ListFilterModel.decodeParams(translated); const query = new ListFilterModel( FilterMode.Scenes, undefined, SceneListFilterOptions.defaultSortBy ); - query.configureFromQueryParameters(decoded); + query.configureFromDecodedParams(decoded); ret.query = query; - } else if (params.qs) { + } else if (params.has("qs")) { // must be scene list - ret.sceneIDs = Array.isArray(params.qs) - ? params.qs.map((v) => Number(v)) - : [Number(params.qs)]; + ret.sceneIDs = params.getAll("qs").map((v) => Number(v)); } return ret; diff --git a/ui/v2.5/src/polyfills.ts b/ui/v2.5/src/polyfills.ts index b232bc907..c66a0a645 100644 --- a/ui/v2.5/src/polyfills.ts +++ b/ui/v2.5/src/polyfills.ts @@ -3,7 +3,7 @@ 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/intersection-observer"; +import "intersection-observer"; // Required for browsers older than August 2020ish. Can be removed at some point. replaceAll.shim(); diff --git a/ui/v2.5/src/react-app-env.d.ts b/ui/v2.5/src/react-app-env.d.ts deleted file mode 100644 index 6431bc5fc..000000000 --- a/ui/v2.5/src/react-app-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/ui/v2.5/src/serviceWorker.ts b/ui/v2.5/src/serviceWorker.ts index ca73860b0..0a394974a 100755 --- a/ui/v2.5/src/serviceWorker.ts +++ b/ui/v2.5/src/serviceWorker.ts @@ -103,10 +103,7 @@ function checkValidServiceWorker(swUrl: string, config?: IConfig) { export function register(config?: IConfig) { if (import.meta.env.PROD && "serviceWorker" in navigator) { // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL( - (process as { env: { [key: string]: string } }).env.PUBLIC_URL, - window.location.href - ); + const publicUrl = new URL(import.meta.env.PUBLIC_URL, window.location.href); if (publicUrl.origin !== window.location.origin) { // Our service worker won't work if PUBLIC_URL is on a different origin // from what our page is served on. This might happen if a CDN is used to diff --git a/ui/v2.5/src/styles/_theme.scss b/ui/v2.5/src/styles/_theme.scss index 03f7e8b8c..e680335e2 100644 --- a/ui/v2.5/src/styles/_theme.scss +++ b/ui/v2.5/src/styles/_theme.scss @@ -126,14 +126,6 @@ hr { border: none; border-color: #414c53; padding: 0.25rem 0.75rem; - - &:first-child { - padding-left: 0; - } - - &:last-child { - padding-right: 0; - } } } diff --git a/ui/v2.5/src/utils/caption.ts b/ui/v2.5/src/utils/caption.ts index 91d7030b6..0c8509865 100644 --- a/ui/v2.5/src/utils/caption.ts +++ b/ui/v2.5/src/utils/caption.ts @@ -8,6 +8,7 @@ export const languageMap = new Map([ ["ko", "한국인"], ["nl", "Holandés"], ["pt", "Português"], + ["ru", "Русский"], ["00", "Unknown"], // stash reserved language code ]); diff --git a/ui/v2.5/src/utils/duration.ts b/ui/v2.5/src/utils/duration.ts index ca81863d6..7e45cdecc 100644 --- a/ui/v2.5/src/utils/duration.ts +++ b/ui/v2.5/src/utils/duration.ts @@ -45,7 +45,9 @@ const stringToSeconds = (v?: string) => { return seconds; }; -export default { +const DurationUtils = { secondsToString, stringToSeconds, }; + +export default DurationUtils; diff --git a/ui/v2.5/src/utils/editabletext.tsx b/ui/v2.5/src/utils/editabletext.tsx index 43d52ec5f..a7d143afb 100644 --- a/ui/v2.5/src/utils/editabletext.tsx +++ b/ui/v2.5/src/utils/editabletext.tsx @@ -1,8 +1,8 @@ import React from "react"; import { Form } from "react-bootstrap"; import { DurationInput } from "src/components/Shared/DurationInput"; -import { FilterSelect } from "src/components/Shared"; -import { DurationUtils } from "."; +import { FilterSelect } from "src/components/Shared/Select"; +import DurationUtils from "./duration"; const renderTextArea = (options: { value: string | undefined; @@ -202,4 +202,5 @@ const EditableTextUtils = { renderFilterSelect, renderMultiSelect, }; + export default EditableTextUtils; diff --git a/ui/v2.5/src/utils/field.tsx b/ui/v2.5/src/utils/field.tsx index a22b042cc..c947b8355 100644 --- a/ui/v2.5/src/utils/field.tsx +++ b/ui/v2.5/src/utils/field.tsx @@ -1,6 +1,6 @@ import React from "react"; import { FormattedMessage } from "react-intl"; -import { TruncatedText } from "../components/Shared"; +import { TruncatedText } from "src/components/Shared/TruncatedText"; interface ITextField { id?: string; diff --git a/ui/v2.5/src/utils/form.tsx b/ui/v2.5/src/utils/form.tsx index 2c273329a..ff1bc3f6c 100644 --- a/ui/v2.5/src/utils/form.tsx +++ b/ui/v2.5/src/utils/form.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { Form, Col, Row, ColProps, FormLabelProps } from "react-bootstrap"; import EditableTextUtils from "./editabletext"; @@ -165,4 +164,5 @@ const FormUtils = { renderFilterSelect, renderMultiSelect, }; + export default FormUtils; diff --git a/ui/v2.5/src/utils/gender.ts b/ui/v2.5/src/utils/gender.ts index b68faf4ef..29502695c 100644 --- a/ui/v2.5/src/utils/gender.ts +++ b/ui/v2.5/src/utils/gender.ts @@ -9,7 +9,7 @@ export const stringGenderMap = new Map([ ["Non-Binary", GQL.GenderEnum.NonBinary], ]); -export const genderToString = (value?: GQL.GenderEnum | string) => { +export const genderToString = (value?: GQL.GenderEnum | string | null) => { if (!value) { return undefined; } diff --git a/ui/v2.5/src/utils/image.tsx b/ui/v2.5/src/utils/image.tsx index 484c5d055..b31387e83 100644 --- a/ui/v2.5/src/utils/image.tsx +++ b/ui/v2.5/src/utils/image.tsx @@ -66,9 +66,10 @@ const imageToDataURL = async (url: string) => { }); }; -const Image = { +const ImageUtils = { onImageChange, usePasteImage, imageToDataURL, }; -export default Image; + +export default ImageUtils; diff --git a/ui/v2.5/src/utils/imageWall.ts b/ui/v2.5/src/utils/imageWall.ts new file mode 100644 index 000000000..3f50673e0 --- /dev/null +++ b/ui/v2.5/src/utils/imageWall.ts @@ -0,0 +1,23 @@ +export enum ImageWallDirection { + Column = "column", + Row = "row", +} + +export type ImageWallOptions = { + margin: number; + direction: ImageWallDirection; +}; + +export const defaultImageWallDirection: ImageWallDirection = + ImageWallDirection.Row; +export const defaultImageWallMargin = 3; + +export const imageWallDirectionIntlMap = new Map([ + [ImageWallDirection.Column, "dialogs.imagewall.direction.column"], + [ImageWallDirection.Row, "dialogs.imagewall.direction.row"], +]); + +export const defaultImageWallOptions = { + margin: defaultImageWallMargin, + direction: defaultImageWallDirection, +}; diff --git a/ui/v2.5/src/utils/index.ts b/ui/v2.5/src/utils/index.ts deleted file mode 100644 index 4ed80cc3e..000000000 --- a/ui/v2.5/src/utils/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -export { default as ScreenUtils } from "./screen"; -export { default as ImageUtils } from "./image"; -export { default as NavUtils } from "./navigation"; -export { default as TableUtils } from "./table"; -export { default as TextUtils } from "./text"; -export { default as EditableTextUtils } from "./editabletext"; -export { default as FormUtils } from "./form"; -export { default as DurationUtils } from "./duration"; -export { default as PercentUtils } from "./percent"; -export { default as SessionUtils } from "./session"; -export { default as flattenMessages } from "./flattenMessages"; -export * from "./country"; -export { default as useFocus } from "./focus"; -export { default as downloadFile } from "./download"; -export * from "./data"; -export { getStashIDs } from "./stashIds"; -export * from "./stashbox"; -export * from "./gender"; diff --git a/ui/v2.5/src/utils/lazyComponent.ts b/ui/v2.5/src/utils/lazyComponent.ts new file mode 100644 index 000000000..38bacceed --- /dev/null +++ b/ui/v2.5/src/utils/lazyComponent.ts @@ -0,0 +1,24 @@ +import { ComponentType, lazy } from "react"; + +interface ILazyComponentError { + __lazyComponentError?: true; +} + +export const isLazyComponentError = (e: unknown) => { + return !!(e as ILazyComponentError).__lazyComponentError; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const lazyComponent = >( + factory: Parameters>[0] +) => { + return lazy(async () => { + try { + return await factory(); + } catch (e) { + // set flag to identify lazy component loading errors + (e as ILazyComponentError).__lazyComponentError = true; + throw e; + } + }); +}; diff --git a/ui/v2.5/src/utils/navigation.ts b/ui/v2.5/src/utils/navigation.ts index bbdd34ef8..e9a0bb324 100644 --- a/ui/v2.5/src/utils/navigation.ts +++ b/ui/v2.5/src/utils/navigation.ts @@ -148,6 +148,18 @@ const makeStudioMoviesUrl = (studio: Partial) => { return `/movies?${filter.makeQueryParameters()}`; }; +const makeStudioPerformersUrl = (studio: Partial) => { + if (!studio.id) return "#"; + const filter = new ListFilterModel(GQL.FilterMode.Performers, undefined); + const criterion = new StudiosCriterion(); + criterion.value = { + items: [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }], + depth: 0, + }; + filter.criteria.push(criterion); + return `/performers?${filter.makeQueryParameters()}`; +}; + const makeChildStudiosUrl = (studio: Partial) => { if (!studio.id) return "#"; const filter = new ListFilterModel(GQL.FilterMode.Studios, undefined); @@ -297,7 +309,7 @@ const makeGalleryImagesUrl = ( return `/images?${filter.makeQueryParameters()}`; }; -export default { +const NavUtils = { makePerformerScenesUrl, makePerformerImagesUrl, makePerformerGalleriesUrl, @@ -307,6 +319,7 @@ export default { makeStudioImagesUrl, makeStudioGalleriesUrl, makeStudioMoviesUrl, + makeStudioPerformersUrl, makeParentTagsUrl, makeChildTagsUrl, makeTagSceneMarkersUrl, @@ -320,3 +333,5 @@ export default { makeChildStudiosUrl, makeGalleryImagesUrl, }; + +export default NavUtils; diff --git a/ui/v2.5/src/utils/percent.ts b/ui/v2.5/src/utils/percent.ts index 6616733af..893ec60b0 100644 --- a/ui/v2.5/src/utils/percent.ts +++ b/ui/v2.5/src/utils/percent.ts @@ -11,7 +11,9 @@ const stringToNumber = (v?: string) => { return parseInt(numStr, 10); }; -export default { +const PercentUtils = { numberToString, stringToNumber, }; + +export default PercentUtils; diff --git a/ui/v2.5/src/utils/rating.ts b/ui/v2.5/src/utils/rating.ts index f129239a5..8821f2ef9 100644 --- a/ui/v2.5/src/utils/rating.ts +++ b/ui/v2.5/src/utils/rating.ts @@ -7,6 +7,7 @@ export enum RatingStarPrecision { Full = "full", Half = "half", Quarter = "quarter", + Tenth = "tenth", } export const defaultRatingSystemType: RatingSystemType = RatingSystemType.Stars; @@ -37,6 +38,10 @@ export const ratingStarPrecisionIntlMap = new Map([ RatingStarPrecision.Quarter, "config.ui.editing.rating_system.star_precision.options.quarter", ], + [ + RatingStarPrecision.Tenth, + "config.ui.editing.rating_system.star_precision.options.tenth", + ], ]); export type RatingSystemOptions = { @@ -66,6 +71,8 @@ export function getRatingPrecision(precision: RatingStarPrecision) { return 0.5; case RatingStarPrecision.Quarter: return 0.25; + case RatingStarPrecision.Tenth: + return 0.1; default: return 1; } diff --git a/ui/v2.5/src/utils/screen.ts b/ui/v2.5/src/utils/screen.ts index e79fb1c84..16b13feb4 100644 --- a/ui/v2.5/src/utils/screen.ts +++ b/ui/v2.5/src/utils/screen.ts @@ -1,6 +1,8 @@ const isMobile = () => window.matchMedia("only screen and (max-width: 767px)").matches; -export default { +const ScreenUtils = { isMobile, }; + +export default ScreenUtils; diff --git a/ui/v2.5/src/utils/session.ts b/ui/v2.5/src/utils/session.ts index 2e0d3cdf3..33c307034 100644 --- a/ui/v2.5/src/utils/session.ts +++ b/ui/v2.5/src/utils/session.ts @@ -4,6 +4,8 @@ const isLoggedIn = () => { return new Cookies().get("session") !== undefined; }; -export default { +const SessionUtils = { isLoggedIn, }; + +export default SessionUtils; diff --git a/ui/v2.5/src/utils/table.tsx b/ui/v2.5/src/utils/table.tsx index e78ba579d..ca408696e 100644 --- a/ui/v2.5/src/utils/table.tsx +++ b/ui/v2.5/src/utils/table.tsx @@ -1,4 +1,3 @@ -import React from "react"; import EditableTextUtils from "./editabletext"; const renderEditableTextTableRow = (options: { @@ -94,7 +93,7 @@ const renderMultiSelect = (options: { ); -const Table = { +const TableUtils = { renderEditableTextTableRow, renderTextArea, renderInputGroup, @@ -103,4 +102,5 @@ const Table = { renderFilterSelect, renderMultiSelect, }; -export default Table; + +export default TableUtils; diff --git a/ui/v2.5/src/utils/text.ts b/ui/v2.5/src/utils/text.ts index 6e67276e3..a4150df91 100644 --- a/ui/v2.5/src/utils/text.ts +++ b/ui/v2.5/src/utils/text.ts @@ -189,6 +189,70 @@ const stringToDate = (dateString: string) => { return new Date(year, monthIndex, day, 0, 0, 0, 0); }; +const stringToFuzzyDate = (dateString: string) => { + if (!dateString) return null; + + const parts = dateString.split("-"); + // Invalid date string + let year = Number(parts[0]); + if (isNaN(year)) year = new Date().getFullYear(); + let monthIndex = 0; + if (parts.length > 1) { + monthIndex = Math.max(0, Number(parts[1]) - 1); + if (monthIndex > 11 || isNaN(monthIndex)) monthIndex = 0; + } + let day = 1; + if (parts.length > 2) { + day = Number(parts[2]); + if (day > 31 || isNaN(day)) day = 1; + } + + return new Date(year, monthIndex, day, 0, 0, 0, 0); +}; + +const stringToFuzzyDateTime = (dateString: string) => { + if (!dateString) return null; + + const dateTime = dateString.split(" "); + + let date: Date | null = null; + if (dateTime.length > 0) { + date = stringToFuzzyDate(dateTime[0]); + } + + if (!date) { + date = new Date(); + } + + if (dateTime.length > 1) { + const timeParts = dateTime[1].split(":"); + if (date && timeParts.length > 0) { + date.setHours(Number(timeParts[0])); + } + if (date && timeParts.length > 1) { + date.setMinutes(Number(timeParts[1])); + } + if (date && timeParts.length > 2) { + date.setSeconds(Number(timeParts[2])); + } + } + + return date; +}; + +function dateToString(date: Date) { + return `${date.getFullYear()}-${(date.getMonth() + 1) + .toString() + .padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`; +} + +function dateTimeToString(date: Date) { + return `${dateToString(date)} ${date + .getHours() + .toString() + .padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`; +} + const getAge = (dateString?: string | null, fromDateString?: string | null) => { if (!dateString) return 0; @@ -284,6 +348,26 @@ const sanitiseURL = (url?: string, siteURL?: URL) => { return `https://${url}`; }; +const domainFromURL = (urlString?: string, url?: URL) => { + if (url) { + return url.hostname; + } else if (urlString) { + var urlDomain = ""; + try { + var sanitizedUrl = sanitiseURL(urlString); + if (sanitizedUrl) { + urlString = sanitizedUrl; + } + urlDomain = new URL(urlString).hostname; + } catch { + urlDomain = urlString; // We cant determine the hostname so we return the base string + } + return urlDomain; + } else { + return ""; + } +}; + const formatDate = (intl: IntlShape, date?: string, utc = true) => { if (!date) { return ""; @@ -335,10 +419,15 @@ const TextUtils = { secondsToTimestamp, fileNameFromPath, stringToDate, + stringToFuzzyDate, + stringToFuzzyDateTime, + dateToString, + dateTimeToString, age: getAge, bitRate, resolution, sanitiseURL, + domainFromURL, twitterURL, instagramURL, formatDate, diff --git a/ui/v2.5/src/utils/units.ts b/ui/v2.5/src/utils/units.ts index 63a2199d4..3115eed5f 100644 --- a/ui/v2.5/src/utils/units.ts +++ b/ui/v2.5/src/utils/units.ts @@ -1,11 +1,11 @@ export function cmToImperial(cm: number) { const cmInInches = 0.393700787; const inchesInFeet = 12; - const inches = Math.floor(cm * cmInInches); + const inches = Math.round(cm * cmInInches); const feet = Math.floor(inches / inchesInFeet); return [feet, inches % inchesInFeet]; } export function kgToLbs(kg: number) { - return Math.floor(kg * 2.20462262185); + return Math.round(kg * 2.20462262185); } diff --git a/ui/v2.5/tsconfig.json b/ui/v2.5/tsconfig.json index 4ae477afe..93c8c0b5a 100644 --- a/ui/v2.5/tsconfig.json +++ b/ui/v2.5/tsconfig.json @@ -1,33 +1,25 @@ { "compilerOptions": { - "target": "ESNext", - "lib": [ - "dom", - "dom.iterable", - "esnext", - "esnext.intl", - "es2017.intl", - "es2018.intl" - ], + "target": "es2019", + "lib": ["dom", "dom.iterable", "esnext"], "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, - "module": "esnext", + "module": "es2020", "moduleResolution": "node", "resolveJsonModule": true, "noEmit": true, "jsx": "react-jsx", - "downlevelIteration": true, "experimentalDecorators": true, "baseUrl": ".", "sourceMap": true, "allowJs": true, "isolatedModules": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "useDefineForClassFields": true, + "types": ["vite/client"] }, - "include": [ - "src/**/*" - ] + "include": ["src"] } diff --git a/ui/v2.5/vite.config.js b/ui/v2.5/vite.config.js index ffa2cf0cb..544d2f67b 100644 --- a/ui/v2.5/vite.config.js +++ b/ui/v2.5/vite.config.js @@ -1,27 +1,39 @@ -import { defineConfig } from 'vite' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import legacy from "@vitejs/plugin-legacy"; import tsconfigPaths from "vite-tsconfig-paths"; -import viteCompression from 'vite-plugin-compression'; +import viteCompression from "vite-plugin-compression"; // https://vitejs.dev/config/ export default defineConfig({ base: "", build: { - outDir: 'build', + outDir: "build", + reportCompressedSize: false, + rollupOptions: { + output: { + experimentalDeepDynamicChunkOptimization: true, + }, + }, }, optimizeDeps: { - entries: "src/index.tsx" + entries: "src/index.tsx", }, server: { - cors: false + port: 3000, + cors: false, }, - publicDir: 'public', - assetsInclude: ['**/*.md'], - plugins: [tsconfigPaths(), + publicDir: "public", + assetsInclude: ["**/*.md"], + plugins: [ + react(), + legacy(), + tsconfigPaths(), viteCompression({ - algorithm: 'gzip', - disable: false, - deleteOriginFile: true, - filter: /\.(js|json|css|svg|md)$/i - }) -], -}) + algorithm: "gzip", + disable: false, + deleteOriginFile: true, + filter: /\.(js|json|css|svg|md)$/i, + }), + ], +}); diff --git a/ui/v2.5/yarn.lock b/ui/v2.5/yarn.lock index 95829b3d8..32d6db07d 100644 --- a/ui/v2.5/yarn.lock +++ b/ui/v2.5/yarn.lock @@ -2,309 +2,640 @@ # yarn lockfile v1 -"@apollo/client@^3.1.3", "@apollo/client@^3.2.5", "@apollo/client@^3.3.7": - version "3.3.12" - resolved "https://registry.npmjs.org/@apollo/client/-/client-3.3.12.tgz" - integrity sha512-1wLVqRpujzbLRWmFPnRCDK65xapOe2txY0sTI+BaqEbumMUVNS3vxojT6hRHf9ODFEK+F6MLrud2HGx0mB3eQw== +"@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== dependencies: - "@graphql-typed-document-node/core" "^3.0.0" - "@types/zen-observable" "^0.8.0" - "@wry/context" "^0.5.2" - "@wry/equality" "^0.3.0" - fast-json-stable-stringify "^2.0.0" - graphql-tag "^2.12.0" + "@jridgewell/gen-mapping" "^0.1.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.7.0", "@apollo/client@^3.7.8": + version "3.7.8" + resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.7.8.tgz#e1c8dfd02cbbe1baf9b18fa86918904efd9cc580" + integrity sha512-o1NxF4ytET2w9HSVMLwYUEEdv6H3XPpbh9M+ABVGnUVT0s6T9pgqRtYO4pFP1TmeDmb1pbRfVhFwh3gC167j5Q== + dependencies: + "@graphql-typed-document-node/core" "^3.1.1" + "@wry/context" "^0.7.0" + "@wry/equality" "^0.5.0" + "@wry/trie" "^0.3.0" + graphql-tag "^2.12.6" hoist-non-react-statics "^3.3.2" - optimism "^0.14.0" + optimism "^0.16.1" prop-types "^15.7.2" - symbol-observable "^2.0.0" - ts-invariant "^0.6.2" - tslib "^1.10.0" - zen-observable "^0.8.14" + 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/aggregate-error@0.0.6": - version "0.0.6" - resolved "https://registry.npmjs.org/@ardatan/aggregate-error/-/aggregate-error-0.0.6.tgz" - integrity sha512-vyrkEHG1jrukmzTPtyWB4NLPauUw5bQeg4uhn8f+1SSynmrOcyvlb1GKQjjgoBzElLdfXCRYX8UnBlhklOHYRQ== +"@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: - tslib "~2.0.1" + "@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" -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== +"@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: - "@babel/highlight" "^7.10.4" + node-fetch "^2.6.1" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" - integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== dependencies: - "@babel/highlight" "^7.12.13" + "@babel/highlight" "^7.18.6" -"@babel/compat-data@^7.13.8": - version "7.13.12" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.12.tgz#a8a5ccac19c200f9dd49624cac6e19d7be1236a1" - integrity sha512-3eJJ841uKxeV8dcN/2yGEUy+RfgQspPEgQat85umsE1rotuquQ2AbIub4S6j7c50a2d+4myc+zSlnXeIHrOnhQ== +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.0.tgz#c241dc454e5b5917e40d37e525e2f4530c399298" + integrity sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g== -"@babel/core@>=7.9.0", "@babel/core@^7.0.0", "@babel/core@^7.9.0": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.10.tgz#07de050bbd8193fcd8a3c27918c0890613a94559" - integrity sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw== +"@babel/compat-data@^7.20.5": + version "7.20.14" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.14.tgz#4106fc8b755f3e3ee0a0a7c27dde5de1d2b2baf8" + integrity sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw== + +"@babel/core@^7.14.0", "@babel/core@^7.20.12", "@babel/core@^7.9.0": + version "7.20.12" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d" + integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg== dependencies: - "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.13.9" - "@babel/helper-compilation-targets" "^7.13.10" - "@babel/helper-module-transforms" "^7.13.0" - "@babel/helpers" "^7.13.10" - "@babel/parser" "^7.13.10" - "@babel/template" "^7.12.13" - "@babel/traverse" "^7.13.0" - "@babel/types" "^7.13.0" + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.20.7" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-module-transforms" "^7.20.11" + "@babel/helpers" "^7.20.7" + "@babel/parser" "^7.20.7" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.12" + "@babel/types" "^7.20.7" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" - json5 "^2.1.2" - lodash "^4.17.19" + json5 "^2.2.2" semver "^6.3.0" - source-map "^0.5.0" -"@babel/generator@^7.12.13", "@babel/generator@^7.13.0", "@babel/generator@^7.13.9", "@babel/generator@^7.5.0": - version "7.13.9" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39" - integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw== +"@babel/generator@^7.14.0", "@babel/generator@^7.18.13", "@babel/generator@^7.20.7": + version "7.20.14" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.14.tgz#9fa772c9f86a46c6ac9b321039400712b96f64ce" + integrity sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg== dependencies: - "@babel/types" "^7.13.0" + "@babel/types" "^7.20.7" + "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" - source-map "^0.5.0" -"@babel/helper-annotate-as-pure@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz#0f58e86dfc4bb3b1fcd7db806570e177d439b6ab" - integrity sha512-7YXfX5wQ5aYM/BOlbSccHDbuXXFPxeoUmfWtz8le2yTkTZc+BxsiEnENFoi2SlmA8ewDkG2LgIMIVzzn2h8kfw== +"@babel/generator@^7.21.0": + version "7.21.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.1.tgz#951cc626057bc0af2c35cd23e9c64d384dea83dd" + integrity sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA== dependencies: - "@babel/types" "^7.12.13" + "@babel/types" "^7.21.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" -"@babel/helper-compilation-targets@^7.13.10", "@babel/helper-compilation-targets@^7.13.8": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.10.tgz#1310a1678cb8427c07a753750da4f8ce442bdd0c" - integrity sha512-/Xju7Qg1GQO4mHZ/Kcs6Au7gfafgZnwm+a7sy/ow/tV1sHeraRUHbjdat8/UvDor4Tez+siGKDk6zIKtCPKVJA== +"@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/compat-data" "^7.13.8" - "@babel/helper-validator-option" "^7.12.17" - browserslist "^4.14.5" + "@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": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb" + integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ== + dependencies: + "@babel/compat-data" "^7.20.5" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.21.3" + lru-cache "^5.1.1" semver "^6.3.0" -"@babel/helper-create-class-features-plugin@^7.13.0": - version "7.13.11" - resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.11.tgz" - integrity sha512-ays0I7XYq9xbjCSvT+EvysLgfc3tOkwCULHjrnscGT3A9qD4sk3wXnJ3of0MAWsWGjdinFvajHU2smYuqXKMrw== +"@babel/helper-create-class-features-plugin@^7.18.6": + version "7.20.12" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz#4349b928e79be05ed2d1643b20b99bb87c503819" + integrity sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ== dependencies: - "@babel/helper-function-name" "^7.12.13" - "@babel/helper-member-expression-to-functions" "^7.13.0" - "@babel/helper-optimise-call-expression" "^7.12.13" - "@babel/helper-replace-supers" "^7.13.0" - "@babel/helper-split-export-declaration" "^7.12.13" + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" + "@babel/helper-member-expression-to-functions" "^7.20.7" + "@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-function-name@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a" - integrity sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA== +"@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-get-function-arity" "^7.12.13" - "@babel/template" "^7.12.13" - "@babel/types" "^7.12.13" + "@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-get-function-arity@^7.12.13": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.0.tgz#0088c7486b29a9cb5d948b1a1de46db66e089cfa" - integrity sha512-ASCquNcywC1NkYh/z7Cgp3w31YW8aojjYIlNg4VeJiHkqyP4AzIvr4qx7pYDb4/s8YcsZWqqOSxgkvjUz1kpDQ== +"@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/types" "^7.16.0" + "@babel/helper-annotate-as-pure" "^7.18.6" + regexpu-core "^5.3.1" -"@babel/helper-member-expression-to-functions@^7.13.0": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz" - integrity sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA== +"@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/types" "^7.15.4" + "@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-member-expression-to-functions@^7.13.12": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.0.tgz#29287040efd197c77636ef75188e81da8bccd5a4" - integrity sha512-bsjlBFPuWT6IWhl28EdrQ+gTvSvj5tqVP5Xeftp07SEuz5pLnsXZuDkDD3Rfcxy0IsHmbZ+7B2/9SHzxO0T+sQ== +"@babel/helper-environment-visitor@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" + integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== + +"@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.16.0" + "@babel/types" "^7.18.6" -"@babel/helper-module-imports@^7.13.12": - version "7.13.12" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977" - integrity sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA== +"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" + integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== dependencies: - "@babel/types" "^7.13.12" + "@babel/template" "^7.18.10" + "@babel/types" "^7.19.0" -"@babel/helper-module-transforms@^7.13.0": - version "7.13.12" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.12.tgz#600e58350490828d82282631a1422268e982ba96" - integrity sha512-7zVQqMO3V+K4JOOj40kxiCrMf6xlQAkewBB0eu2b03OO/Q21ZutOzjpfD79A5gtE/2OWi1nv625MrDlGlkbknQ== +"@babel/helper-function-name@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz#d552829b10ea9f120969304023cd0645fa00b1b4" + integrity sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg== dependencies: - "@babel/helper-module-imports" "^7.13.12" - "@babel/helper-replace-supers" "^7.13.12" - "@babel/helper-simple-access" "^7.13.12" - "@babel/helper-split-export-declaration" "^7.12.13" - "@babel/helper-validator-identifier" "^7.12.11" - "@babel/template" "^7.12.13" - "@babel/traverse" "^7.13.0" - "@babel/types" "^7.13.12" + "@babel/template" "^7.20.7" + "@babel/types" "^7.21.0" -"@babel/helper-optimise-call-expression@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz#5c02d171b4c8615b1e7163f888c1c81c30a2aaea" - integrity sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA== +"@babel/helper-hoist-variables@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" + integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== dependencies: - "@babel/types" "^7.12.13" + "@babel/types" "^7.18.6" -"@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz" - integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== - -"@babel/helper-replace-supers@^7.12.13", "@babel/helper-replace-supers@^7.13.0", "@babel/helper-replace-supers@^7.13.12": - version "7.13.12" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz#6442f4c1ad912502481a564a7386de0c77ff3804" - integrity sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw== +"@babel/helper-member-expression-to-functions@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz#a6f26e919582275a93c3aa6594756d71b0bb7f05" + integrity sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw== dependencies: - "@babel/helper-member-expression-to-functions" "^7.13.12" - "@babel/helper-optimise-call-expression" "^7.12.13" - "@babel/traverse" "^7.13.0" - "@babel/types" "^7.13.12" + "@babel/types" "^7.20.7" -"@babel/helper-simple-access@^7.12.13": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz" - integrity sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg== +"@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.15.4" + "@babel/types" "^7.21.0" -"@babel/helper-simple-access@^7.13.12": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.0.tgz#21d6a27620e383e37534cf6c10bba019a6f90517" - integrity sha512-o1rjBT/gppAqKsYfUdfHq5Rk03lMQrkPHG1OWzHWpLgVXRH4HnMM9Et9CVdIqwkCQlobnGHEJMsgWP/jE1zUiw== +"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" + integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== dependencies: - "@babel/types" "^7.16.0" + "@babel/types" "^7.18.6" -"@babel/helper-skip-transparent-expression-wrappers@^7.12.1": - version "7.12.1" - resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz" - integrity sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA== +"@babel/helper-module-transforms@^7.18.6": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.0.tgz#89a8f86ad748870e3d024e470b2e8405e869db67" + integrity sha512-eD/JQ21IG2i1FraJnTMbUarAUkA7G988ofehG5MDCRXaUU91rEBJuCeSoou2Sk1y4RbLYXzqEg1QLwEmRU4qcQ== dependencies: - "@babel/types" "^7.12.1" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.19.1" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.21.0" + "@babel/types" "^7.21.0" -"@babel/helper-split-export-declaration@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz#e9430be00baf3e88b0e13e6f9d4eaf2136372b05" - integrity sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg== +"@babel/helper-module-transforms@^7.20.11": + version "7.20.11" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz#df4c7af713c557938c50ea3ad0117a7944b2f1b0" + integrity sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg== dependencies: - "@babel/types" "^7.12.13" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.19.1" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.10" + "@babel/types" "^7.20.7" -"@babel/helper-validator-identifier@^7.12.11", "@babel/helper-validator-identifier@^7.14.9", "@babel/helper-validator-identifier@^7.15.7": - version "7.15.7" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz" - integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== - -"@babel/helper-validator-option@^7.12.17": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" - integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow== - -"@babel/helpers@^7.13.10": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.10.tgz#fd8e2ba7488533cdeac45cc158e9ebca5e3c7df8" - integrity sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ== +"@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/template" "^7.12.13" - "@babel/traverse" "^7.13.0" - "@babel/types" "^7.13.0" + "@babel/types" "^7.18.6" -"@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1" - integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg== +"@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-validator-identifier" "^7.12.11" + "@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": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" + integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== + dependencies: + "@babel/types" "^7.20.2" + +"@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": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" + integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-string-parser@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" + integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== + +"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + +"@babel/helper-validator-option@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" + integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== + +"@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.20.7": + version "7.20.13" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.13.tgz#e3cb731fb70dc5337134cadc24cbbad31cc87ad2" + integrity sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg== + dependencies: + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.13" + "@babel/types" "^7.20.7" + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@7.12.16": - version "7.12.16" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.12.16.tgz" - integrity sha512-c/+u9cqV6F0+4Hpq01jnJO+GLp2DdT63ppz9Xa+6cHaajM9VFzK/iDXiKK65YtpeVwu+ctfS6iqlMqRgQRzeCw== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.0", "@babel/parser@^7.16.8", "@babel/parser@^7.20.13", "@babel/parser@^7.20.7": + version "7.20.15" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.15.tgz#eec9f36d8eaf0948bb88c87a46784b5ee9fd0c89" + integrity sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg== -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.0", "@babel/parser@^7.13.10": - version "7.13.12" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.13.12.tgz#a3484d84d0b549f3fc916b99ee4783f26fabad2a" - integrity sha512-d0u3zWKcoZf379fOeJdr1a5WPDny4aOFZ6hlfKivgK0LY7ZxNfoaHL2fWwdGtHyVvra38FC+HVYkO+byfSA8AQ== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" - "@babel/plugin-proposal-optional-chaining" "^7.13.12" +"@babel/parser@^7.21.0": + version "7.21.1" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.1.tgz#a8f81ee2fe872af23faea4b17a08fcc869de7bcc" + integrity sha512-JzhBFpkuhBNYUY7qs+wTzNmyCWUHEaAFpQQD2YfU1rPL38/L43Wvid0fFkiOCnHvsGncRZgEPyGnltABLcVDTg== -"@babel/plugin-proposal-class-properties@^7.0.0": - version "7.13.0" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.13.0.tgz" - integrity sha512-KnTDjFNC1g+45ka0myZNvSBFLhNCLN+GeGYLDEA8Oq7MZ6yMgfLoIRh86GRT0FjtJhZw8JyUskP9uvj5pHM9Zg== +"@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-create-class-features-plugin" "^7.13.0" - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-proposal-object-rest-spread@^7.0.0": - version "7.13.8" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.13.8.tgz" - integrity sha512-DhB2EuB1Ih7S3/IRX5AFVgZ16k3EzfRbq97CxAVI1KSYcW+lexV8VZb7G7L8zuPVSdQMRn0kiBpf/Yzu9ZKH0g== +"@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/compat-data" "^7.13.8" - "@babel/helper-compilation-targets" "^7.13.8" - "@babel/helper-plugin-utils" "^7.13.0" + "@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.13.0" + "@babel/plugin-transform-parameters" "^7.20.7" -"@babel/plugin-proposal-optional-chaining@^7.13.12": - version "7.13.12" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.12.tgz#ba9feb601d422e0adea6760c2bd6bbb7bfec4866" - integrity sha512-fcEdKOkIB7Tf4IxrgEVeFC4zeJSTr78no9wTdBuZZbqF64kzllU0ybo2zrzm7gUQfxGhBgq4E39oRs8Zx/RMYQ== +"@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.13.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" + "@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-syntax-class-properties@^7.0.0": +"@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.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" + 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-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.12.13": - version "7.12.13" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.12.13.tgz" - integrity sha512-J/RYxnlSLXZLVR7wTRsozxKT8qbsx1mNKJzXEEjQ0Kjx1ZACcyHgbanNWNCFtc36IzuWhYWPpvJFFoexoOWFmA== +"@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.12.13" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.13.tgz#044fb81ebad6698fe62c478875575bcbb9b70f15" - integrity sha512-d4HM23Q1K7oq/SLNmG6mRt85l2csmQ0cHRaxRXjKW0YFdEXqlZ5kzFQKH5Uc3rDJECgu+yCRgPkG04Mm98R/1g== +"@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.12.13" + "@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", "@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.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" + 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" @@ -312,1080 +643,1650 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-transform-arrow-functions@^7.0.0": - version "7.13.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.13.0.tgz" - integrity sha512-96lgJagobeVmazXFaDrbmCLQxBysKu7U6Do3mLsx27gf5Dk85ezysrs2BZUpXD703U/Su1xTBDxxar2oa4jAGg== +"@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.13.0" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-block-scoped-functions@^7.0.0": - version "7.12.13" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.13.tgz" - integrity sha512-zNyFqbc3kI/fVpqwfqkg6RvBgFpC4J18aKKMmv7KdQ/1GgREapSJAykLMVNwfRGO3BtHj3YQZl8kxCXPcVMVeg== +"@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.12.13" + "@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": - version "7.12.13" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.13.tgz" - integrity sha512-Pxwe0iqWJX4fOOM2kEZeUuAxHMWb9nK+9oh5d11bsLoB0xMg+mkDpt0eYuDZB7ETrY9bbcVlKUGTOGWy7BHsMQ== + version "7.20.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.15.tgz#3e1b2aa9cbbe1eb8d644c823141a9c5c2a22392d" + integrity sha512-Vv4DMZ6MiNOhu/LdaZsT/bsLRxgL94d269Mv4R/9sp6+Mp++X/JqypZYypJXLlM4mlL352/Egzbzr98iABH1CA== dependencies: - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-plugin-utils" "^7.20.2" + +"@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": - version "7.13.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.13.0.tgz" - integrity sha512-9BtHCPUARyVH1oXGcSJD3YpsqRLROJx5ZNP6tN5vnk17N0SVf9WCtf8Nuh1CFmgByKKAIMstitKduoCmsaDK5g== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.7.tgz#f438216f094f6bb31dc266ebfab8ff05aecad073" + integrity sha512-LWYbsiXTPKl+oBlXUGlwNlJZetXD5Am+CyBdqhPsDVjM9Jc8jwBJFrKhHf900Kfk2eZG1y9MAG3UNajol7A4VQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.12.13" - "@babel/helper-function-name" "^7.12.13" - "@babel/helper-optimise-call-expression" "^7.12.13" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-replace-supers" "^7.13.0" - "@babel/helper-split-export-declaration" "^7.12.13" + "@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.19.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": - version "7.13.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.13.0.tgz" - integrity sha512-RRqTYTeZkZAz8WbieLTvKUEUxZlUTdmL5KGMyZj7FnMfLNKV4+r5549aORG/mgojRmFlQMJDUupwAMiF2Q7OUg== +"@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-plugin-utils" "^7.13.0" + "@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-destructuring@^7.0.0": - version "7.13.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.13.0.tgz" - integrity sha512-zym5em7tePoNT9s964c0/KU3JPPnuq7VhIxPRefJ4/s82cD+q1mgKfuGRDMCPL0HTyKz4dISuQlCusfgCJ86HA== +"@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.13.0" + "@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.13.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.13.0.tgz" - integrity sha512-EXAGFMJgSX8gxWD7PZtW/P6M+z74jpx3wm/+9pn+c2dOawPpBkUX7BrfyPvo6ZpXbgRIEuwgwDb/MGlKvu2pOg== + 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.13.0" - "@babel/plugin-syntax-flow" "^7.12.13" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/plugin-syntax-flow" "^7.18.6" "@babel/plugin-transform-for-of@^7.0.0": - version "7.13.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.13.0.tgz" - integrity sha512-IHKT00mwUVYE0zzbkDgNRP6SRzvfGCYsOxIRz8KsiaaHCcT9BWIkO+H9QRJseHBLOGBZkHUdHiqj6r0POsdytg== + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz#6ef8a50b244eb6a0bdbad0c7c61877e4e30097c1" + integrity sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-function-name@^7.0.0": - version "7.12.13" - resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.13.tgz" - integrity sha512-6K7gZycG0cmIwwF7uMK/ZqeCikCGVBdyP2J5SKNCXO5EOHcqi+z7Jwf8AmyDNcBgxET8DrEtCt/mPKPyAzXyqQ== +"@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-function-name" "^7.12.13" - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-plugin-utils" "^7.20.2" -"@babel/plugin-transform-literals@^7.0.0": - version "7.12.13" - resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.13.tgz" - integrity sha512-FW+WPjSR7hiUxMcKqyNjP05tQ2kmBCdpEpZHY1ARm96tGQCCBvXKnpjILtDplUnJ/eHZ0lALLM+d2lMFSpYJrQ== +"@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-plugin-utils" "^7.12.13" + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-plugin-utils" "^7.18.9" -"@babel/plugin-transform-member-expression-literals@^7.0.0": - version "7.12.13" - resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.13.tgz" - integrity sha512-kxLkOsg8yir4YeEPHLuO2tXP9R/gTjpuTOjshqSpELUN3ZAg2jfDnKUvzzJxObun38sw3wm4Uu69sX/zA7iRvg== +"@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.12.13" + "@babel/helper-plugin-utils" "^7.18.9" -"@babel/plugin-transform-modules-commonjs@^7.0.0": - version "7.13.8" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.13.8.tgz" - integrity sha512-9QiOx4MEGglfYZ4XOnU79OHr6vIWUakIj9b4mioN8eQIoEh+pf5p/zEB36JpDFWA12nNMiRf7bfoRvl9Rn79Bw== +"@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-module-transforms" "^7.13.0" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-simple-access" "^7.12.13" - babel-plugin-dynamic-import-node "^2.3.3" + "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-object-super@^7.0.0": - version "7.12.13" - resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.13.tgz" - integrity sha512-JzYIcj3XtYspZDV8j9ulnoMPZZnF/Cj0LUxPOjR89BdBVx+zYJI9MdMIlUZjbXDX+6YVeS6I3e8op+qQ3BYBoQ== +"@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-plugin-utils" "^7.12.13" - "@babel/helper-replace-supers" "^7.12.13" + "@babel/helper-module-transforms" "^7.20.11" + "@babel/helper-plugin-utils" "^7.20.2" -"@babel/plugin-transform-parameters@^7.0.0", "@babel/plugin-transform-parameters@^7.13.0": - version "7.13.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.13.0.tgz" - integrity sha512-Jt8k/h/mIwE2JFEOb3lURoY5C85ETcYPnbuAJ96zRBzh1XHtQZfs62ChZ6EP22QlC8c7Xqr9q+e1SU5qttwwjw== +"@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-plugin-utils" "^7.13.0" + "@babel/helper-module-transforms" "^7.20.11" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-simple-access" "^7.20.2" -"@babel/plugin-transform-property-literals@^7.0.0": - version "7.12.13" - resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.13.tgz" - integrity sha512-nqVigwVan+lR+g8Fj8Exl0UQX2kymtjcWfMOYM1vTYEKujeyv2SkMgazf2qNcK7l4SDiKyTA/nHCPqL4e2zo1A== +"@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-plugin-utils" "^7.12.13" + "@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.12.13" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.13.tgz" - integrity sha512-MprESJzI9O5VnJZrL7gg1MpdqmiFcUv41Jc7SahxYsNP2kDkFqClxxTZq+1Qv4AFCamm+GXMRDQINNn+qrxmiA== + 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.12.13" + "@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.13.12" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.13.12.tgz#1df5dfaf0f4b784b43e96da6f28d630e775f68b3" - integrity sha512-jcEI2UqIcpCqB5U5DRxIl0tQEProI2gcu+g8VTIqxLO5Iidojb4d77q+fwGseCvd8af/lJ9masp4QWzBXFE2xA== + 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.12.13" - "@babel/helper-module-imports" "^7.13.12" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/plugin-syntax-jsx" "^7.12.13" - "@babel/types" "^7.13.12" + "@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-shorthand-properties@^7.0.0": - version "7.12.13" - resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.13.tgz" - integrity sha512-xpL49pqPnLtf0tVluuqvzWIgLEhuPpZzvs2yabUHSKRNlN7ScYU7aMlmavOeyXJZKgZKQRBlh8rHbKiJDraTSw== +"@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.12.13" + "@babel/helper-plugin-utils" "^7.20.2" + regenerator-transform "^0.15.1" -"@babel/plugin-transform-spread@^7.0.0": - version "7.13.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.13.0.tgz" - integrity sha512-V6vkiXijjzYeFmQTr3dBxPtZYLPcUfY34DebOU27jIl2M/Y8Egm52Hw82CSjjPqd54GTlJs5x+CR7HeNr24ckg== +"@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.13.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" + "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-template-literals@^7.0.0": - version "7.13.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.13.0.tgz" - integrity sha512-d67umW6nlfmr1iehCcBv69eSUSySk1EsIS8aTDX4Xo9qajAh6mYtcl4kJrBkGXuxZPEgVr7RVfAvNW6YQkd4Mw== +"@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.13.0" + "@babel/helper-plugin-utils" "^7.18.6" -"@babel/runtime-corejs3@^7.10.2": - version "7.13.10" - resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.13.10.tgz" - integrity sha512-x/XYVQ1h684pp1mJwOV4CyvqZXqbc8CMsMGUnAbuc82ZCdv1U63w5RSUzgDSXQHG5Rps/kiksH6g2D5BuaKyXg== +"@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: - core-js-pure "^3.0.0" - regenerator-runtime "^0.13.4" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.4.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7": - version "7.13.10" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz" - integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== +"@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: - regenerator-runtime "^0.13.4" + "@babel/helper-plugin-utils" "^7.18.6" -"@babel/template@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" - integrity sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA== +"@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/code-frame" "^7.12.13" - "@babel/parser" "^7.12.13" - "@babel/types" "^7.12.13" + "@babel/helper-plugin-utils" "^7.18.9" -"@babel/traverse@7.12.13": - version "7.12.13" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz" - integrity sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA== +"@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/code-frame" "^7.12.13" - "@babel/generator" "^7.12.13" - "@babel/helper-function-name" "^7.12.13" - "@babel/helper-split-export-declaration" "^7.12.13" - "@babel/parser" "^7.12.13" - "@babel/types" "^7.12.13" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.19" + "@babel/helper-plugin-utils" "^7.18.9" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.0.tgz#6d95752475f86ee7ded06536de309a65fc8966cc" - integrity sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ== +"@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/code-frame" "^7.12.13" - "@babel/generator" "^7.13.0" - "@babel/helper-function-name" "^7.12.13" - "@babel/helper-split-export-declaration" "^7.12.13" - "@babel/parser" "^7.13.0" - "@babel/types" "^7.13.0" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.19" + "@babel/helper-plugin-utils" "^7.18.9" -"@babel/types@7.12.13": - version "7.12.13" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz" - integrity sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ== +"@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-validator-identifier" "^7.12.11" - lodash "^4.17.19" - to-fast-properties "^2.0.0" + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" -"@babel/types@^7.0.0", "@babel/types@^7.12.1", "@babel/types@^7.12.13", "@babel/types@^7.13.0", "@babel/types@^7.13.12", "@babel/types@^7.3.0", "@babel/types@^7.9.5": - version "7.13.12" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz" - integrity sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA== +"@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/helper-validator-identifier" "^7.12.11" - lodash "^4.17.19" - to-fast-properties "^2.0.0" + "@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/types@^7.15.4": - version "7.15.6" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz" - integrity sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig== +"@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-validator-identifier" "^7.14.9" - to-fast-properties "^2.0.0" + "@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/types@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.0.tgz#db3b313804f96aadd0b776c4823e127ad67289ba" - integrity sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg== - dependencies: - "@babel/helper-validator-identifier" "^7.15.7" - to-fast-properties "^2.0.0" - -"@cush/relative@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@cush/relative/-/relative-1.0.0.tgz" - integrity sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA== - -"@emotion/cache@^11.0.0", "@emotion/cache@^11.1.3": - version "11.1.3" - resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.1.3.tgz" - integrity sha512-n4OWinUPJVaP6fXxWZD9OUeQ0lY7DvtmtSuqtRWT0Ofo/sBLCVSgb4/Oa0Q5eFxcwablRKjUXqXtNZVyEwCAuA== - dependencies: - "@emotion/memoize" "^0.7.4" - "@emotion/sheet" "^1.0.0" - "@emotion/utils" "^1.0.0" - "@emotion/weak-memoize" "^0.2.5" - stylis "^4.0.3" - -"@emotion/hash@^0.8.0": +"@babel/regjsgen@^0.8.0": version "0.8.0" - resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz" - integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" + integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@emotion/memoize@^0.7.4": - version "0.7.5" - resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.5.tgz" - integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== - -"@emotion/react@^11.1.1": - version "11.1.5" - resolved "https://registry.npmjs.org/@emotion/react/-/react-11.1.5.tgz" - integrity sha512-xfnZ9NJEv9SU9K2sxXM06lzjK245xSeHRpUh67eARBm3PBHjjKIZlfWZ7UQvD0Obvw6ZKjlC79uHrlzFYpOB/Q== +"@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.18.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7": + version "7.20.13" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b" + integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA== dependencies: - "@babel/runtime" "^7.7.2" - "@emotion/cache" "^11.1.3" - "@emotion/serialize" "^1.0.0" - "@emotion/sheet" "^1.0.1" - "@emotion/utils" "^1.0.0" - "@emotion/weak-memoize" "^0.2.5" + regenerator-runtime "^0.13.11" + +"@babel/runtime@^7.8.4": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" + integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== + dependencies: + regenerator-runtime "^0.13.11" + +"@babel/template@^7.18.10", "@babel/template@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" + integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + +"@babel/traverse@^7.14.0", "@babel/traverse@^7.16.8", "@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.13", "@babel/traverse@^7.20.7": + version "7.20.13" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.13.tgz#817c1ba13d11accca89478bd5481b2d168d07473" + integrity sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.20.7" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.20.13" + "@babel/types" "^7.20.7" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/traverse@^7.20.5", "@babel/traverse@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.0.tgz#0e1807abd5db98e6a19c204b80ed1e3f5bca0edc" + integrity sha512-Xdt2P1H4LKTO8ApPfnO1KmzYMFpp7D/EinoXzLYN/cHcBNrVCAkAtGUcXnHXrl/VGktureU6fkQrHSBE2URfoA== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.21.0" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.21.0" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.21.0" + "@babel/types" "^7.21.0" + debug "^4.1.0" + 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.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.3.0", "@babel/types@^7.9.5": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f" + integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + +"@babel/types@^7.18.9", "@babel/types@^7.20.5", "@babel/types@^7.21.0", "@babel/types@^7.4.4": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.0.tgz#1da00d89c2f18b226c9207d96edbeb79316a1819" + integrity sha512-uR7NWq2VNFnDi7EYqiRz2Jv/VQIu38tu64Zy8TX2nQFQ6etJ9V/Rr2msW8BS132mum2rL645qpDrLtAJtVpuow== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + 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.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.0.1.tgz#ff02629c7c95d1f4f8ea84d5ef1173461610535e" + integrity sha512-B9/8PmOtU6nBiibJg0glnNktQDZ3rZnGn/7UmDfrm2vMtrdlXO3p7ErE95N0up80IRk9YEtB5jyj/TmQ1WH3dw== + +"@csstools/css-tokenizer@^2.0.1": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.0.2.tgz#3635560ffc8f1994295d7ce3482e14f956d3f9e1" + integrity sha512-prUTipz0NZH7Lc5wyBUy93NFy3QYDMVEQgSeZzNdpMbKRd6V2bgRFyJ+O0S0Dw0MXWuE/H9WXlJk3kzMZRHZ/g== + +"@csstools/media-query-list-parser@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.0.1.tgz#d85a366811563a5d002755ed10e5212a1613c91d" + integrity sha512-X2/OuzEbjaxhzm97UJ+95GrMeT29d1Ib+Pu+paGLuRWZnWRK9sI9r3ikmKXPWGA1C4y4JEdBEFpp9jEqCvLeRA== + +"@csstools/selector-specificity@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.1.1.tgz#c9c61d9fe5ca5ac664e1153bb0aa0eba1c6d6308" + integrity sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw== + +"@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.0.0": - version "1.0.1" - resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.1.tgz" - integrity sha512-TXlKs5sgUKhFlszp/rg4lIAZd7UUSmJpwaf9/lAEFcUh2vPi32i7x4wk7O8TN8L8v2Ol8k0CxnhRBY0zQalTxA== +"@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.8.0" - "@emotion/memoize" "^0.7.4" - "@emotion/unitless" "^0.7.5" - "@emotion/utils" "^1.0.0" + "@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.0.0", "@emotion/sheet@^1.0.1": - version "1.0.1" - resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.0.1.tgz" - integrity sha512-GbIvVMe4U+Zc+929N1V7nW6YYJtidj31lidSmdYcWozwoBIObXBnaJkKNDjZrLm9Nc0BR+ZyHNaRZxqNZbof5g== +"@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.7.5": - version "0.7.5" - resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz" - integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== +"@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/utils@^1.0.0": +"@emotion/use-insertion-effect-with-fallbacks@^1.0.0": version "1.0.0" - resolved "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz" - integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA== + 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/weak-memoize@^0.2.5": - version "0.2.5" - resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz" - integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== +"@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== -"@endemolshinegroup/cosmiconfig-typescript-loader@3.0.2": - version "3.0.2" - resolved "https://registry.npmjs.org/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz" - integrity sha512-QRVtqJuS1mcT56oHpVegkKBlgtWjXw/gHNWO3eL9oyB5Sc7HBoc2OLG/nYpVfT/Jejvo3NUrD0Udk7XgoyDKkA== - dependencies: - lodash.get "^4" - make-error "^1" - ts-node "^9" - tslib "^2" +"@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/linux-loong64@0.14.54": - version "0.14.54" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028" - integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw== +"@esbuild/android-arm64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz#cf91e86df127aa3d141744edafcba0abdc577d23" + integrity sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg== -"@eslint/eslintrc@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" - integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== +"@esbuild/android-arm@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.16.17.tgz#025b6246d3f68b7bbaa97069144fb5fb70f2fff2" + integrity sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw== + +"@esbuild/android-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.16.17.tgz#c820e0fef982f99a85c4b8bfdd582835f04cd96e" + integrity sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ== + +"@esbuild/darwin-arm64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz#edef4487af6b21afabba7be5132c26d22379b220" + integrity sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w== + +"@esbuild/darwin-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz#42829168730071c41ef0d028d8319eea0e2904b4" + integrity sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg== + +"@esbuild/freebsd-arm64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz#1f4af488bfc7e9ced04207034d398e793b570a27" + integrity sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw== + +"@esbuild/freebsd-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz#636306f19e9bc981e06aa1d777302dad8fddaf72" + integrity sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug== + +"@esbuild/linux-arm64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz#a003f7ff237c501e095d4f3a09e58fc7b25a4aca" + integrity sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g== + +"@esbuild/linux-arm@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz#b591e6a59d9c4fe0eeadd4874b157ab78cf5f196" + integrity sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ== + +"@esbuild/linux-ia32@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz#24333a11027ef46a18f57019450a5188918e2a54" + integrity sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg== + +"@esbuild/linux-loong64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz#d5ad459d41ed42bbd4d005256b31882ec52227d8" + integrity sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ== + +"@esbuild/linux-mips64el@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz#4e5967a665c38360b0a8205594377d4dcf9c3726" + integrity sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw== + +"@esbuild/linux-ppc64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz#206443a02eb568f9fdf0b438fbd47d26e735afc8" + integrity sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g== + +"@esbuild/linux-riscv64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz#c351e433d009bf256e798ad048152c8d76da2fc9" + integrity sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw== + +"@esbuild/linux-s390x@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz#661f271e5d59615b84b6801d1c2123ad13d9bd87" + integrity sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w== + +"@esbuild/linux-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz#e4ba18e8b149a89c982351443a377c723762b85f" + integrity sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw== + +"@esbuild/netbsd-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz#7d4f4041e30c5c07dd24ffa295c73f06038ec775" + integrity sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA== + +"@esbuild/openbsd-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz#970fa7f8470681f3e6b1db0cc421a4af8060ec35" + integrity sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg== + +"@esbuild/sunos-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz#abc60e7c4abf8b89fb7a4fe69a1484132238022c" + integrity sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw== + +"@esbuild/win32-arm64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz#7b0ff9e8c3265537a7a7b1fd9a24e7bd39fcd87a" + integrity sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw== + +"@esbuild/win32-ia32@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz#e90fe5267d71a7b7567afdc403dfd198c292eb09" + integrity sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig== + +"@esbuild/win32-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz#c5a1a4bfe1b57f0c3e61b29883525c6da3e5c091" + integrity sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q== + +"@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.1.1" - espree "^7.3.0" - globals "^13.9.0" - ignore "^4.0.6" + debug "^4.3.2" + espree "^9.4.0" + globals "^13.19.0" + ignore "^5.2.0" import-fresh "^3.2.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" + 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.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.4.0.tgz" + 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.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.5.0.tgz" + 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/ecma402-abstract@1.6.3": - version "1.6.3" - resolved "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.6.3.tgz" - integrity sha512-7ijswObmYXabVy5GvcpKG29jbyJ9rGtFdRBdmdQvoDmMo0PwlOl/L08GtrjA4YWLAZ0j2owb2YrRLGNAvLBk+Q== +"@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.1.0" + tslib "^2.4.0" -"@formatjs/intl-displaynames@4.0.11": - version "4.0.11" - resolved "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-4.0.11.tgz" - integrity sha512-e3917+HmXStxb2fNP3sOr3R1DMALdWrUteBb3nerA2AKa12mXwmL0lDavrdltwZWqF7/Egh8fF/esB0Z/fqOgQ== +"@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.6.3" - tslib "^2.1.0" + "@formatjs/ecma402-abstract" "1.14.3" + "@formatjs/icu-skeleton-parser" "1.3.18" + tslib "^2.4.0" -"@formatjs/intl-getcanonicallocales@1.5.7", "@formatjs/intl-getcanonicallocales@^1.5.3": - version "1.5.7" - resolved "https://registry.npmjs.org/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-1.5.7.tgz" - integrity sha512-raPV3Dw7CBC9kPvKdgxkVGgwzYBsQDDG9qXGWblpj/zR+ZJ6Q2V+Co5jZhrviy6lq3qaM2T1Itc0ibvvil1tBw== +"@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: - cldr-core "38" - tslib "^2.1.0" + "@formatjs/ecma402-abstract" "1.14.3" + tslib "^2.4.0" -"@formatjs/intl-listformat@5.0.12": - version "5.0.12" - resolved "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-5.0.12.tgz" - integrity sha512-xWAndG73lqJ1+ar6SljCpM9nUsi2YoZfKi45F2YZRSxtUx4JbWYkhpbroOwxjCQ8ppZFoPc2mlLZjhPZiTyG7g== +"@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.6.3" - tslib "^2.1.0" + "@formatjs/ecma402-abstract" "1.14.3" + "@formatjs/intl-localematcher" "0.2.32" + tslib "^2.4.0" -"@formatjs/intl-locale@^2.4.14": - version "2.4.20" - resolved "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.20.tgz" - integrity sha512-ZrVFxKab+W6jFP6WEYsNW0b7IlGYnCS20fdLN6u0LwPCPYRP5oqHBl0FFVD2+aNnQ1T/21Aol54fCr5LdN/49Q== +"@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: - "@formatjs/ecma402-abstract" "1.6.3" - "@formatjs/intl-getcanonicallocales" "1.5.7" - cldr-core "38" - tslib "^2.1.0" + 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.npmjs.org/@formatjs/intl-numberformat/-/intl-numberformat-5.7.6.tgz" + 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@^6.1.3": - version "6.2.4" - resolved "https://registry.npmjs.org/@formatjs/intl-numberformat/-/intl-numberformat-6.2.4.tgz" - integrity sha512-C82K7GbR06jjJKendLEZKTdTAtgrSkehXS6bX9snOL/nY9BIQYEeFJY/VBFz228jVAFyTsYCiL5toKeyPsfKXA== +"@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.6.3" - tslib "^2.1.0" + "@formatjs/ecma402-abstract" "1.14.3" + "@formatjs/intl-localematcher" "0.2.32" + tslib "^2.4.0" -"@formatjs/intl-pluralrules@^4.0.6": - version "4.0.12" - resolved "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.12.tgz" - integrity sha512-jXXsWGQbBMvuhvxuG1AXBMMNMS1ZphSt/rWsGo6bE3KyWmddJnnVokeUD8E2sTtXoCJZoGUQkOxxjFa/gGLyxw== +"@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.6.3" - tslib "^2.1.0" + "@formatjs/ecma402-abstract" "1.14.3" + "@formatjs/intl-localematcher" "0.2.32" + tslib "^2.4.0" -"@formatjs/intl@1.8.4": - version "1.8.4" - resolved "https://registry.npmjs.org/@formatjs/intl/-/intl-1.8.4.tgz" - integrity sha512-m0/5ZRQZZfzXmeDieoG8kxu3QRvJazv2VbXhROs5khJKfUKu1rz6xfuUrh3gkmydWYtHuwJDIoC9oGR0ik4+/g== +"@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.6.3" - "@formatjs/intl-displaynames" "4.0.11" - "@formatjs/intl-listformat" "5.0.12" - fast-memoize "^2.5.2" - intl-messageformat "9.5.3" - intl-messageformat-parser "6.4.3" - tslib "^2.1.0" + "@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.npmjs.org/@formatjs/ts-transformer/-/ts-transformer-2.13.0.tgz" + 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@^0.2.35": - version "0.2.35" - resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.35.tgz" - integrity sha512-IHUfxSEDS9dDGqYwIW7wTN6tn/O8E0n5PcAHz9cAaBoZw6UpG20IG/YM3NNLaGPwPqgjBAFjIURzqoQs3rrtuw== +"@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@^1.2.34": - version "1.2.35" - resolved "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.35.tgz" - integrity sha512-uLEXifXIL7hnh2sNZQrIJWNol7cTVIzwI+4qcBIq9QWaZqUblm0IDrtSqbNg+3SQf8SMGHkiSigD++rHmCHjBg== +"@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" "^0.2.35" + "@fortawesome/fontawesome-common-types" "6.3.0" -"@fortawesome/free-regular-svg-icons@^5.15.2": - version "5.15.3" - resolved "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.3.tgz" - integrity sha512-q4/p8Xehy9qiVTdDWHL4Z+o5PCLRChePGZRTXkl+/Z7erDVL8VcZUuqzJjs6gUz6czss4VIPBRdCz6wP37/zMQ== +"@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" "^0.2.35" + "@fortawesome/fontawesome-common-types" "6.3.0" -"@fortawesome/free-solid-svg-icons@^5.15.2": - version "5.15.3" - resolved "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.3.tgz" - integrity sha512-XPeeu1IlGYqz4VWGRAT5ukNMd4VHUEEJ7ysZ7pSSgaEtNvSo+FLurybGJVmiqkQdK50OkSja2bfZXOeyMGRD8Q== +"@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" "^0.2.35" + "@fortawesome/fontawesome-common-types" "6.3.0" -"@fortawesome/react-fontawesome@^0.1.14": - version "0.1.14" - resolved "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.14.tgz" - integrity sha512-4wqNb0gRLVaBm/h+lGe8UfPPivcbuJ6ecI4hIgW0LjI7kzpYB9FkN0L9apbVzg+lsBdcTf0AlBtODjcSX5mmKA== +"@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: - prop-types "^15.7.2" + "@fortawesome/fontawesome-common-types" "6.3.0" -"@graphql-codegen/add@^2.0.2": - version "2.0.2" - resolved "https://registry.npmjs.org/@graphql-codegen/add/-/add-2.0.2.tgz" - integrity sha512-0X1ofeSvAjCNcLar2ZR1EOmm5dvyKJMFbgM+ySf1PaHyoi3yf/xRI2Du81ONzQ733Lhmn3KTX1VKybm/OB1Qtg== +"@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: - "@graphql-codegen/plugin-helpers" "^1.18.2" - tslib "~2.0.1" + prop-types "^15.8.1" -"@graphql-codegen/cli@^1.20.0": - version "1.21.3" - resolved "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-1.21.3.tgz" - integrity sha512-jwg0mKhseg0QI4/T4IQcttTBCZgnahiTWqnYWIK+E8nrbXCE9o2hxvaYin/Kq9+5oFtxDePED56cjVs/ESRw6g== +"@graphql-codegen/cli@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@graphql-codegen/cli/-/cli-3.0.0.tgz#eb367adfe51349e4822518183fc7ea7c06aea11b" + integrity sha512-16nuFabHCfPQ/d+v52OvR1ueL8eiJvS/nRuvuEV8d9T1fkborHKRw4lhyKVebu9izFBs6G0CvVCLhgVzQwHSLw== dependencies: - "@graphql-codegen/core" "1.17.9" - "@graphql-codegen/plugin-helpers" "^1.18.4" - "@graphql-tools/apollo-engine-loader" "^6" - "@graphql-tools/code-file-loader" "^6" - "@graphql-tools/git-loader" "^6" - "@graphql-tools/github-loader" "^6" - "@graphql-tools/graphql-file-loader" "^6" - "@graphql-tools/json-file-loader" "^6" - "@graphql-tools/load" "^6" - "@graphql-tools/prisma-loader" "^6" - "@graphql-tools/url-loader" "^6" - "@graphql-tools/utils" "^7.0.0" - ansi-escapes "^4.3.1" + "@babel/generator" "^7.18.13" + "@babel/template" "^7.18.10" + "@babel/types" "^7.18.13" + "@graphql-codegen/core" "^3.0.0" + "@graphql-codegen/plugin-helpers" "^4.0.0" + "@graphql-tools/apollo-engine-loader" "^7.3.6" + "@graphql-tools/code-file-loader" "^7.3.17" + "@graphql-tools/git-loader" "^7.2.13" + "@graphql-tools/github-loader" "^7.3.20" + "@graphql-tools/graphql-file-loader" "^7.5.0" + "@graphql-tools/json-file-loader" "^7.4.1" + "@graphql-tools/load" "^7.8.0" + "@graphql-tools/prisma-loader" "^7.2.49" + "@graphql-tools/url-loader" "^7.13.2" + "@graphql-tools/utils" "^9.0.0" + "@whatwg-node/fetch" "^0.6.0" chalk "^4.1.0" - change-case-all "1.0.12" - chokidar "^3.4.3" - common-tags "^1.8.0" + chokidar "^3.5.2" cosmiconfig "^7.0.0" + cosmiconfig-typescript-loader "^4.3.0" debounce "^1.2.0" - dependency-graph "^0.11.0" detect-indent "^6.0.0" - glob "^7.1.6" - graphql-config "^3.2.0" - indent-string "^4.0.0" - inquirer "^7.3.3" + graphql-config "^4.4.0" + inquirer "^8.0.0" is-glob "^4.0.1" json-to-pretty-yaml "^1.2.2" - latest-version "5.1.0" - listr "^0.14.3" - listr-update-renderer "^0.5.0" + listr2 "^4.0.5" log-symbols "^4.0.0" - minimatch "^3.0.4" - mkdirp "^1.0.4" + shell-quote "^1.7.3" string-env-interpolation "^1.0.1" ts-log "^2.2.3" - tslib "~2.1.0" - valid-url "^1.0.9" - wrap-ansi "^7.0.0" + ts-node "^10.9.1" + tslib "^2.4.0" yaml "^1.10.0" - yargs "^16.1.1" + yargs "^17.0.0" -"@graphql-codegen/core@1.17.9": - version "1.17.9" - resolved "https://registry.npmjs.org/@graphql-codegen/core/-/core-1.17.9.tgz" - integrity sha512-7nwy+bMWqb0iYJ2DKxA9UiE16meeJ2Ch2XWS/N/ZnA0snTR+GZ20USI8z6YqP1Fuist7LvGO1MbitO2qBT8raA== +"@graphql-codegen/core@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@graphql-codegen/core/-/core-3.0.0.tgz#ff6d643bb9d80c54ddd9ee5c68194888c971ae45" + integrity sha512-WUfAUTmUcgeHPR7F5ZQqaBqJLJb5+3Lvp6v9SrnupKOFed+Q3u8CvZL6sPTvDpqqW8Ucjy59DEZqumPLp99pdQ== dependencies: - "@graphql-codegen/plugin-helpers" "^1.18.2" - "@graphql-tools/merge" "^6" - "@graphql-tools/utils" "^6" - tslib "~2.0.1" + "@graphql-codegen/plugin-helpers" "^4.0.0" + "@graphql-tools/schema" "^9.0.0" + "@graphql-tools/utils" "^9.1.1" + tslib "~2.4.0" -"@graphql-codegen/plugin-helpers@^1.18.2", "@graphql-codegen/plugin-helpers@^1.18.3", "@graphql-codegen/plugin-helpers@^1.18.4": - version "1.18.4" - resolved "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-1.18.4.tgz" - integrity sha512-dpfhUmn9GOS8ByoOPIN3V4Nn9HX7sl9NR7Hf26TgN6Clg7cQvkT6XjHdS2e56Q3kWrxZT1zJ1sEa67D3tj9ZtQ== +"@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" "^7.0.0" - common-tags "1.8.0" - import-from "3.0.0" - lodash "~4.17.20" - tslib "~2.1.0" + "@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/time@^2.0.2": - version "2.0.2" - resolved "https://registry.npmjs.org/@graphql-codegen/time/-/time-2.0.2.tgz" - integrity sha512-r+mFtkkRX/QaZMmSGJC0Gj8+z97oSfKSeMt7kQhcG4gzZBEmXc3gY540sjI0B4RjlBtxoeWundgsE6VtM1HsRQ== +"@graphql-codegen/plugin-helpers@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@graphql-codegen/plugin-helpers/-/plugin-helpers-4.0.0.tgz#9c10e4700dc6efe657781dff47347d0c99674061" + integrity sha512-vgNGTanT36hC4RAC/LAThMEjDvnu3WCyx6MtKZcPUtfCWFxbUAr88+OarGl1LAEiOef0agIREC7tIBXCqjKkJA== dependencies: - "@graphql-codegen/plugin-helpers" "^1.18.2" + "@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/schema-ast@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@graphql-codegen/schema-ast/-/schema-ast-3.0.0.tgz#3311a58184f885853d33d8272c527ff5bdf3023b" + integrity sha512-5gC8nNk/bxufS2LBSpaPExRgn6eNo8LQdtwDtwfM9XGEzt/F6rIBQoyOmqqwkiBmgu1PHHH8kLZMBYvYB1x5DA== + dependencies: + "@graphql-codegen/plugin-helpers" "^4.0.0" + "@graphql-tools/utils" "^9.0.0" + tslib "~2.4.0" + +"@graphql-codegen/time@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@graphql-codegen/time/-/time-4.0.0.tgz#70f225d1902dc83a778770da8166d322b6dc430e" + integrity sha512-2hnkYej1pTOGfL1xCzxuvDKYfE1EPjdOYeZP3iw8v8azWrO4UG0XFX4Zteii4u9YhZlSmp5kWPTHpL6F3lOPmw== + dependencies: + "@graphql-codegen/plugin-helpers" "^4.0.0" moment "~2.29.1" -"@graphql-codegen/typescript-operations@^1.17.13": - version "1.17.15" - resolved "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-1.17.15.tgz" - integrity sha512-HStWj3mUe+0ir2J0jqgjegrvcO1DIe2gzsoBBo9RHIYwyaxedUivxXvWY9XBfKpHv6sLa/ST1iYGeedrJELPtw== +"@graphql-codegen/typescript-operations@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@graphql-codegen/typescript-operations/-/typescript-operations-3.0.0.tgz#e67c9dedb739a20f8a1d2c9a9631701c269de0c1" + integrity sha512-t+Lk+lxkUFDh6F0t8CErowOccP3bZwxhl66qmEeBcOrC7jQrSCnRZoFvOXhFKFBJe/y4DIJiizgSr34AqjiJIQ== dependencies: - "@graphql-codegen/plugin-helpers" "^1.18.3" - "@graphql-codegen/typescript" "^1.21.1" - "@graphql-codegen/visitor-plugin-common" "^1.19.0" + "@graphql-codegen/plugin-helpers" "^4.0.0" + "@graphql-codegen/typescript" "^3.0.0" + "@graphql-codegen/visitor-plugin-common" "3.0.0" auto-bind "~4.0.0" - tslib "~2.1.0" + tslib "~2.4.0" -"@graphql-codegen/typescript-react-apollo@^2.2.1": - version "2.2.3" - resolved "https://registry.npmjs.org/@graphql-codegen/typescript-react-apollo/-/typescript-react-apollo-2.2.3.tgz" - integrity sha512-6OjDJQfVwMDEqQkgoQ3Uixq3KRb0H+lSwcWWFmdWErYhe5k2F8niBUJ32FEwu0KIqqx3PhzdwmLcXpBTC5gtyg== +"@graphql-codegen/typescript-react-apollo@^3.3.7": + version "3.3.7" + resolved "https://registry.yarnpkg.com/@graphql-codegen/typescript-react-apollo/-/typescript-react-apollo-3.3.7.tgz#e856caa22c5f7bc9a546c44f54e5f3bd5801ab67" + integrity sha512-9DUiGE8rcwwEkf/S1kpBT/Py/UUs9Qak14bOnTT1JHWs1MWhiDA7vml+A8opU7YFI1EVbSSaE5jjRv11WHoikQ== dependencies: - "@graphql-codegen/plugin-helpers" "^1.18.4" - "@graphql-codegen/visitor-plugin-common" "^1.19.1" + "@graphql-codegen/plugin-helpers" "^2.7.2" + "@graphql-codegen/visitor-plugin-common" "2.13.1" auto-bind "~4.0.0" - change-case-all "1.0.12" - tslib "~2.1.0" + change-case-all "1.0.14" + tslib "~2.4.0" -"@graphql-codegen/typescript@^1.20.00", "@graphql-codegen/typescript@^1.21.1": - version "1.21.1" - resolved "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-1.21.1.tgz" - integrity sha512-JF6Vsu5HSv3dAoS2ca3PFLUN0qVxotex/+BgWw/6SKhtd83MUPnzJ/RU3lACg4vuNTCWeQSeGvg8x5qrw9Go9w== +"@graphql-codegen/typescript@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@graphql-codegen/typescript/-/typescript-3.0.0.tgz#473dde1646540039bca5db4b6daf174d13af0ce3" + integrity sha512-FQWyuIUy1y+fxb9+EZfvdBHBQpYExlIBHV5sg2WGNCsyVyCqBTl0mO8icyOtsQPVg6YFMFe8JJO69vQbwHma5w== dependencies: - "@graphql-codegen/plugin-helpers" "^1.18.3" - "@graphql-codegen/visitor-plugin-common" "^1.19.0" + "@graphql-codegen/plugin-helpers" "^4.0.0" + "@graphql-codegen/schema-ast" "^3.0.0" + "@graphql-codegen/visitor-plugin-common" "3.0.0" auto-bind "~4.0.0" - tslib "~2.1.0" + tslib "~2.4.0" -"@graphql-codegen/visitor-plugin-common@^1.19.0", "@graphql-codegen/visitor-plugin-common@^1.19.1": - version "1.19.1" - resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-1.19.1.tgz" - integrity sha512-MJZXe5vXxV6PLOgHhQoz93gnjzJtbnVQXQKqVEcbyB9W8ImoKuTHsEf/eJ6yCL79f7X/2dnOOM84d5Osh1eaKg== +"@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" "^1.18.4" - "@graphql-tools/optimize" "^1.0.1" - "@graphql-tools/relay-operation-optimizer" "^6" - array.prototype.flatmap "^1.2.4" + "@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.12" + change-case-all "1.0.14" dependency-graph "^0.11.0" graphql-tag "^2.11.0" parse-filepath "^1.0.2" - tslib "~2.1.0" + tslib "~2.4.0" -"@graphql-tools/apollo-engine-loader@^6": - version "6.2.5" - resolved "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-6.2.5.tgz" - integrity sha512-CE4uef6PyxtSG+7OnLklIr2BZZDgjO89ZXK47EKdY7jQy/BQD/9o+8SxPsgiBc+2NsDJH2I6P/nqoaJMOEat6g== +"@graphql-codegen/visitor-plugin-common@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-3.0.0.tgz#527185eb3b1b06739702084bc6263713e167a166" + integrity sha512-ZoNlCmmkGClB137SpJT9og/nkihLN7Z4Ynl9Ir3OlbDuI20dbpyXsclpr9QGLcxEcfQeVfhGw9CooW7wZJJ8LA== dependencies: - "@graphql-tools/utils" "^7.0.0" - cross-fetch "3.0.6" - tslib "~2.0.1" + "@graphql-codegen/plugin-helpers" "^4.0.0" + "@graphql-tools/optimize" "^1.3.0" + "@graphql-tools/relay-operation-optimizer" "^6.5.0" + "@graphql-tools/utils" "^9.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.4.0" -"@graphql-tools/batch-execute@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-7.0.0.tgz" - integrity sha512-+ywPfK6N2Ddna6oOa5Qb1Mv7EA8LOwRNOAPP9dL37FEhksJM9pYqPSceUcqMqg7S9b0+Cgr78s408rgvurV3/Q== +"@graphql-tools/apollo-engine-loader@^7.3.6": + version "7.3.26" + resolved "https://registry.yarnpkg.com/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-7.3.26.tgz#91e54460d5579933e42a2010b8688c3459c245d8" + integrity sha512-h1vfhdJFjnCYn9b5EY1Z91JTF0KB3hHVJNQIsiUV2mpQXZdeOXQoaWeYEKaiI5R6kwBw5PP9B0fv3jfUIG8LyQ== dependencies: - "@graphql-tools/utils" "^7.0.0" - dataloader "2.0.0" - is-promise "4.0.0" - tslib "~2.0.1" + "@ardatan/sync-fetch" "^0.0.1" + "@graphql-tools/utils" "^9.2.1" + "@whatwg-node/fetch" "^0.8.0" + tslib "^2.4.0" -"@graphql-tools/code-file-loader@^6": - version "6.3.1" - resolved "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-6.3.1.tgz" - integrity sha512-ZJimcm2ig+avgsEOWWVvAaxZrXXhiiSZyYYOJi0hk9wh5BxZcLUNKkTp6EFnZE/jmGUwuos3pIjUD3Hwi3Bwhg== +"@graphql-tools/batch-execute@8.5.18": + version "8.5.18" + resolved "https://registry.yarnpkg.com/@graphql-tools/batch-execute/-/batch-execute-8.5.18.tgz#2f0e91cc12e8eed32f14bc814f27c6a498b75e17" + integrity sha512-mNv5bpZMLLwhkmPA6+RP81A6u3KF4CSKLf3VX9hbomOkQR4db8pNs8BOvpZU54wKsUzMzdlws/2g/Dabyb2Vsg== dependencies: - "@graphql-tools/graphql-tag-pluck" "^6.5.1" - "@graphql-tools/utils" "^7.0.0" - tslib "~2.1.0" + "@graphql-tools/utils" "9.2.1" + dataloader "2.2.2" + tslib "^2.4.0" + value-or-promise "1.0.12" -"@graphql-tools/delegate@^7.0.1", "@graphql-tools/delegate@^7.0.7": - version "7.0.10" - resolved "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-7.0.10.tgz" - integrity sha512-6Di9ia5ohoDvrHuhj2cak1nJGhIefJmUsd3WKZcJ2nu2yZAFawWMxGvQImqv3N7iyaWKiVhrrK8Roi/JrYhdKg== +"@graphql-tools/code-file-loader@^7.3.17": + version "7.3.21" + resolved "https://registry.yarnpkg.com/@graphql-tools/code-file-loader/-/code-file-loader-7.3.21.tgz#3eed4ff4610cf0a6f4b1be17d0bce1eec9359479" + integrity sha512-dj+OLnz1b8SYkXcuiy0CUQ25DWnOEyandDlOcdBqU3WVwh5EEVbn0oXUYm90fDlq2/uut00OrtC5Wpyhi3tAvA== dependencies: - "@ardatan/aggregate-error" "0.0.6" - "@graphql-tools/batch-execute" "^7.0.0" - "@graphql-tools/schema" "^7.0.0" - "@graphql-tools/utils" "^7.1.6" - dataloader "2.0.0" - is-promise "4.0.0" - tslib "~2.1.0" + "@graphql-tools/graphql-tag-pluck" "7.5.0" + "@graphql-tools/utils" "9.2.1" + globby "^11.0.3" + tslib "^2.4.0" + unixify "^1.0.0" -"@graphql-tools/git-loader@^6": - version "6.2.6" - resolved "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-6.2.6.tgz" - integrity sha512-ooQTt2CaG47vEYPP3CPD+nbA0F+FYQXfzrB1Y1ABN9K3d3O2RK3g8qwslzZaI8VJQthvKwt0A95ZeE4XxteYfw== +"@graphql-tools/delegate@9.0.27", "@graphql-tools/delegate@^9.0.27": + version "9.0.27" + resolved "https://registry.yarnpkg.com/@graphql-tools/delegate/-/delegate-9.0.27.tgz#e500554bace46cc7ededd48a0c28079f747c9f49" + integrity sha512-goYewiPls/RDXiRTl1S2tRPlsyDQCxlDWqd0uEIzQZ6aWSyiutfwQnTzdbZPXK0qOblEVMIqFhSGrB6fp0OkBA== dependencies: - "@graphql-tools/graphql-tag-pluck" "^6.2.6" - "@graphql-tools/utils" "^7.0.0" - tslib "~2.1.0" + "@graphql-tools/batch-execute" "8.5.18" + "@graphql-tools/executor" "0.0.14" + "@graphql-tools/schema" "9.0.16" + "@graphql-tools/utils" "9.2.1" + dataloader "2.2.2" + tslib "~2.5.0" + value-or-promise "1.0.12" -"@graphql-tools/github-loader@^6": - version "6.2.5" - resolved "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-6.2.5.tgz" - integrity sha512-DLuQmYeNNdPo8oWus8EePxWCfCAyUXPZ/p1PWqjrX/NGPyH2ZObdqtDAfRHztljt0F/qkBHbGHCEk2TKbRZTRw== +"@graphql-tools/executor-graphql-ws@^0.0.11": + version "0.0.11" + resolved "https://registry.yarnpkg.com/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-0.0.11.tgz#c6536aa862f76a9c7ac83e7e07fe8d5119e6de38" + integrity sha512-muRj6j897ks2iKqe3HchWFFzd+jFInSRuLPvHJ7e4WPrejFvaZx3BQ9gndfJvVkfYUZIFm13stCGXaJJTbVM0Q== dependencies: - "@graphql-tools/graphql-tag-pluck" "^6.2.6" - "@graphql-tools/utils" "^7.0.0" - cross-fetch "3.0.6" - tslib "~2.0.1" + "@graphql-tools/utils" "9.2.1" + "@repeaterjs/repeater" "3.0.4" + "@types/ws" "^8.0.0" + graphql-ws "5.11.3" + isomorphic-ws "5.0.0" + tslib "^2.4.0" + ws "8.12.1" -"@graphql-tools/graphql-file-loader@^6", "@graphql-tools/graphql-file-loader@^6.0.0": - version "6.2.7" - resolved "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-6.2.7.tgz" - integrity sha512-5k2SNz0W87tDcymhEMZMkd6/vs6QawDyjQXWtqkuLTBF3vxjxPD1I4dwHoxgWPIjjANhXybvulD7E+St/7s9TQ== +"@graphql-tools/executor-http@^0.1.7": + version "0.1.9" + resolved "https://registry.yarnpkg.com/@graphql-tools/executor-http/-/executor-http-0.1.9.tgz#ddd74ef376b4a2ed59c622acbcca068890854a30" + integrity sha512-tNzMt5qc1ptlHKfpSv9wVBVKCZ7gks6Yb/JcYJluxZIT4qRV+TtOFjpptfBU63usgrGVOVcGjzWc/mt7KhmmpQ== dependencies: - "@graphql-tools/import" "^6.2.6" - "@graphql-tools/utils" "^7.0.0" - tslib "~2.1.0" + "@graphql-tools/utils" "^9.2.1" + "@repeaterjs/repeater" "^3.0.4" + "@whatwg-node/fetch" "^0.8.1" + dset "^3.1.2" + extract-files "^11.0.0" + meros "^1.2.1" + tslib "^2.4.0" + value-or-promise "^1.0.12" -"@graphql-tools/graphql-tag-pluck@^6.2.6", "@graphql-tools/graphql-tag-pluck@^6.5.1": - version "6.5.1" - resolved "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-6.5.1.tgz" - integrity sha512-7qkm82iFmcpb8M6/yRgzjShtW6Qu2OlCSZp8uatA3J0eMl87TxyJoUmL3M3UMMOSundAK8GmoyNVFUrueueV5Q== +"@graphql-tools/executor-legacy-ws@^0.0.9": + version "0.0.9" + resolved "https://registry.yarnpkg.com/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-0.0.9.tgz#1ff517998f750af2be9c1dae8924665a136e4986" + integrity sha512-L7oDv7R5yoXzMH+KLKDB2WHVijfVW4dB2H+Ae1RdW3MFvwbYjhnIB6QzHqKEqksjp/FndtxZkbuTIuAOsYGTYw== dependencies: - "@babel/parser" "7.12.16" - "@babel/traverse" "7.12.13" - "@babel/types" "7.12.13" - "@graphql-tools/utils" "^7.0.0" - tslib "~2.1.0" + "@graphql-tools/utils" "9.2.1" + "@types/ws" "^8.0.0" + isomorphic-ws "5.0.0" + tslib "^2.4.0" + ws "8.12.1" -"@graphql-tools/import@^6.2.6": - version "6.3.0" - resolved "https://registry.npmjs.org/@graphql-tools/import/-/import-6.3.0.tgz" - integrity sha512-zmaVhJ3UPjzJSb005Pjn2iWvH+9AYRXI4IUiTi14uPupiXppJP3s7S25Si3+DbHpFwurDF2nWRxBLiFPWudCqw== +"@graphql-tools/executor@0.0.14": + version "0.0.14" + resolved "https://registry.yarnpkg.com/@graphql-tools/executor/-/executor-0.0.14.tgz#7c6073d75c77dd6e7fab0c835761ed09c85a3bc6" + integrity sha512-YiBbN9NT0FgqPJ35+Eg0ty1s5scOZTgiPf+6hLVJBd5zHEURwojEMCTKJ9e0RNZHETp2lN+YaTFGTSoRk0t4Sw== dependencies: + "@graphql-tools/utils" "9.2.1" + "@graphql-typed-document-node/core" "3.1.1" + "@repeaterjs/repeater" "3.0.4" + tslib "^2.4.0" + value-or-promise "1.0.12" + +"@graphql-tools/git-loader@^7.2.13": + version "7.2.20" + resolved "https://registry.yarnpkg.com/@graphql-tools/git-loader/-/git-loader-7.2.20.tgz#b17917c89be961c272bfbf205dcf32287247494b" + integrity sha512-D/3uwTzlXxG50HI8BEixqirT4xiUp6AesTdfotRXAs2d4CT9wC6yuIWOHkSBqgI1cwKWZb6KXZr467YPS5ob1w== + dependencies: + "@graphql-tools/graphql-tag-pluck" "7.5.0" + "@graphql-tools/utils" "9.2.1" + is-glob "4.0.3" + micromatch "^4.0.4" + tslib "^2.4.0" + unixify "^1.0.0" + +"@graphql-tools/github-loader@^7.3.20": + version "7.3.27" + resolved "https://registry.yarnpkg.com/@graphql-tools/github-loader/-/github-loader-7.3.27.tgz#77a2fbaeb7bf5f8edc4a865252ecb527a5399e01" + integrity sha512-fFFC35qenyhjb8pfcYXKknAt0CXP5CkQYtLfJXgTXSgBjIsfAVMrqxQ/Y0ejeM19XNF/C3VWJ7rE308yOX6ywA== + dependencies: + "@ardatan/sync-fetch" "^0.0.1" + "@graphql-tools/graphql-tag-pluck" "^7.4.6" + "@graphql-tools/utils" "^9.2.1" + "@whatwg-node/fetch" "^0.8.0" + tslib "^2.4.0" + +"@graphql-tools/graphql-file-loader@^7.3.7", "@graphql-tools/graphql-file-loader@^7.5.0": + version "7.5.16" + resolved "https://registry.yarnpkg.com/@graphql-tools/graphql-file-loader/-/graphql-file-loader-7.5.16.tgz#d954b25ee14c6421ddcef43f4320a82e9800cb23" + integrity sha512-lK1N3Y2I634FS12nd4bu7oAJbai3bUc28yeX+boT+C83KTO4ujGHm+6hPC8X/FRGwhKOnZBxUM7I5nvb3HiUxw== + dependencies: + "@graphql-tools/import" "6.7.17" + "@graphql-tools/utils" "9.2.1" + globby "^11.0.3" + tslib "^2.4.0" + unixify "^1.0.0" + +"@graphql-tools/graphql-tag-pluck@7.5.0", "@graphql-tools/graphql-tag-pluck@^7.4.6": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-7.5.0.tgz#be99bc6b5e8331a2379ab4585d71b057eb981497" + integrity sha512-76SYzhSlH50ZWkhWH6OI94qrxa8Ww1ZeOU04MdtpSeQZVT2rjGWeTb3xM3kjTVWQJsr/YJBhDeNPGlwNUWfX4Q== + dependencies: + "@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" "9.2.1" + tslib "^2.4.0" + +"@graphql-tools/import@6.7.17": + version "6.7.17" + resolved "https://registry.yarnpkg.com/@graphql-tools/import/-/import-6.7.17.tgz#ab51ed08bcbf757f952abf3f40793ce3db42d4a3" + integrity sha512-bn9SgrECXq3WIasgNP7ful/uON51wBajPXtxdY+z/ce7jLWaFE6lzwTDB/GAgiZ+jo7nb0ravlxteSAz2qZmuA== + dependencies: + "@graphql-tools/utils" "9.2.1" resolve-from "5.0.0" - tslib "~2.1.0" + tslib "^2.4.0" -"@graphql-tools/json-file-loader@^6", "@graphql-tools/json-file-loader@^6.0.0": - version "6.2.6" - resolved "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-6.2.6.tgz" - integrity sha512-CnfwBSY5926zyb6fkDBHnlTblHnHI4hoBALFYXnrg0Ev4yWU8B04DZl/pBRUc459VNgO2x8/mxGIZj2hPJG1EA== +"@graphql-tools/json-file-loader@^7.3.7", "@graphql-tools/json-file-loader@^7.4.1": + version "7.4.17" + resolved "https://registry.yarnpkg.com/@graphql-tools/json-file-loader/-/json-file-loader-7.4.17.tgz#3f08e74ab1a3534c02dc97875acc7f15aa460011" + integrity sha512-KOSTP43nwjPfXgas90rLHAFgbcSep4nmiYyR9xRVz4ZAmw8VYHcKhOLTSGylCAzi7KUfyBXajoW+6Z7dQwdn3g== dependencies: - "@graphql-tools/utils" "^7.0.0" - tslib "~2.0.1" + "@graphql-tools/utils" "9.2.1" + globby "^11.0.3" + tslib "^2.4.0" + unixify "^1.0.0" -"@graphql-tools/load@^6", "@graphql-tools/load@^6.0.0": - version "6.2.7" - resolved "https://registry.npmjs.org/@graphql-tools/load/-/load-6.2.7.tgz" - integrity sha512-b1qWjki1y/QvGtoqW3x8bcwget7xmMfLGsvGFWOB6m38tDbzVT3GlJViAC0nGPDks9OCoJzAdi5IYEkBaqH5GQ== +"@graphql-tools/load@^7.5.5", "@graphql-tools/load@^7.8.0": + version "7.8.12" + resolved "https://registry.yarnpkg.com/@graphql-tools/load/-/load-7.8.12.tgz#6457fe6ec8cd2e2b5ca0d2752464bc937d186cca" + integrity sha512-JwxgNS2c6i6oIdKttcbXns/lpKiyN7c6/MkkrJ9x2QE9rXk5HOhSJxRvPmOueCuAin1542xUrcDRGBXJ7thSig== dependencies: - "@graphql-tools/merge" "^6.2.9" - "@graphql-tools/utils" "^7.5.0" - globby "11.0.2" - import-from "3.0.0" - is-glob "4.0.1" + "@graphql-tools/schema" "9.0.16" + "@graphql-tools/utils" "9.2.1" p-limit "3.1.0" - tslib "~2.1.0" - unixify "1.0.0" - valid-url "1.0.9" + tslib "^2.4.0" -"@graphql-tools/merge@^6", "@graphql-tools/merge@^6.0.0", "@graphql-tools/merge@^6.2.9": - version "6.2.10" - resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.2.10.tgz" - integrity sha512-dM3n37PcslvhOAkCz7Cwk0BfoiSVKXGmCX+VMZkATbXk/0vlxUfNEpVfA5yF4IkP27F04SzFQSaNrbD0W2Rszw== +"@graphql-tools/merge@8.3.18", "@graphql-tools/merge@^8.2.6": + version "8.3.18" + resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-8.3.18.tgz#bfbb517c68598a885809f16ce5c3bb1ebb8f04a2" + integrity sha512-R8nBglvRWPAyLpZL/f3lxsY7wjnAeE0l056zHhcO/CgpvK76KYUt9oEkR05i8Hmt8DLRycBN0FiotJ0yDQWTVA== dependencies: - "@graphql-tools/schema" "^7.0.0" - "@graphql-tools/utils" "^7.5.0" - tslib "~2.1.0" + "@graphql-tools/utils" "9.2.1" + tslib "^2.4.0" -"@graphql-tools/optimize@^1.0.1": - version "1.0.1" - resolved "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-1.0.1.tgz" - integrity sha512-cRlUNsbErYoBtzzS6zXahXeTBZGPVlPHXCpnEZ0XiK/KY/sQL96cyzak0fM/Gk6qEI9/l32MYEICjasiBQrl5w== +"@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.0.1" + tslib "^2.4.0" -"@graphql-tools/prisma-loader@^6": - version "6.3.0" - resolved "https://registry.npmjs.org/@graphql-tools/prisma-loader/-/prisma-loader-6.3.0.tgz" - integrity sha512-9V3W/kzsFBmUQqOsd96V4a4k7Didz66yh/IK89B1/rrvy9rYj+ULjEqR73x9BYZ+ww9FV8yP8LasWAJwWaqqJQ== +"@graphql-tools/prisma-loader@^7.2.49": + version "7.2.64" + resolved "https://registry.yarnpkg.com/@graphql-tools/prisma-loader/-/prisma-loader-7.2.64.tgz#e9fc85054b15a22a16c8e69ad4f9543da30c0164" + integrity sha512-W8GfzfBKiBSIEgw+/nJk6zUlF6k/jterlNoFhM27mBsbeMtWxKnm1+gEU6KA0N1PNEdq2RIa2W4AfVfVBl2GgQ== dependencies: - "@graphql-tools/url-loader" "^6.8.2" - "@graphql-tools/utils" "^7.0.0" - "@types/http-proxy-agent" "^2.0.2" + "@graphql-tools/url-loader" "7.17.13" + "@graphql-tools/utils" "9.2.1" "@types/js-yaml" "^4.0.0" "@types/json-stable-stringify" "^1.0.32" - "@types/jsonwebtoken" "^8.5.0" + "@types/jsonwebtoken" "^9.0.0" chalk "^4.1.0" debug "^4.3.1" - dotenv "^8.2.0" - graphql-request "^3.3.0" - http-proxy-agent "^4.0.1" + dotenv "^16.0.0" + graphql-request "^5.0.0" + http-proxy-agent "^5.0.0" https-proxy-agent "^5.0.0" isomorphic-fetch "^3.0.0" js-yaml "^4.0.0" json-stable-stringify "^1.0.1" - jsonwebtoken "^8.5.1" + jsonwebtoken "^9.0.0" lodash "^4.17.20" - replaceall "^0.1.6" scuid "^1.1.0" - tslib "~2.1.0" + tslib "^2.4.0" yaml-ast-parser "^0.0.43" -"@graphql-tools/relay-operation-optimizer@^6": - version "6.3.0" - resolved "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-6.3.0.tgz" - integrity sha512-Or3UgRvkY9Fq1AAx7q38oPqFmTepLz7kp6wDHKyR0ceG7AvHv5En22R12mAeISInbhff4Rpwgf6cE8zHRu6bCw== +"@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: - "@graphql-tools/utils" "^7.1.0" - relay-compiler "10.1.0" - tslib "~2.0.1" + "@ardatan/relay-compiler" "12.0.0" + "@graphql-tools/utils" "9.2.1" + tslib "^2.4.0" -"@graphql-tools/schema@^7.0.0", "@graphql-tools/schema@^7.1.2": - version "7.1.3" - resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-7.1.3.tgz" - integrity sha512-ZY76hmcJlF1iyg3Im0sQ3ASRkiShjgv102vLTVcH22lEGJeCaCyyS/GF1eUHom418S60bS8Th6+autRUxfBiBg== +"@graphql-tools/schema@9.0.16", "@graphql-tools/schema@^9.0.0": + version "9.0.16" + resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-9.0.16.tgz#7d340d69e6094dc01a2b9e625c7bb4fff89ea521" + integrity sha512-kF+tbYPPf/6K2aHG3e1SWIbapDLQaqnIHVRG6ow3onkFoowwtKszvUyOASL6Krcv2x9bIMvd1UkvRf9OaoROQQ== dependencies: - "@graphql-tools/utils" "^7.1.2" - tslib "~2.1.0" + "@graphql-tools/merge" "8.3.18" + "@graphql-tools/utils" "9.2.1" + tslib "^2.4.0" + value-or-promise "1.0.12" -"@graphql-tools/url-loader@^6", "@graphql-tools/url-loader@^6.0.0", "@graphql-tools/url-loader@^6.8.2": - version "6.8.2" - resolved "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-6.8.2.tgz" - integrity sha512-YzsXSCOwlSj8UqOMhQThPzgEChgS/MonyWV7f0WKmN9gAT/f3fPaUcYhVamsH0vGbvTkfNM4JdoZO/39amRs5Q== +"@graphql-tools/url-loader@7.17.13", "@graphql-tools/url-loader@^7.13.2", "@graphql-tools/url-loader@^7.9.7": + version "7.17.13" + resolved "https://registry.yarnpkg.com/@graphql-tools/url-loader/-/url-loader-7.17.13.tgz#d4ee8193792ab1c42db2fbdf5f6ca75fa819ac40" + integrity sha512-FEmbvw68kxeZLn4VYGAl+NuBPk09ZnxymjW07A6mCtiDayFgYfHdWeRzXn/iM5PzsEuCD73R1sExtNQ/ISiajg== dependencies: - "@graphql-tools/delegate" "^7.0.1" - "@graphql-tools/utils" "^7.1.5" - "@graphql-tools/wrap" "^7.0.4" - "@types/websocket" "1.0.2" - cross-fetch "3.1.1" - eventsource "1.1.0" - extract-files "9.0.0" - form-data "4.0.0" - graphql-upload "^11.0.0" - graphql-ws "4.2.2" - is-promise "4.0.0" - isomorphic-ws "4.0.1" - sse-z "0.3.0" - sync-fetch "0.3.0" - tslib "~2.1.0" - valid-url "1.0.9" - ws "7.4.4" + "@ardatan/sync-fetch" "^0.0.1" + "@graphql-tools/delegate" "^9.0.27" + "@graphql-tools/executor-graphql-ws" "^0.0.11" + "@graphql-tools/executor-http" "^0.1.7" + "@graphql-tools/executor-legacy-ws" "^0.0.9" + "@graphql-tools/utils" "^9.2.1" + "@graphql-tools/wrap" "^9.3.6" + "@types/ws" "^8.0.0" + "@whatwg-node/fetch" "^0.8.0" + isomorphic-ws "^5.0.0" + tslib "^2.4.0" + value-or-promise "^1.0.11" + ws "^8.12.0" -"@graphql-tools/utils@^6", "@graphql-tools/utils@^6.0.0": - version "6.2.4" - resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.2.4.tgz" - integrity sha512-ybgZ9EIJE3JMOtTrTd2VcIpTXtDrn2q6eiYkeYMKRVh3K41+LZa6YnR2zKERTXqTWqhobROwLt4BZbw2O3Aeeg== +"@graphql-tools/utils@9.2.1", "@graphql-tools/utils@^9.0.0", "@graphql-tools/utils@^9.1.1", "@graphql-tools/utils@^9.2.1": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-9.2.1.tgz#1b3df0ef166cfa3eae706e3518b17d5922721c57" + integrity sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A== dependencies: - "@ardatan/aggregate-error" "0.0.6" - camel-case "4.1.1" - tslib "~2.0.1" + "@graphql-typed-document-node/core" "^3.1.1" + tslib "^2.4.0" -"@graphql-tools/utils@^7.0.0", "@graphql-tools/utils@^7.1.0", "@graphql-tools/utils@^7.1.2", "@graphql-tools/utils@^7.1.5", "@graphql-tools/utils@^7.1.6", "@graphql-tools/utils@^7.2.1", "@graphql-tools/utils@^7.5.0": - version "7.6.0" - resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-7.6.0.tgz" - integrity sha512-YCZDDdhfb4Yhie0IH031eGdvQG8C73apDuNg6lqBNbauNw45OG/b8wi3+vuMiDnJTJN32GQUb1Gt9gxDKoRDKw== +"@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: - "@ardatan/aggregate-error" "0.0.6" - camel-case "4.1.2" - tslib "~2.1.0" + tslib "^2.4.0" -"@graphql-tools/wrap@^7.0.4": - version "7.0.5" - resolved "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-7.0.5.tgz" - integrity sha512-KCWBXsDfvG46GNUawRltJL4j9BMGoOG7oo3WEyCQP+SByWXiTe5cBF45SLDVQgdjljGNZhZ4Lq/7avIkF7/zDQ== +"@graphql-tools/wrap@^9.3.6": + version "9.3.6" + resolved "https://registry.yarnpkg.com/@graphql-tools/wrap/-/wrap-9.3.6.tgz#23beaf9c3713160adda511c6a498d1c7077c2848" + integrity sha512-HtQIYoPz48bzpMYZzoeMmzIIYuVxcaUuLD7dH7GtIhwe2f4hpPDE+JLUPxpYiaXdY10l7kP9wycK+FtRfCsFlw== dependencies: - "@graphql-tools/delegate" "^7.0.7" - "@graphql-tools/schema" "^7.1.2" - "@graphql-tools/utils" "^7.2.1" - is-promise "4.0.0" - tslib "~2.0.1" + "@graphql-tools/delegate" "9.0.27" + "@graphql-tools/schema" "9.0.16" + "@graphql-tools/utils" "9.2.1" + tslib "^2.4.0" + value-or-promise "1.0.12" -"@graphql-typed-document-node/core@^3.0.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.0.tgz" - integrity sha512-wYn6r8zVZyQJ6rQaALBEln5B1pzxb9shV5Ef97kTvn6yVGrqyXVnDqnU24MXnFubR+rZjBY9NWuxX3FB2sTsjg== +"@graphql-typed-document-node/core@3.1.1", "@graphql-typed-document-node/core@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.1.tgz#076d78ce99822258cf813ecc1e7fa460fa74d052" + integrity sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg== -"@humanwhocodes/config-array@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" - integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== +"@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.0" + "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" - minimatch "^3.0.4" + minimatch "^3.0.5" -"@humanwhocodes/object-schema@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" - integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== +"@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== -"@iarna/toml@^2.2.5": - version "2.2.5" - resolved "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz" - integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg== +"@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== -"@nodelib/fs.scandir@2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" - integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== dependencies: - "@nodelib/fs.stat" "2.0.4" + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@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.0", "@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" + +"@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.4", "@nodelib/fs.stat@^2.0.2": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" - integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== +"@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": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" - integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== +"@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.4" + "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@popperjs/core@^2.5.3": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.1.tgz#7f554e7368c9ab679a11f4a042ca17149d70cf12" - integrity sha512-DvJbbn3dUgMxDnJLH+RZQPnXak1h4ZVYQ7CWiFWjQwBFkVajT4rfw2PdpHLTSTwxrYfnoEXkuBiwkDm6tPMQeA== +"@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== + +"@repeaterjs/repeater@3.0.4", "@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.3.21", "@restart/hooks@^0.3.25": - version "0.3.26" - resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.3.26.tgz#ade155a7b0b014ef1073391dda46972c3a14a129" - integrity sha512-7Hwk2ZMYm+JLWcb7R9qIXk1OoUg1Z+saKWqZXlrvFwT3w6UArVNWgxYOzf+PJoK9zZejp8okPAKTctthhXLt5g== +"@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: - lodash "^4.17.20" - lodash-es "^4.17.20" + dequal "^2.0.2" -"@samverschueren/stream-to-observable@^0.3.0": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301" - integrity sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ== +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + +"@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== + +"@types/apollo-upload-client@^17.0.2": + version "17.0.2" + resolved "https://registry.yarnpkg.com/@types/apollo-upload-client/-/apollo-upload-client-17.0.2.tgz#15dc737663928be27c768117603dfc23c21514bb" + integrity sha512-NphAiBqzZv3iY8Cq+qWyi0QUFFzJ+nVd7QKI/iKV8RfILrpYDL69F/vlhjn4BNxKlmc3LxJHymcf3gFzLBwuZQ== dependencies: - any-observable "^0.3.0" - -"@sindresorhus/is@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" - integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== - -"@stylelint/postcss-css-in-js@^0.37.2": - version "0.37.2" - resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz#7e5a84ad181f4234a2480803422a47b8749af3d2" - integrity sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA== - dependencies: - "@babel/core" ">=7.9.0" - -"@stylelint/postcss-markdown@^0.36.2": - version "0.36.2" - resolved "https://registry.yarnpkg.com/@stylelint/postcss-markdown/-/postcss-markdown-0.36.2.tgz#0a540c4692f8dcdfc13c8e352c17e7bfee2bb391" - integrity sha512-2kGbqUVJUGE8dM+bMzXG/PYUWKkjLIkRLWNh39OaADkiabDRdw8ATFCgbMz5xdIcvwspPAluSL7uY+ZiTWdWmQ== - dependencies: - remark "^13.0.0" - unist-util-find-all-after "^3.0.2" - -"@szmarczak/http-timer@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" - integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== - dependencies: - defer-to-connect "^1.0.1" - -"@tootallnate/once@1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" - integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== - -"@types/apollo-upload-client@^14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@types/apollo-upload-client/-/apollo-upload-client-14.1.0.tgz#21a57d7e3f29ff946ba51a53b3d7da46ddd21fbc" - integrity sha512-ZLvcEqu+l9qKGdrIpASt/A2WY1ghAC9L3qaoegkiBOccjxvQmWN9liZzVFiuHTuWseWpVbMklqbs/z+KEjll9Q== - dependencies: - "@apollo/client" "^3.1.3" + "@apollo/client" "^3.7.0" "@types/extract-files" "*" - graphql "^15.3.0" + graphql "14 - 16" "@types/babel__core@^7.1.7": - version "7.1.14" - resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz" - integrity sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g== + 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.1.0" - "@babel/types" "^7.0.0" + "@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.2" - resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz" - integrity sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ== + 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.0" - resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.0.tgz" - integrity sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A== + 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.11.1" - resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.1.tgz" - integrity sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw== + 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/classnames@^2.2.10", "@types/classnames@^2.2.11": - version "2.2.11" - resolved "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.11.tgz" - integrity sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw== - "@types/cookie@^0.3.3": version "0.3.3" - resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== -"@types/debug@^4.0.0": - version "4.1.7" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" - integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== - dependencies: - "@types/ms" "*" - "@types/extract-files@*": - version "8.1.0" - resolved "https://registry.npmjs.org/@types/extract-files/-/extract-files-8.1.0.tgz" - integrity sha512-ulxvlFU71yLVV3JxdBgryASAIp+aZQuQOpkhU1SznJlcWz0qsJCWHqdJqP6Lprs3blqGS5FH5GbBkU0977+Wew== + version "8.1.1" + resolved "https://registry.yarnpkg.com/@types/extract-files/-/extract-files-8.1.1.tgz#11b67e795ad2c8b483431e8d4f190db2fd22944b" + integrity sha512-dMJJqBqyhsfJKuK7p7HyyNmki7qj1AlwhUKWx6KrU7i1K2T2SPsUsSUTWFmr/sEM1q8rfR8j5IyUmYrDbrhfjQ== "@types/fs-extra@^9.0.1": - version "9.0.8" - resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.8.tgz" - integrity sha512-bnlTVTwq03Na7DpWxFJ1dvnORob+Otb8xHyUqUWhqvz/Ksg8+JXPlR52oeMSZ37YEOa5PyccbgUNutiQdi13TA== + 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/fslightbox-react@^1.4.0": - version "1.4.0" - resolved "https://registry.npmjs.org/@types/fslightbox-react/-/fslightbox-react-1.4.0.tgz" - integrity sha512-ocIiZqFQ3BWBZB8Bp0fuNma7Eb0aOjkgk/nEUfW0omdRw4ciaVivabfsWldNuR69KwRJrvs6MZQuvVV6JEqlFg== - dependencies: - "@types/react" "*" - -"@types/hast@^2.0.0": - version "2.3.4" - resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc" - integrity sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g== - dependencies: - "@types/unist" "*" - -"@types/history@*": - version "4.7.8" - resolved "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== +"@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.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz" + 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/http-proxy-agent@^2.0.2": - version "2.0.2" - resolved "https://registry.npmjs.org/@types/http-proxy-agent/-/http-proxy-agent-2.0.2.tgz" - integrity sha512-2S6IuBRhqUnH1/AUx9k8KWtY3Esg4eqri946MnxTG5HwehF1S5mqLln8fcyMiuQkY72p2gH3W+rIPqp5li0LyQ== - dependencies: - "@types/node" "*" - "@types/invariant@^2.2.33": - version "2.2.34" - resolved "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.34.tgz" - integrity sha512-lYUtmJ9BqUN688fGY1U1HZoWT1/Jrmgigx2loq4ZcJpICECm/Om3V314BxdzypO0u5PORKGMM6x0OXaljV1YFg== + 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.0" - resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.0.tgz" - integrity sha512-4vlpCM5KPCL5CfGmTbpjwVKbISRYhduEJvvUWsH5EB7QInhEj94XPZ3ts/9FPiLZFqYO0xoW4ZL8z2AabTGgJA== + 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.6": - version "7.0.7" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz" - integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== - -"@types/json-schema@^7.0.7": - version "7.0.9" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" - integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== +"@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.32" - resolved "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.0.32.tgz" - integrity sha512-q9Q6+eUEGwQkv4Sbst3J4PNgDOvpuVuKj79Hl/qnmBMEIPzB5QoFRUtjcgcg2xNUZyYUGXBk5wYIBKHt0A+Mxw== + 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.npmjs.org/@types/json5/-/json5-0.0.29.tgz" - integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/jsonwebtoken@^8.5.0": - version "8.5.1" - resolved "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz" - integrity sha512-rNAPdomlIUX0i0cg2+I+Q1wOUr531zHBQ+cV/28PJ39bSPKjahatZZ2LMuhiguETkCgLVzfruw/ZvNMNkKoSzw== +"@types/jsonwebtoken@^9.0.0": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz#29b1369c4774200d6d6f63135bf3d1ba3ef997a4" + integrity sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw== dependencies: "@types/node" "*" @@ -1397,189 +2298,147 @@ "@types/lodash" "*" "@types/lodash@*": - version "4.14.182" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" - integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== - -"@types/lodash@^4.14.165": - version "4.14.168" - resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz" - integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q== + 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.3" - resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.3.tgz" - integrity sha512-SXPBMnFVQg1s00dlMCc/jCdvPqdE4mXaMMCeRlxLDmTAEoegHT53xKtkDnzDTOcmMHUfcjyf36/YYZ6SxRdnsw== + 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/mdurl@^1.0.0": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9" - integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== - "@types/minimist@^1.2.0": - version "1.2.1" - resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz" - integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== + 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.5": - version "1.6.5" - resolved "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.5.tgz" - integrity sha512-OwVhKFim9Y/MprzCe4I6a59p31pMy8+LrtP6qS7J0kaOxYmW6VVJPBw5NYm+g7nSbgPUz22FvqU1F1hC5YGTfg== +"@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/ms@*": - version "0.7.31" - resolved "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz" - integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== - -"@types/node@*": - version "14.14.35" - resolved "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz" - integrity sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag== - -"@types/node@14.14.22": - version "14.14.22" - resolved "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz" - integrity sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw== +"@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.0" - resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz" - integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== + 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.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz" + 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.3" - resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz" - integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + 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-dom@*": - version "17.0.3" - resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.3.tgz" - integrity sha512-4NnJbCeWE+8YBzupn/YrJxZ8VnjcJq5iR1laqQ1vkpQgBiA7bwk0Rp24fxsdNinzJY2U+HHS4dJJDPdoMjdJ7w== +"@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.10": - version "17.0.10" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.10.tgz#d6972ec018d23cf22b99597f1289343d99ea9d9d" - integrity sha512-8oz3NAUId2z/zQdFI09IMhQPNgIbiP8Lslhv39DIDamr846/0spjZK0vnrMak0iB8EKb9QFTTIdg2Wj2zH5a3g== +"@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" "*" + "@types/react" "^17" -"@types/react-helmet@^6.1.3": - version "6.1.3" - resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.3.tgz#1a58b26a79e464c59d3f9cdd5b7ece485335937b" - integrity sha512-U4onVxaZxAp78KpXsfmyCIhLjsvJJ3goG3CYFOo+xW0cPYAz9oe5cBAUSAcN7l35OTbrFvu9TuE0YkcZMKGr4A== +"@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.npmjs.org/@types/react-router-bootstrap/-/react-router-bootstrap-0.24.5.tgz" + 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@*", "@types/react-router-dom@5.1.7": - version "5.1.7" - resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.7.tgz" - integrity sha512-D5mHD6TbdV/DNHYsnwBTv+y73ei+mMjrkGrla86HthE4/PVvL1J94Bu3qABU+COXzpL23T1EZapVVpwHuBXiUg== +"@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" "*" + "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router" "*" -"@types/react-router-hash-link@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@types/react-router-hash-link/-/react-router-hash-link-1.2.1.tgz" - integrity sha512-jdzPGE8jFGq7fHUpPaKrJvLW1Yhoe5MQCrmgeesC+eSLseMj3cGCTYMDA4BNWG8JQmwO8NTYt/oT3uBZ77pmBA== +"@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.12" - resolved "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.12.tgz" - integrity sha512-0bhXQwHYfMeJlCh7mGhc0VJTRm0Gk+Z8T00aiP4702mDUuLs9SMhnd2DitpjWFjdOecx2UXtICK14H9iMnziGA== + 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" "*" + "@types/history" "^4.7.11" "@types/react" "*" -"@types/react-select@^4.0.8": - version "4.0.8" - resolved "https://registry.npmjs.org/@types/react-select/-/react-select-4.0.8.tgz" - integrity sha512-dOfoJxPq4s4shWmI9mDjhs6w7tXlH4bgQarqp5HulL3jgwzEPGK/DaGah4pdCNrY70mnIvMAN7cAzZbUWomESQ== - dependencies: - "@emotion/serialize" "^1.0.0" - "@types/react" "*" - "@types/react-dom" "*" - "@types/react-transition-group" "*" - -"@types/react-slick@^0.23.8": - version "0.23.8" - resolved "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.8.tgz" - integrity sha512-SfzSg++/3uyftVZaCgHpW+2fnJFsyJEQ/YdsuqfOWQ5lqUYV/gY/UwAnkw4qksCj5jalto/T5rKXJ8zeFldQeA== +"@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-transition-group@*", "@types/react-transition-group@^4.4.0": - version "4.4.1" - resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz" - integrity sha512-vIo69qKKcYoJ8wKCJjwSgCTM+z3chw3g18dkrDfVX665tMH7tmbDxEAnPdey4gTlwZz5QuHGzd+hul0OVZDqqQ== - dependencies: - "@types/react" "*" - -"@types/react@*", "@types/react@>=16.9.11", "@types/react@>=16.9.35": - version "17.0.3" - resolved "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz" - integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@17.0.31": - version "17.0.31" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.31.tgz#fe05ebf91ff3ae35bb6b13f6c1b461db8089dff8" - integrity sha512-MQSR5EL4JZtdWRvqDgz9kXhSDDoy2zMTYyg7UhP+FZ5ttUOocWyxiqFJiI57sUG0BtaEX7WDXYQlkCYkb3X9vQ== +"@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.1" - resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz" - integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== + 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.npmjs.org/@types/schema-utils/-/schema-utils-2.4.0.tgz" + resolved "https://registry.yarnpkg.com/@types/schema-utils/-/schema-utils-2.4.0.tgz#9983012045d541dcee053e685a27c9c87c840fcd" integrity sha512-454hrj5gz/FXcUE20ygfEiN4DxZ1sprUo0V1gqIqkNZ/CzoEzAZEll2uxMsuyz6BYjiQan4Aa65xbTemfzW9hQ== dependencies: schema-utils "*" -"@types/ungap__global-this@^0.3.1": - version "0.3.1" - resolved "https://registry.npmjs.org/@types/ungap__global-this/-/ungap__global-this-0.3.1.tgz" - integrity sha512-+/DsiV4CxXl6ZWefwHZDXSe1Slitz21tom38qPCaG0DYCS1NnDPIQDTKcmQ/tvK/edJUKkmuIDBJbmKDiB0r/g== +"@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/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2": - version "2.0.3" - resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz" - integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== +"@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.49": - version "7.3.49" - resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.49.tgz#33fbc421a02827c90935afbf7dcaac77170b6cda" - integrity sha512-GtBMH+rm7yyw5DAK7ycQeEd35x/EYoLK/49op+CqDDoNUm9XJEVOfb+EARKKe4TwP5jkaikjWqf5RFjmw8yHoQ== +"@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.5.0": version "0.5.0" @@ -1597,107 +2456,111 @@ "@types/warning@^3.0.0": version "3.0.0" - resolved "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz" - integrity sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI= + resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52" + integrity sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA== -"@types/websocket@1.0.2": - version "1.0.2" - resolved "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.2.tgz" - integrity sha512-B5m9aq7cbbD/5/jThEr33nUY8WEfVi6A2YKCTOvw5Ldy7mtsOkqRvGjnzy6g7iMMDsgu7xREuCzqATLDLQVKcQ== +"@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" "*" -"@types/zen-observable@^0.8.0": - version "0.8.2" - resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.2.tgz" - integrity sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg== - -"@typescript-eslint/eslint-plugin@^4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276" - integrity sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg== +"@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/experimental-utils" "4.33.0" - "@typescript-eslint/scope-manager" "4.33.0" - debug "^4.3.1" - functional-red-black-tree "^1.0.1" - ignore "^5.1.8" - regexpp "^3.1.0" - semver "^7.3.5" + "@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/experimental-utils@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz#6f2a786a4209fa2222989e9380b5331b2810f7fd" - integrity sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q== +"@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: - "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.33.0" - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/typescript-estree" "4.33.0" + "@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/parser@^4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.33.0.tgz#dfe797570d9694e560528d18eecad86c8c744899" - integrity sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA== +"@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/scope-manager" "4.33.0" - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/typescript-estree" "4.33.0" - debug "^4.3.1" + "@typescript-eslint/types" "5.52.0" + eslint-visitor-keys "^3.3.0" -"@typescript-eslint/scope-manager@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz#d38e49280d983e8772e29121cf8c6e9221f280a3" - integrity sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ== - dependencies: - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/visitor-keys" "4.33.0" - -"@typescript-eslint/types@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72" - integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ== - -"@typescript-eslint/typescript-estree@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz#0dfb51c2908f68c5c08d82aefeaf166a17c24609" - integrity sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA== - dependencies: - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/visitor-keys" "4.33.0" - debug "^4.3.1" - globby "^11.0.3" - is-glob "^4.0.1" - semver "^7.3.5" - tsutils "^3.21.0" - -"@typescript-eslint/visitor-keys@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd" - integrity sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg== - dependencies: - "@typescript-eslint/types" "4.33.0" - eslint-visitor-keys "^2.0.0" - -"@ungap/global-this@^0.4.2": - version "0.4.4" - resolved "https://registry.npmjs.org/@ungap/global-this/-/global-this-0.4.4.tgz" - integrity sha512-mHkm6FvepJECMNthFuIgpAEFmPOk71UyXuIxYfjytvFTnSDBIz7jmViO+LfHI/AjrazWije0PnSP3+/NlwzqtA== - -"@videojs/http-streaming@2.14.3": - version "2.14.3" - resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-2.14.3.tgz#3277e03b576766decb4fc663e954e18bfa10d2a1" - integrity sha512-2tFwxCaNbcEZzQugWf8EERwNMyNtspfHnvxRGRABQs09W/5SqmkWFuGWfUAm4wQKlXGfdPyAJ1338ASl459xAA== +"@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.7.1" - mpd-parser "0.21.1" + m3u8-parser "4.8.0" + mpd-parser "^0.22.1" mux.js "6.0.1" video.js "^6 || ^7" @@ -1719,41 +2582,117 @@ global "~4.4.0" is-function "^1.0.1" -"@wry/context@^0.5.2": - version "0.5.4" - resolved "https://registry.npmjs.org/@wry/context/-/context-0.5.4.tgz" - integrity sha512-/pktJKHUXDr4D6TJqWgudOPJW2Z+Nb+bqk40jufA3uTkLbnCRKdJPiYDIa/c7mfcPH8Hr6O8zjCERpg5Sq04Zg== +"@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: - tslib "^1.14.1" + "@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" -"@wry/equality@^0.3.0": - version "0.3.4" - resolved "https://registry.npmjs.org/@wry/equality/-/equality-0.3.4.tgz" - integrity sha512-1gQQhCPenzxw/1HzLlvSIs/59eBHJf9ZDIussjjZhqNSqQuPKQIzN6SWt4kemvlBPDi7RqMuUa03pId7MAE93g== +"@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: - tslib "^1.14.1" + "@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" -"@wry/trie@^0.2.1": - version "0.2.2" - resolved "https://registry.npmjs.org/@wry/trie/-/trie-0.2.2.tgz" - integrity sha512-OxqBB39x6MfHaa2HpMiRMfhuUnQTddD32Ko020eBeJXq87ivX6xnSSnzKHVbA21p7iqBASz8n/07b6W5wW1BVQ== +"@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/fetch@^0.6.0": + version "0.6.9" + resolved "https://registry.yarnpkg.com/@whatwg-node/fetch/-/fetch-0.6.9.tgz#6cc694cc0378e27b8dfed427c5bf633eda6972b9" + integrity sha512-JfrBCJdMu9n9OARc0e/hPHcD98/8Nz1CKSdGYDg6VbObDkV/Ys30xe5i/wPOatYbxuvatj1kfWeHf7iNX3i17w== dependencies: - tslib "^1.14.1" + "@peculiar/webcrypto" "^1.4.0" + "@whatwg-node/node-fetch" "^0.0.5" + busboy "^1.6.0" + urlpattern-polyfill "^6.0.2" + web-streams-polyfill "^3.2.1" -"@xmldom/xmldom@^0.7.2": - version "0.7.5" - resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d" - integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A== +"@whatwg-node/fetch@^0.8.0", "@whatwg-node/fetch@^0.8.1": + 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" -acorn-jsx@^5.3.1: - version "5.3.1" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz" - integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== +"@whatwg-node/node-fetch@^0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@whatwg-node/node-fetch/-/node-fetch-0.0.5.tgz#bebf18891088e5e2fc449dea8d1bc94af5ec38df" + integrity sha512-hbccmaSZaItdsRuBKBEEhLoO+5oXJPxiyd0kG2xXd0Dh3Rt+vZn4pADHxuSiSHLd9CM+S2z4+IxlEGbWUgiz9g== + dependencies: + "@whatwg-node/events" "^0.0.2" + busboy "^1.6.0" + tslib "^2.3.1" -acorn@^7.4.0: - version "7.4.1" - resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +"@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" + +"@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.0": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.5.3.tgz#fafebc69561aa2d40340da89fa7dc4b1f6fb7831" + integrity sha512-avR+UXdSrsF2v8vIqIgmeTY0UR91UT+IyablCyKe/uk22uOJ8fusKZnH9JH9e1/EtLeNJBtagNmL3eJdnOV53g== + dependencies: + tslib "^2.3.0" + +"@wry/trie@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.3.2.tgz#a06f235dc184bd26396ba456711f69f8c35097e6" + integrity sha512-yRTyhWSls2OY/pYLfwff867r8ekooZ4UI+/gxot5Wj8EFwSf2rG+n+Mo/6LoLQm1TKA4GRj2+LCpbfS937dClQ== + 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" @@ -1767,19 +2706,41 @@ aes-decrypter@3.1.3: agent-base@6: version "6.0.2" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "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.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: +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.npmjs.org/ajv/-/ajv-6.12.6.tgz" + 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" @@ -1787,262 +2748,239 @@ ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^7.0.2: - version "7.2.3" - resolved "https://registry.npmjs.org/ajv/-/ajv-7.2.3.tgz" - integrity sha512-idv5WZvKVXDqKralOImQgPM9v6WOdLNa0IY3B3doOjw/YxRGT8I+allIJ6kd7Uaj+SF1xZUSU+nPM5aDNBVtnw== +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" -ajv@^8.0.1: - version "8.6.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764" - integrity sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw== +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: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - -ansi-colors@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== - -ansi-escapes@^3.0.0: - version "3.2.0" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz" - integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== - -ansi-escapes@^4.2.1, ansi-escapes@^4.3.1: - version "4.3.1" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz" - integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== - dependencies: - type-fest "^0.11.0" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + 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@^2.2.1: - version "2.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - ansi-styles@^3.2.1: version "3.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" + 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.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" -any-observable@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz" - integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog== - -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== +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@^14.1.3: - version "14.1.3" - resolved "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-14.1.3.tgz" - integrity sha512-X2T+7pHk5lcaaWnvP9h2tuAAMCzOW6/9juedQ0ZuGp3Ufl81BpDISlCs0o6u29wBV0RRT/QpMU2gbP+3FCfVpQ== +apollo-upload-client@^17.0.0: + version "17.0.0" + resolved "https://registry.yarnpkg.com/apollo-upload-client/-/apollo-upload-client-17.0.0.tgz#d9baaff8d14e54510de9f2855b487e75ca63b392" + integrity sha512-pue33bWVbdlXAGFPkgz53TTmxVMrKeQr0mdRcftNY+PoHIdbGZD0hoaXHvO6OePJAkFz7OiCFUf98p1G/9+Ykw== dependencies: - "@apollo/client" "^3.2.5" - "@babel/runtime" "^7.12.5" - extract-files "^9.0.0" + extract-files "^11.0.0" arg@^4.1.0: version "4.1.3" - resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" + 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.npmjs.org/argparse/-/argparse-1.0.10.tgz" + 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.npmjs.org/argparse/-/argparse-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-query@^4.2.2: - version "4.2.2" - resolved "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz" - integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== +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: - "@babel/runtime" "^7.10.2" - "@babel/runtime-corejs3" "^7.10.2" + deep-equal "^2.0.5" -array-includes@^3.1.1, array-includes@^3.1.2, array-includes@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz" - integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A== +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.3" - es-abstract "^1.18.0-next.2" - get-intrinsic "^1.1.1" - is-string "^1.0.5" - -array-includes@^3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" - integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - get-intrinsic "^1.1.1" + 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.npmjs.org/array-union/-/array-union-2.1.0.tgz" + 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.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" - integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg== +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.3" - es-abstract "^1.19.0" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" -array.prototype.flatmap@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz" - integrity sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q== +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.0" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - function-bind "^1.1.1" + 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.npmjs.org/arrify/-/arrify-1.0.1.tgz" - integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + 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.npmjs.org/asap/-/asap-2.0.6.tgz" - integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + 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.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" - integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= + 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.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - asynckit@^0.4.0: version "0.4.0" - resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== at-least-node@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" + 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.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-4.0.0.tgz#e3589fc6c2da8f7ca43ba9f84fa52a744fc997fb" integrity sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ== -autoprefixer@^9.8.6: - version "9.8.6" - resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz" - integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== - dependencies: - browserslist "^4.12.0" - caniuse-lite "^1.0.30001109" - colorette "^1.2.1" - normalize-range "^0.1.2" - num2fraction "^1.2.2" - postcss "^7.0.32" - postcss-value-parser "^4.1.0" +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.0.2: - version "4.1.3" - resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.1.3.tgz" - integrity sha512-vwPpH4Aj4122EW38mxO/fxhGKtwWTMLDIJfZ1He0Edbtjcfna/R3YB67yVhezUMzqc3Jr3+Ii50KRntlENL4xQ== +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== -axios@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.1.3.tgz#8274250dada2edf53814ed7db644b9c2866c1e35" - integrity sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA== +axios@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.3.tgz#e7011384ba839b885007c9c9fae1ff23dceb295b" + integrity sha512-eYq77dYIFS77AQlhzEL937yUBSepBfPIe8FcgEDN35vMNZKMrs81pgnyrQpwfy4NF4b4XWX1Zgx7yX+25w8QJA== dependencies: follow-redirects "^1.15.0" form-data "^4.0.0" proxy-from-env "^1.1.0" -axobject-query@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz" - integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== +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.npmjs.org/b64-to-blob/-/b64-to-blob-1.2.19.tgz" + resolved "https://registry.yarnpkg.com/b64-to-blob/-/b64-to-blob-1.2.19.tgz#157d85fdc8811665b9a35d29ffbc6a522ba28fbe" integrity sha512-L3nSu8GgF4iEyNYakCQSfL2F5GI5aCXcot9mNTf+4N0/BMhpxqqHyOb6jIR24iq2xLjQZLG8FOt3gnUcV+9NVg== -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== +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: - object.assign "^4.1.0" + "@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.npmjs.org/babel-plugin-react-intl/-/babel-plugin-react-intl-7.9.4.tgz" + 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" @@ -2058,13 +2996,13 @@ babel-plugin-react-intl@^7.0.0: babel-plugin-syntax-trailing-function-commas@^7.0.0-beta.0: version "7.0.0-beta.0" - resolved "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz" + 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.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-3.3.0.tgz" - integrity sha512-7QTLTCd2gwB2qGoi5epSULMHugSVgpcVt5YAeiFO9ABLrutDQzKfGwzxgZHLpugq8qMdg/DhRZDZ5CLKxBkEbw== +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" @@ -2094,73 +3032,98 @@ babel-preset-fbjs@^3.3.0: "@babel/plugin-transform-template-literals" "^7.0.0" babel-plugin-syntax-trailing-function-commas "^7.0.0-beta.0" -backo2@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz" - integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= - bail@^1.0.0: version "1.0.5" - resolved "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz" + resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== -bail@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/bail/-/bail-2.0.1.tgz" - integrity sha512-d5FoTAr2S5DSUPKl85WNm2yUwsINN8eidIdIwsOge2t33DaOfOdSmmsI11jMN3GmALCXaw+Y6HMVHDzePshFAA== - balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + 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.npmjs.org/base64-blob/-/base64-blob-1.4.1.tgz" + 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.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + 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.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -bootstrap@^4.6.0: - version "4.6.0" - resolved "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz" - integrity sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw== +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.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + 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.1, braces@~3.0.2: +braces@^3.0.2, braces@~3.0.2: version "3.0.2" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== dependencies: fill-range "^7.0.1" -browserslist@^4.12.0, browserslist@^4.14.5: - version "4.18.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.18.1.tgz#60d3920f25b6860eb917c6c7b185576f4d8b017f" - integrity sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ== +browserslist@^4.21.3, browserslist@^4.21.4, browserslist@^4.21.5: + version "4.21.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" + integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== dependencies: - caniuse-lite "^1.0.30001280" - electron-to-chromium "^1.3.896" - escalade "^3.1.1" - node-releases "^2.0.1" - picocolors "^1.0.0" + caniuse-lite "^1.0.30001449" + electron-to-chromium "^1.4.284" + node-releases "^2.0.8" + update-browserslist-db "^1.0.10" bser@2.1.1: version "2.1.1" @@ -2171,45 +3134,32 @@ bser@2.1.1: buffer-equal-constant-time@1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + 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.7.0: +buffer@^5.5.0: version "5.7.1" - resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" + 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@^0.3.1: - version "0.3.1" - resolved "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz" - integrity sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw== +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: - dicer "0.3.0" - -cacheable-request@^6.0.0: - version "6.1.0" - resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz" - integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^3.0.0" - lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^1.0.2" + streamsearch "^1.1.0" call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" + 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" @@ -2217,20 +3167,12 @@ call-bind@^1.0.0, call-bind@^1.0.2: callsites@^3.0.0: version "3.1.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camel-case@4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz" - integrity sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q== - dependencies: - pascal-case "^3.1.1" - tslib "^1.10.0" - -camel-case@4.1.2, camel-case@^4.1.2: +camel-case@^4.1.2: version "4.1.2" - resolved "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== dependencies: pascal-case "^3.1.2" @@ -2238,7 +3180,7 @@ camel-case@4.1.2, camel-case@^4.1.2: camelcase-keys@^6.2.2: version "6.2.2" - resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz" + 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" @@ -2247,17 +3189,17 @@ camelcase-keys@^6.2.2: camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001280: - version "1.0.30001282" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001282.tgz#38c781ee0a90ccfe1fe7fefd00e43f5ffdcb96fd" - integrity sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg== +caniuse-lite@^1.0.30001449: + version "1.0.30001453" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001453.tgz#6d3a1501622bf424a3cee5ad9550e640b0de3de8" + integrity sha512-R9o/uySW38VViaTrOtwfbFEiBFUh7ST3uIG4OEymIG3/uKdHDO4xk/FaqfUw0d+irSUyFPy3dZszf9VvSTPnsA== capital-case@^1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" integrity sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A== dependencies: no-case "^3.0.4" @@ -2266,38 +3208,19 @@ capital-case@^1.0.4: ccount@^1.0.0: version "1.1.0" - resolved "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== -chalk@^1.0.0, chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.0.0: version "2.4.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" + 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: - version "4.1.0" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz" - integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@^4.1.2: +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== @@ -2305,25 +3228,41 @@ chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -change-case-all@1.0.12: - version "1.0.12" - resolved "https://registry.npmjs.org/change-case-all/-/change-case-all-1.0.12.tgz" - integrity sha512-zdQus7R0lkprF99lrWUC5bFj6Nog4Xt4YCEjQ/vM4vbc6b5JHFBQMxRPAjfx+HJH8WxMzH0E+lQ8yQJLgmPCBg== +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.1" - is-lower-case "^2.0.1" - is-upper-case "^2.0.1" - lower-case "^2.0.1" - lower-case-first "^2.0.1" - sponge-case "^1.0.0" - swap-case "^2.0.1" - title-case "^3.0.2" - upper-case "^2.0.1" - upper-case-first "^2.0.1" + 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.1: +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.npmjs.org/change-case/-/change-case-4.1.2.tgz" + 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" @@ -2341,237 +3280,218 @@ change-case@^4.1.1: character-entities-legacy@^1.0.0: version "1.1.4" - resolved "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz" + 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-legacy@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-2.0.0.tgz" - integrity sha512-YwaEtEvWLpFa6Wh3uVLrvirA/ahr9fki/NUd/Bd4OR6EdJ8D22hovYQEOUCBfQfcqnC4IAMGMsHXY1eXgL4ZZA== - character-entities@^1.0.0: version "1.2.4" - resolved "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== -character-entities@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/character-entities/-/character-entities-2.0.0.tgz" - integrity sha512-oHqMj3eAuJ77/P5PaIRcqk+C3hdfNwyCD2DAUcD5gyXkegAuF2USC40CEqPscDk4I8FRGMTojGJQkXDsN5QlJA== - character-reference-invalid@^1.0.0: version "1.1.4" - resolved "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== -character-reference-invalid@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.0.tgz" - integrity sha512-pE3Z15lLRxDzWJy7bBHBopRwfI20sbrMVLQTC7xsPglCHf4Wv1e167OgYAFP78co2XlhojDyAqA+IAJse27//g== - chardet@^0.7.0: version "0.7.0" - resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -"chokidar@>=2.0.0 <4.0.0", chokidar@^3.4.3: - version "3.5.1" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.2: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== dependencies: - anymatch "~3.1.1" + anymatch "~3.1.2" braces "~3.0.2" - glob-parent "~5.1.0" + glob-parent "~5.1.2" is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.5.0" + readdirp "~3.6.0" optionalDependencies: - fsevents "~2.3.1" + fsevents "~2.3.2" -classnames@^2.2.5: - version "2.3.1" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" - integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== +classnames@^2.2.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== -classnames@^2.2.6: - version "2.2.6" - resolved "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz" - integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== - -cldr-core@38: - version "38.1.0" - resolved "https://registry.npmjs.org/cldr-core/-/cldr-core-38.1.0.tgz" - integrity sha512-Da9xKjDp4qGGIX0VDsBqTan09iR5nuYD2a/KkfEaUyqKhu6wFVNRiCpPDXeRbpVwPBY6PgemV8WiHatMhcpy4A== - -cli-cursor@^2.0.0, cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz" - integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= - dependencies: - restore-cursor "^2.0.0" +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.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz" + 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-truncate@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz" - integrity sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ= +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 "0.0.4" - string-width "^1.0.1" + slice-ansi "^3.0.0" + string-width "^4.2.0" cli-width@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" + 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.npmjs.org/cliui/-/cliui-6.0.0.tgz" + 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@^7.0.2: - version "7.0.4" - resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== +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.0" + strip-ansi "^6.0.1" wrap-ansi "^7.0.0" -clone-regexp@^2.1.0: - version "2.2.0" - resolved "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz" - integrity sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q== - dependencies: - is-regexp "^2.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== -clone-response@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz" - integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= - dependencies: - mimic-response "^1.0.0" - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +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.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" + 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.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + 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.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + 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.npmjs.org/color-name/-/color-name-1.1.4.tgz" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colorette@^1.2.1: - version "1.2.2" - resolved "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== +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== combined-stream@^1.0.8: version "1.0.8" - resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" -comma-separated-tokens@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz" - integrity sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg== +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== -common-tags@1.8.0, common-tags@^1.8.0: - version "1.8.0" - resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz" - integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== +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.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + 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.10" - resolved "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz" - integrity sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA== + 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.npmjs.org/constant-case/-/constant-case-3.0.4.tgz" + 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.7.0: - version "1.7.0" - resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz" - integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== - dependencies: - safe-buffer "~5.1.1" +convert-source-map@^1.5.0, convert-source-map@^1.7.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== cookie@^0.4.0: - version "0.4.1" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz" - integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + 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-pure@^3.0.0: - version "3.9.1" - resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.9.1.tgz" - integrity sha512-laz3Zx0avrw9a4QEIdmIblnVuJz8W51leY9iLThatCsFawWxC3sE4guASC78JbCin+DkwMpCdp1AVAuzL/GN7A== - -cosmiconfig-toml-loader@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/cosmiconfig-toml-loader/-/cosmiconfig-toml-loader-1.0.0.tgz" - integrity sha512-H/2gurFWVi7xXvCyvsWRLCMekl4tITJcX0QEsDMpzxtuxDyM59xLatYNg4s/k9AA/HdtCYfj2su8mgA0GSDLDA== +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: - "@iarna/toml" "^2.2.5" + browserslist "^4.21.5" -cosmiconfig@6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== +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-typescript-loader@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.3.0.tgz#c4259ce474c9df0f32274ed162c0447c951ef073" + integrity sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q== + +cosmiconfig@8.0.0, cosmiconfig@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.0.0.tgz#e9feae014eab580f858f8a0288f38997a7bebe97" + integrity sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ== dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" parse-json "^5.0.0" path-type "^4.0.0" - yaml "^1.7.2" cosmiconfig@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz" - integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== + 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" @@ -2581,83 +3501,91 @@ cosmiconfig@^7.0.0: create-require@^1.1.0: version "1.1.1" - resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-fetch@3.0.6: - version "3.0.6" - resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.6.tgz" - integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ== +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.1" - -cross-fetch@3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.1.tgz" - integrity sha512-eIF+IHQpRzoGd/0zPrwQmHwDC90mdvjk+hcbYhKoaRrEk4GEIDqdjs/MljmdPPoHTQudbmWS+f0hZsEpFaEvWw== - dependencies: - node-fetch "2.6.1" - -cross-fetch@^3.0.4, cross-fetch@^3.0.6: - version "3.1.2" - resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.2.tgz" - integrity sha512-+JhD65rDNqLbGmB3Gzs3HrEKC0aQnD+XA3SY6RjgkF88jV2q5cTc5+CwxlS3sdmLk98gpPt5CF9XRnPdlxZe6w== - dependencies: - node-fetch "2.6.1" + node-fetch "2.6.7" cross-spawn@^7.0.2: version "7.0.3" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" which "^2.0.1" +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.0.7" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.7.tgz#2a5fb75e1015e84dd15692f71e89a1450290950b" - integrity sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g== + 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.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791" - integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug== +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== -dataloader@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.0.0.tgz#41eaf123db115987e21ca93c005cd7753c55fe6f" - integrity sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ== +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" -date-fns@^1.27.2: - version "1.30.1" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" - integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== +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.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== +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@^2.6.9: - 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" - debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -2665,581 +3593,436 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== - dependencies: - ms "2.1.2" - decamelize-keys@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz" - integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= + 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.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz" - integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= +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: - mimic-response "^1.0.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + 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.3" - resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + 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.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== -defer-to-connect@^1.0.1: - version "1.1.3" - resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz" - integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== - -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== +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: - object-keys "^1.0.12" + 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" delayed-stream@~1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== dependency-graph@^0.11.0: version "0.11.0" - resolved "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz" + resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27" integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg== -dequal@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz" - integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug== +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.0.0" - resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-6.0.0.tgz" - integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA== + 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.npmjs.org/diacritics/-/diacritics-1.3.0.tgz" - integrity sha1-PvqHMj67hj5mls67AILUj/PW96E= - -dicer@0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz" - integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA== - dependencies: - streamsearch "0.1.2" + 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.npmjs.org/diff/-/diff-4.0.2.tgz" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -diff@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz" - integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== - dir-glob@^3.0.1: version "3.0.1" - resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" + 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.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" + 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.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" + 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.1.2, dom-helpers@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz" - integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ== +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-serializer@0: - version "0.2.2" - resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz" - integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== - dependencies: - domelementtype "^2.0.1" - entities "^2.0.0" - 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== -domelementtype@1, domelementtype@^1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz" - integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== - -domelementtype@^2.0.1: - version "2.1.0" - resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz" - integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== - -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - -domutils@^1.5.1: - version "1.7.0" - resolved "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - dot-case@^3.0.4: version "3.0.4" - resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz" + 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@^8.2.0: - version "8.2.0" - resolved "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz" - integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== +dotenv@^16.0.0: + version "16.0.3" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" + integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= +dset@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.2.tgz#89c436ca6450398396dc6538ea00abc0c54cd45a" + integrity sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q== ecdsa-sig-formatter@1.0.11: version "1.0.11" - resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== dependencies: safe-buffer "^5.0.1" -electron-to-chromium@^1.3.896: - version "1.3.901" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.901.tgz#ce2c3157d61bce9f42f1e83225c17358ae9f4918" - integrity sha512-ToJdV2vzwT2jeAsw8zIggTFllJ4Kxvwghk39AhJEHHlIxor10wsFI3wo69p8nFc0s/ATWBqugPv/k3nW4Y9Mww== - -elegant-spinner@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz" - integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= +electron-to-chromium@^1.4.284: + version "1.4.299" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.299.tgz#faa2069cd4879a73e540e533178db5c618768d41" + integrity sha512-lQ7ijJghH6pCGbfWXr6EY+KYCMaRSjgsY925r1p/TlpSfVM1VjHTcn1gAc15VM4uwti283X6QtjPTXdpoSGiZQ== emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emoji-regex@^9.0.0: +emoji-regex@^9.2.2: version "9.2.2" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enquire.js@^2.1.6: - version "2.1.6" - resolved "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz" - integrity sha1-PoeAybi4NQhMP2DhZtvDwqPImBQ= - -enquirer@^2.3.5: - version "2.3.6" - resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== - dependencies: - ansi-colors "^4.1.1" - -entities@^1.1.1: - version "1.1.2" - resolved "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz" - integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - error-ex@^1.3.1: version "1.3.2" - resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" + 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.18.0-next.1, es-abstract@^1.18.0-next.2: - version "1.18.0" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz" - integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw== +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" - get-intrinsic "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.2" - is-callable "^1.2.3" - is-negative-zero "^2.0.1" - is-regex "^1.1.2" - is-string "^1.0.5" - object-inspect "^1.9.0" - object-keys "^1.1.1" - object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.0" - -es-abstract@^1.19.0, es-abstract@^1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" - integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - get-intrinsic "^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-symbols "^1.0.2" - internal-slot "^1.0.3" - is-callable "^1.2.4" - is-negative-zero "^2.0.1" + 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.1" + is-shared-array-buffer "^1.0.2" is-string "^1.0.7" - is-weakref "^1.0.1" - object-inspect "^1.11.0" + 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.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.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.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" + 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" -esbuild-android-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be" - integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ== +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-android-arm64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771" - integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg== - -esbuild-darwin-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25" - integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug== - -esbuild-darwin-arm64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73" - integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw== - -esbuild-freebsd-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d" - integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg== - -esbuild-freebsd-arm64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48" - integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q== - -esbuild-linux-32@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5" - integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw== - -esbuild-linux-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652" - integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg== - -esbuild-linux-arm64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b" - integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig== - -esbuild-linux-arm@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59" - integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw== - -esbuild-linux-mips64le@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34" - integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw== - -esbuild-linux-ppc64le@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e" - integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ== - -esbuild-linux-riscv64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8" - integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg== - -esbuild-linux-s390x@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6" - integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA== - -esbuild-netbsd-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81" - integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w== - -esbuild-openbsd-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b" - integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw== - -esbuild-sunos-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da" - integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw== - -esbuild-windows-32@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31" - integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w== - -esbuild-windows-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4" - integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ== - -esbuild-windows-arm64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982" - integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg== - -esbuild@^0.14.27: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2" - integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA== +esbuild@^0.16.14: + version "0.16.17" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.16.17.tgz#fc2c3914c57ee750635fee71b89f615f25065259" + integrity sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg== optionalDependencies: - "@esbuild/linux-loong64" "0.14.54" - esbuild-android-64 "0.14.54" - esbuild-android-arm64 "0.14.54" - esbuild-darwin-64 "0.14.54" - esbuild-darwin-arm64 "0.14.54" - esbuild-freebsd-64 "0.14.54" - esbuild-freebsd-arm64 "0.14.54" - esbuild-linux-32 "0.14.54" - esbuild-linux-64 "0.14.54" - esbuild-linux-arm "0.14.54" - esbuild-linux-arm64 "0.14.54" - esbuild-linux-mips64le "0.14.54" - esbuild-linux-ppc64le "0.14.54" - esbuild-linux-riscv64 "0.14.54" - esbuild-linux-s390x "0.14.54" - esbuild-netbsd-64 "0.14.54" - esbuild-openbsd-64 "0.14.54" - esbuild-sunos-64 "0.14.54" - esbuild-windows-32 "0.14.54" - esbuild-windows-64 "0.14.54" - esbuild-windows-arm64 "0.14.54" + "@esbuild/android-arm" "0.16.17" + "@esbuild/android-arm64" "0.16.17" + "@esbuild/android-x64" "0.16.17" + "@esbuild/darwin-arm64" "0.16.17" + "@esbuild/darwin-x64" "0.16.17" + "@esbuild/freebsd-arm64" "0.16.17" + "@esbuild/freebsd-x64" "0.16.17" + "@esbuild/linux-arm" "0.16.17" + "@esbuild/linux-arm64" "0.16.17" + "@esbuild/linux-ia32" "0.16.17" + "@esbuild/linux-loong64" "0.16.17" + "@esbuild/linux-mips64el" "0.16.17" + "@esbuild/linux-ppc64" "0.16.17" + "@esbuild/linux-riscv64" "0.16.17" + "@esbuild/linux-s390x" "0.16.17" + "@esbuild/linux-x64" "0.16.17" + "@esbuild/netbsd-x64" "0.16.17" + "@esbuild/openbsd-x64" "0.16.17" + "@esbuild/sunos-x64" "0.16.17" + "@esbuild/win32-arm64" "0.16.17" + "@esbuild/win32-ia32" "0.16.17" + "@esbuild/win32-x64" "0.16.17" 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.2, escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.5: version "1.0.5" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + 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.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-airbnb-base@14.2.1, eslint-config-airbnb-base@^14.2.1: - version "14.2.1" - resolved "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz" - integrity sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA== +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.2" - -eslint-config-airbnb-typescript@^14.0.1: - version "14.0.1" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-14.0.1.tgz#6721eb320d3953ae0d4bf258e877900fcb543a38" - integrity sha512-tF4GwC3sRrw8kEj4/yxX8F7AcLzj/1IESBnsCiFMplzYmxre459qm2z9DFkCpqBVQFSH6j2K4+VKVteX4m0GsQ== - dependencies: - eslint-config-airbnb-base "14.2.1" - -eslint-config-airbnb@^18.2.1: - version "18.2.1" - resolved "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.2.1.tgz" - integrity sha512-glZNDEZ36VdlZWoxn/bUR1r/sdFKPd1mHPbqUtkctgNG4yT2DLLtJ3D+yCV+jzZCc2V1nBVkmdknOJBZ5Hc0fg== - dependencies: - eslint-config-airbnb-base "^14.2.1" - object.assign "^4.1.2" - object.entries "^1.1.2" - -eslint-config-prettier@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" - integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== - -eslint-import-resolver-node@^0.3.6: - version "0.3.6" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" - integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== - dependencies: - debug "^3.2.7" - resolve "^1.20.0" - -eslint-module-utils@^2.7.0: - version "2.7.1" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz#b435001c9f8dd4ab7f6d0efcae4b9696d4c24b7c" - integrity sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ== - dependencies: - debug "^3.2.7" - find-up "^2.1.0" - pkg-dir "^2.0.0" - -eslint-plugin-import@^2.25.2: - version "2.25.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.2.tgz#b3b9160efddb702fc1636659e71ba1d10adbe9e9" - integrity sha512-qCwQr9TYfoBHOFcVGKY9C9unq05uOxxdklmBXLVvcwo68y5Hta6/GzCZEMx2zQiu0woKNEER0LE7ZgaOfBU14g== - dependencies: - array-includes "^3.1.4" - array.prototype.flat "^1.2.5" - debug "^2.6.9" - doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.6" - eslint-module-utils "^2.7.0" - has "^1.0.3" - is-core-module "^2.7.0" - is-glob "^4.0.3" - minimatch "^3.0.4" - object.values "^1.1.5" - resolve "^1.20.0" - tsconfig-paths "^3.11.0" - -eslint-plugin-jsx-a11y@^6.4.1: - version "6.4.1" - resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz" - integrity sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg== - dependencies: - "@babel/runtime" "^7.11.2" - aria-query "^4.2.2" - array-includes "^3.1.1" - ast-types-flow "^0.0.7" - axe-core "^4.0.2" - axobject-query "^2.2.0" - damerau-levenshtein "^1.0.6" - emoji-regex "^9.0.0" - has "^1.0.3" - jsx-ast-utils "^3.1.0" - language-tags "^1.0.5" - -eslint-plugin-react-hooks@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz" - integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== - -eslint-plugin-react@^7.26.1: - version "7.26.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.26.1.tgz#41bcfe3e39e6a5ac040971c1af94437c80daa40e" - integrity sha512-Lug0+NOFXeOE+ORZ5pbsh6mSKjBKXDXItUD2sQoT+5Yl0eoT82DqnXeTMfUare4QVCn9QwXbfzO/dBLjLXwVjQ== - dependencies: - array-includes "^3.1.3" - array.prototype.flatmap "^1.2.4" - doctrine "^2.1.0" - estraverse "^5.2.0" - jsx-ast-utils "^2.4.1 || ^3.0.0" - minimatch "^3.0.4" - object.entries "^1.1.4" - object.fromentries "^2.0.4" - object.hasown "^1.0.0" - object.values "^1.1.4" - prop-types "^15.7.2" - resolve "^2.0.0-next.3" + object.entries "^1.1.5" semver "^6.3.0" - string.prototype.matchall "^4.0.5" + +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.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" + 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-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== +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: - eslint-visitor-keys "^1.1.0" + esrecurse "^4.3.0" + estraverse "^5.2.0" eslint-utils@^3.0.0: version "3.0.0" @@ -3248,124 +4031,104 @@ eslint-utils@^3.0.0: dependencies: eslint-visitor-keys "^2.0.0" -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - eslint-visitor-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz" - integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + 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@^7.32.0: - version "7.32.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" - integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== +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: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.3" - "@humanwhocodes/config-array" "^0.5.0" + "@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.0.1" + debug "^4.3.2" doctrine "^3.0.0" - enquirer "^2.3.5" escape-string-regexp "^4.0.0" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" + 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" - functional-red-black-tree "^1.0.1" - glob-parent "^5.1.2" - globals "^13.6.0" - ignore "^4.0.6" + 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" - js-yaml "^3.13.1" + 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.0.4" + minimatch "^3.1.2" natural-compare "^1.4.0" optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" + regexpp "^3.2.0" + strip-ansi "^6.0.1" strip-json-comments "^3.1.0" - table "^6.0.9" text-table "^0.2.0" - v8-compile-cache "^2.0.3" -espree@^7.3.0, espree@^7.3.1: - version "7.3.1" - resolved "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== +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 "^7.4.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" + 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.npmjs.org/esprima/-/esprima-4.0.1.tgz" + 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.0" - resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + 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.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + 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.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== +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.npmjs.org/esutils/-/esutils-2.0.3.tgz" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -eventemitter3@^3.1.0: - version "3.1.2" - resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz" - integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== - -eventsource@1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz" - integrity sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg== - dependencies: - original "^1.0.0" - -execall@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/execall/-/execall-2.0.0.tgz#16a06b5fe5099df7d00be5d9c06eecded1663b45" - integrity sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow== - dependencies: - clone-regexp "^2.1.0" - extend@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -3373,21 +4136,26 @@ extend@^3.0.0: external-editor@^3.0.3: version "3.1.0" - resolved "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz" + 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@9.0.0, extract-files@^9.0.0: +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@^9.0.0: version "9.0.0" - resolved "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz" + resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a" integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ== extract-react-intl-messages@^4.1.1: version "4.1.1" - resolved "https://registry.npmjs.org/extract-react-intl-messages/-/extract-react-intl-messages-4.1.1.tgz" + 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" @@ -3406,134 +4174,143 @@ extract-react-intl-messages@^4.1.1: 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.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + 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.1.1, fast-glob@^3.2.5: - version "3.2.5" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz" - integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== +fast-glob@^3.2.12, fast-glob@^3.2.9: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" + glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" + micromatch "^4.0.4" fast-json-stable-stringify@^2.0.0: version "2.1.0" - resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + 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.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-memoize@^2.5.2: - version "2.5.2" - resolved "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz" - integrity sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw== +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" -fastest-levenshtein@^1.0.12: - version "1.0.12" - resolved "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz" - integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== +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.11.0" - resolved "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz" - integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== + 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.1" - resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz" - integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== + 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.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz" + 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.0" - resolved "https://registry.npmjs.org/fbjs/-/fbjs-3.0.0.tgz" - integrity sha512-dJd4PiDOFuhe7vk4F80Mba83Vr2QuK86FoxtgPmzBqEJahncp+13YCmfoa53KHCo6OnlXLG7eeMWPfB5CrpVKg== + 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.0.4" + 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.18" - -figures@^1.7.0: - version "1.7.0" - resolved "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz" - integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= - dependencies: - escape-string-regexp "^1.0.5" - object-assign "^4.1.0" - -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= - dependencies: - escape-string-regexp "^1.0.5" + ua-parser-js "^0.7.30" figures@^3.0.0: version "3.2.0" - resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz" + 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.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" + 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.0.1: version "7.0.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" + 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-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" +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.npmjs.org/find-up/-/find-up-4.1.0.tgz" + 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" -flag-icon-css@^3.5.0: - version "3.5.0" - resolved "https://registry.npmjs.org/flag-icon-css/-/flag-icon-css-3.5.0.tgz" - integrity sha512-pgJnJLrtb0tcDgU1fzGaQXmR8h++nXvILJ+r5SmOXaaL/2pocunQo2a8TAXhjQnBpRLPtZ1KCz/TYpqeNuE2ew== +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.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== dependencies: flatted "^3.1.0" @@ -3541,64 +4318,66 @@ flat-cache@^3.0.4: flat@^5.0.0: version "5.0.2" - resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== flatted@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz" - integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== + 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.npmjs.org/flexbin/-/flexbin-0.2.0.tgz" - integrity sha1-ASYwbT1ZX8t9/LhxSbnJWZ/49Ok= + resolved "https://registry.yarnpkg.com/flexbin/-/flexbin-0.2.0.tgz#0126306d3d595fcb7dfcb87149b9c9599ff8f4e9" + integrity sha512-dgCeT6/oVljr0eao0f7Eg2VXutK/+rp02J6Nkw22uTTFE4HSC7zfYRzjuy2/r0dhr/sUBRMJM2tMyOCi+HeU+A== follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== -form-data@4.0.0, form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== +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: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" + is-callable "^1.1.3" form-data@^3.0.0: version "3.0.1" - resolved "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" mime-types "^2.1.12" -formik@^2.2.6: - version "2.2.6" - resolved "https://registry.npmjs.org/formik/-/formik-2.2.6.tgz" - integrity sha512-Kxk2zQRafy56zhLmrzcbryUpMBvT0tal5IvcifK5+4YNGelKsnrODFJ0sZQRMQboblWNym4lAW3bt+tf2vApSA== +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formik@^2.2.9: + version "2.2.9" + resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0" + integrity sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA== dependencies: deepmerge "^2.1.1" hoist-non-react-statics "^3.3.0" - lodash "^4.17.14" - lodash-es "^4.17.14" + lodash "^4.17.21" + lodash-es "^4.17.21" react-fast-compare "^2.0.1" tiny-warning "^1.0.2" tslib "^1.10.0" -fs-capacitor@^6.1.0: - version "6.2.0" - resolved "https://registry.npmjs.org/fs-capacitor/-/fs-capacitor-6.2.0.tgz" - integrity sha512-nKcE1UduoSKX27NSZlg879LdQc94OtbOsEmKMN2MBNudXREvijRKx2GEBsTMTfws+BrbkJoEuynbGSVRSpauvw== - fs-extra@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" - integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + 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" @@ -3606,7 +4385,7 @@ fs-extra@^10.0.0: fs-extra@^9.0.0: version "9.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" + 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" @@ -3616,61 +4395,52 @@ fs-extra@^9.0.0: fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.1, fsevents@~2.3.2: +fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== function-bind@^1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +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.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" + 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.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + 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.0, get-intrinsic@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz" - integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== +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.1" - -get-stdin@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz" - integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== - -get-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" + has-symbols "^1.0.3" get-symbol-description@^1.0.0: version "1.0.0" @@ -3680,47 +4450,49 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" -glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + 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-regex@^0.3.0: - version "0.3.2" - resolved "https://registry.yarnpkg.com/glob-regex/-/glob-regex-0.3.2.tgz#27348f2f60648ec32a4a53137090b9fb934f3425" - integrity sha512-m5blUd3/OqDTWwzBBtWBPrGlAzatRywHameHeekAZyZrskYouOGdNB8T/q6JucucvJXtOuyHIn0/Yia7iDasDw== +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.1.6" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + 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.0.4" + 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.npmjs.org/global-modules/-/global-modules-2.0.0.tgz" + 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.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz" + 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.4.0, global@~4.4.0: +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== @@ -3730,189 +4502,141 @@ global@^4.3.1, global@^4.4.0, global@~4.4.0: globals@^11.1.0: version "11.12.0" - resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.6.0: - version "13.7.0" - resolved "https://registry.npmjs.org/globals/-/globals-13.7.0.tgz" - integrity sha512-Aipsz6ZKRxa/xQkZhNg0qIWXT6x6rD46f6x/PCnBomlttdIyAPak4YD9jTmKpZ72uROSMU87qJtcgpgHaVchiA== +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" -globals@^13.9.0: - version "13.11.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.11.0.tgz#40ef678da117fe7bd2e28f1fab24951bd0255be7" - integrity sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g== +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: - type-fest "^0.20.2" + define-properties "^1.1.3" -globby@11.0.2: - version "11.0.2" - resolved "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz" - integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og== +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.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" - -globby@^11.0.2: - version "11.0.3" - resolved "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz" - integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" - -globby@^11.0.3: - version "11.0.4" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" - integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" + 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.npmjs.org/globjoin/-/globjoin-0.1.4.tgz" - integrity sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM= + 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.npmjs.org/globrex/-/globrex-0.1.2.tgz" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== -gonzales-pe@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz" - integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ== +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: - minimist "^1.2.5" - -got@^9.6.0: - version "9.6.0" - resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz" - integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== - dependencies: - "@sindresorhus/is" "^0.14.0" - "@szmarczak/http-timer" "^1.1.2" - cacheable-request "^6.0.0" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^4.1.0" - lowercase-keys "^1.0.1" - mimic-response "^1.0.1" - p-cancelable "^1.0.0" - to-readable-stream "^1.0.0" - url-parse-lax "^3.0.0" + get-intrinsic "^1.1.3" graceful-fs@^4.1.15, graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.6" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz" - integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -graphql-config@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/graphql-config/-/graphql-config-3.2.0.tgz" - integrity sha512-ygEKDeQNZKpm4137560n2oY3bGM0D5zyRsQVaJntKkufWdgPg6sb9/4J1zJW2y/yC1ortAbhNho09qmeJeLa9g== +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@^4.4.0: + version "4.4.1" + resolved "https://registry.yarnpkg.com/graphql-config/-/graphql-config-4.4.1.tgz#2b1b5215b38911c0b15ff9b2e878101c984802d6" + integrity sha512-B8wlvfBHZ5WnI4IiuQZRqql6s+CKz7S+xpUeTb28Z8nRBi8tH9ChEBgT5FnTyE05PUhHlrS2jK9ICJ4YBl9OtQ== dependencies: - "@endemolshinegroup/cosmiconfig-typescript-loader" "3.0.2" - "@graphql-tools/graphql-file-loader" "^6.0.0" - "@graphql-tools/json-file-loader" "^6.0.0" - "@graphql-tools/load" "^6.0.0" - "@graphql-tools/merge" "^6.0.0" - "@graphql-tools/url-loader" "^6.0.0" - "@graphql-tools/utils" "^6.0.0" - cosmiconfig "6.0.0" - cosmiconfig-toml-loader "1.0.0" - minimatch "3.0.4" + "@graphql-tools/graphql-file-loader" "^7.3.7" + "@graphql-tools/json-file-loader" "^7.3.7" + "@graphql-tools/load" "^7.5.5" + "@graphql-tools/merge" "^8.2.6" + "@graphql-tools/url-loader" "^7.9.7" + "@graphql-tools/utils" "^9.0.0" + cosmiconfig "8.0.0" + minimatch "4.2.1" string-env-interpolation "1.0.1" - tslib "^2.0.0" + tslib "^2.4.0" -graphql-request@^3.3.0: - version "3.4.0" - resolved "https://registry.npmjs.org/graphql-request/-/graphql-request-3.4.0.tgz" - integrity sha512-acrTzidSlwAj8wBNO7Q/UQHS8T+z5qRGquCQRv9J1InwR01BBWV9ObnoE+JS5nCCEj8wSGS0yrDXVDoRiKZuOg== +graphql-request@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-5.1.0.tgz#dbc8feee27d21b993cd5da2d3af67821827b240a" + integrity sha512-0OeRVYigVwIiXhNmqnPDt+JhMzsjinxHE7TVy3Lm6jUzav0guVcL0lfSbi6jVTRAxcbwgyr6yrZioSHxf9gHzw== dependencies: - cross-fetch "^3.0.6" + "@graphql-typed-document-node/core" "^3.1.1" + cross-fetch "^3.1.5" extract-files "^9.0.0" form-data "^3.0.0" -graphql-tag@^2.11.0: - version "2.11.0" - resolved "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.11.0.tgz" - integrity sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA== - -graphql-tag@^2.12.0: - version "2.12.3" - resolved "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.3.tgz" - integrity sha512-5wJMjSvj30yzdciEuk9dPuUBUR56AqDi3xncoYQl1i42pGdSqOJrJsdb/rz5BDoy+qoGvQwABcBeF0xXY3TrKw== +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-upload@^11.0.0: - version "11.0.0" - resolved "https://registry.npmjs.org/graphql-upload/-/graphql-upload-11.0.0.tgz" - integrity sha512-zsrDtu5gCbQFDWsNa5bMB4nf1LpKX9KDgh+f8oL1288ijV4RxeckhVozAjqjXAfRpxOHD1xOESsh6zq8SjdgjA== - dependencies: - busboy "^0.3.1" - fs-capacitor "^6.1.0" - http-errors "^1.7.3" - isobject "^4.0.0" - object-path "^0.11.4" +graphql-ws@5.11.3, graphql-ws@^5.11.3: + version "5.11.3" + resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.11.3.tgz#eaf8e6baf669d167975cff13ad86abca4ecfe82f" + integrity sha512-fU8zwSgAX2noXAsuFiCZ8BtXeXZOzXyK5u1LloCdacsVth4skdBMPO74EG51lBoWSIZ8beUocdpV8+cQHBODnQ== -graphql-ws@4.2.2: - version "4.2.2" - resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.2.2.tgz" - integrity sha512-b6TLtWLAmKunD72muL9EeItRGpio9+V3Cx4zJsBkRA+3wxzTWXDvQr9/3qSwJ3D/2abz0ys2KHTM6lB1uH7KIQ== - -graphql@^15.3.0, graphql@^15.4.0: - version "15.5.0" - resolved "https://registry.npmjs.org/graphql/-/graphql-15.5.0.tgz" - integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA== +"graphql@14 - 16", graphql@^16.6.0: + version "16.6.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.6.0.tgz#c2dcffa4649db149f6282af726c8c83f1c7c5fdb" + integrity sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw== hard-rejection@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-bigints@^1.0.0, has-bigints@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz" - integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== +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.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + 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.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz" - integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +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" @@ -3923,19 +4647,27 @@ has-tostringtag@^1.0.0: has@^1.0.3: version "1.0.3" - resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== dependencies: function-bind "^1.1.1" -hast-util-whitespace@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz" - integrity sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg== +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.npmjs.org/header-case/-/header-case-2.0.4.tgz" + resolved "https://registry.yarnpkg.com/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063" integrity sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q== dependencies: capital-case "^1.0.4" @@ -3943,7 +4675,7 @@ header-case@^2.0.4: history@^4.9.0: version "4.10.1" - resolved "https://registry.npmjs.org/history/-/history-4.10.1.tgz" + resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== dependencies: "@babel/runtime" "^7.1.2" @@ -3955,7 +4687,7 @@ history@^4.9.0: 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.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" + 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" @@ -3966,244 +4698,211 @@ hosted-git-info@^2.1.4: integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== hosted-git-info@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.1.tgz" - integrity sha512-eT7NrxAsppPRQEBSwKSosReE+v8OzABwEScQYk5d4uxaEPlzxTIku7LINXtBGalthkLhJnq5lBI89PfK43zAKg== + 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-tags@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz" - integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg== +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== -htmlparser2@^3.10.0: - version "3.10.1" - resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== +html-tags@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961" + integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg== + +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - -http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== - -http-errors@^1.7.3: - version "1.8.0" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz" - integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-proxy-agent@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== - dependencies: - "@tootallnate/once" "1" + "@tootallnate/once" "2" agent-base "6" debug "4" https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" debug "4" -i18n-iso-countries@^6.4.0: - version "6.6.0" - resolved "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-6.6.0.tgz" - integrity sha512-3r8nV9GR0mY4BiJbrOBYBhepqiiarLeWI7WKxYy2eWNBvU0Ozk0Qc4KTSfqsFKpapdnV3h9sukZdfOwhiqOU9w== +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.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + 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.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -ignore@^5.1.4, ignore@^5.1.8: - version "5.1.8" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== +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.npmjs.org/immediate/-/immediate-3.0.6.tgz" - integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + 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.npmjs.org/immutable/-/immutable-3.7.6.tgz" - integrity sha1-E7TTyxK++hVIKib+Gy665kAHHks= + 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.1.0, import-fresh@^3.2.1: +import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" + 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@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz" - integrity sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ== - dependencies: - resolve-from "^5.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.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz" + 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.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -indent-string@^3.0.0: - version "3.2.0" - resolved "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz" - integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= + 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.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz" - integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= - individual@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/individual/-/individual-2.0.0.tgz#833b097dad23294e76117a98fb38e0d9ad61bb97" - integrity sha1-gzsJfa0jKU52EXqY+zjg2a1hu5c= + integrity sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g== inflight@^1.0.4: version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + 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.4, inherits@^2.0.1, inherits@^2.0.3: +inherits@2, inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@^1.3.5, ini@~1.3.0: +ini@^1.3.5: version "1.3.8" - resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" + 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.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== -inquirer@^7.3.3: - version "7.3.3" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz" - integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== +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.0" + 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.19" + lodash "^4.17.21" mute-stream "0.0.8" + ora "^5.4.1" run-async "^2.4.0" - rxjs "^6.6.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: - version "1.0.3" - resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz" - integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== +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.1.0" + get-intrinsic "^1.2.0" has "^1.0.3" side-channel "^1.0.4" -intersection-observer@^0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.0.tgz" - integrity sha512-2Vkz8z46Dv401zTWudDGwO7KiGHNDkMv417T5ItcNYfmvHR/1qCTVBO9vwH8zZmQ0WkA/1ARwpysR9bsnop4NQ== +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.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-6.1.2.tgz" + 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@6.4.3: - version "6.4.3" - resolved "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-6.4.3.tgz" - integrity sha512-gpB7OeKDSd9wqjIQ7wVQM9byrpMlokGoUfJND7DS9SjoBbOsZIHAHw+lrmAWYmq+MI3WQUeLouSFdYAZ6zSX9A== - dependencies: - "@formatjs/ecma402-abstract" "1.6.3" - tslib "^2.1.0" - intl-messageformat-parser@^5.3.7: version "5.5.1" - resolved "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-5.5.1.tgz" + 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@9.5.3: - version "9.5.3" - resolved "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.5.3.tgz" - integrity sha512-Ei8vH41/icJsc16ZfWk1FzZ2SpaVn0gElXsQCKKPerxK/28m1gVdH0G26GuCqAyz5ETEJiSRn8sPMaSWJDuTjg== +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: - fast-memoize "^2.5.2" - intl-messageformat-parser "6.4.3" - tslib "^2.1.0" + "@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.npmjs.org/invariant/-/invariant-2.2.4.tgz" + 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.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz" + 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" @@ -4211,125 +4910,98 @@ is-absolute@^1.0.0: is-alphabetical@^1.0.0: version "1.0.4" - resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== -is-alphabetical@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.0.tgz" - integrity sha512-5OV8Toyq3oh4eq6sbWTYzlGdnMT/DPI5I0zxUBxjiigQsZycpkKF3kskkao3JyYGuYDHvhgJF+DrjMQp9SX86w== - is-alphanumerical@^1.0.0: version "1.0.4" - resolved "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz" + 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-alphanumerical@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" - integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== +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: - is-alphabetical "^2.0.0" - is-decimal "^2.0.0" + 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.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + 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.1" - resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz" - integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg== + 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.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + 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.0" - resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz" - integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA== + 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.0" + call-bind "^1.0.2" + has-tostringtag "^1.0.0" is-buffer@^2.0.0: version "2.0.5" - resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== -is-callable@^1.1.4, is-callable@^1.2.3: - version "1.2.3" - resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz" - integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== +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-callable@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" - integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== - -is-core-module@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz" - integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== +is-core-module@^2.11.0, is-core-module@^2.5.0, is-core-module@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== dependencies: has "^1.0.3" -is-core-module@^2.7.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548" - integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw== +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 "^1.0.3" - -is-core-module@^2.9.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" - integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== - dependencies: - has "^1.0.3" - -is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + has-tostringtag "^1.0.0" is-decimal@^1.0.0: version "1.0.4" - resolved "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== -is-decimal@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" - integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== - is-extglob@^2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + 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.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + 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: @@ -4337,14 +5009,7 @@ is-function@^1.0.1: 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.1, is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-glob@^4.0.3: +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== @@ -4353,75 +5018,62 @@ is-glob@^4.0.3: is-hexadecimal@^1.0.0: version "1.0.4" - resolved "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== -is-hexadecimal@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.0.tgz" - integrity sha512-vGOtYkiaxwIiR0+Ng/zNId+ZZehGfINwTzdrDqc6iubbnQWhnPuYymOzOKUDqa2cSl59yHnEh2h6MvRLQsyNug== +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.1: +is-lower-case@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/is-lower-case/-/is-lower-case-2.0.2.tgz" + 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-negative-zero@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz" - integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== +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.4" - resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz" - integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== + 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.npmjs.org/is-number/-/is-number-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-observable@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz" - integrity sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA== - dependencies: - symbol-observable "^1.1.0" +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.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz" - integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + 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.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz" + 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.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.0.0.tgz#06c0999fd7574edf5a906ba5644ad0feb3a84d22" - integrity sha512-NXRbBtUdBioI73y/HmOhogw/U5msYPC9DAtGkJXeFcFWSFZw0mCUsPxk/snTuJHzNKA8kLBK4rH97RMB1BfCXw== - -is-promise@4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz" - integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== - -is-promise@^2.1.0: - version "2.2.2" - resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz" - integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== - -is-regex@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz" - integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== - dependencies: - call-bind "^1.0.2" - has-symbols "^1.0.1" +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" @@ -4431,34 +5083,26 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-regexp@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz" - integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA== - is-relative@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz" + 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-shared-array-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" - integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== +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-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= +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: - version "1.0.5" - resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz" - integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== - -is-string@^1.0.7: +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== @@ -4466,80 +5110,104 @@ is-string@^1.0.7: has-tostringtag "^1.0.0" is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== + 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.1" + 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.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + 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.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz" + 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.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" + 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.1: +is-upper-case@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/is-upper-case/-/is-upper-case-2.0.2.tgz" + 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-weakref@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.1.tgz#842dba4ec17fa9ac9850df2d6efbc1737274f2a2" - integrity sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ== +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.0" + 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.npmjs.org/is-windows/-/is-windows-1.0.2.tgz" + 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.npmjs.org/isarray/-/isarray-0.0.1.tgz" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + 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.npmjs.org/isexe/-/isexe-2.0.0.tgz" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz" - integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA== + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== isomorphic-fetch@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== dependencies: node-fetch "^2.6.1" whatwg-fetch "^3.4.1" -isomorphic-ws@4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz" - integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== +isomorphic-ws@5.0.0, 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== -iterall@^1.2.1: - version "1.3.0" - resolved "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz" - integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== +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" @@ -4554,10 +5222,10 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" - integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== +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" @@ -4566,10 +5234,10 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= +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" @@ -4589,43 +5257,41 @@ json-schema-traverse@^1.0.0: 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 sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8= + 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.0" + 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 sha1-9M0L0KXo/h3yWq9boRiwmf2ZLVs= + 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.npmjs.org/json2mq/-/json2mq-0.2.0.tgz" - integrity sha1-tje9O6nqvhIsg+lyBIOusQ0skEo= + 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.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + 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: - version "2.2.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" - integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== - dependencies: - minimist "^1.2.5" +json5@^2.1.2, json5@^2.2.2: + 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" @@ -4636,38 +5302,32 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz" - integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= +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== -jsonwebtoken@^8.5.1: - version "8.5.1" - resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== +jsonwebtoken@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d" + integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw== dependencies: jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" + lodash "^4.17.21" ms "^2.1.1" - semver "^5.6.0" + semver "^7.3.8" -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.1.0: - version "3.2.0" - resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz" - integrity sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q== +"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.2" - object.assign "^4.1.2" + array-includes "^3.1.5" + object.assign "^4.1.3" jwa@^1.4.1: version "1.4.1" - resolved "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== dependencies: buffer-equal-constant-time "1.0.1" @@ -4676,7 +5336,7 @@ jwa@^1.4.1: jws@^3.2.2: version "3.2.2" - resolved "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== dependencies: jwa "^1.4.1" @@ -4687,50 +5347,31 @@ keycode@^2.2.0: resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff" integrity sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg== -keyv@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz" - integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== - dependencies: - json-buffer "3.0.0" - kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" - resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -kleur@^4.0.3: - version "4.1.4" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d" - integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA== - -known-css-properties@^0.21.0: - version "0.21.0" - resolved "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.21.0.tgz" - integrity sha512-sZLUnTqimCkvkgRS+kbPlYW5o8q5w1cu+uIisKpEWkj31I8mx8kNG162DwRav8Zirkva6N5uoFsm9kzK4mUXjw== +known-css-properties@^0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.26.0.tgz#008295115abddc045a9f4ed7e2a84dc8b3a77649" + integrity sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg== language-subtag-registry@~0.3.2: - version "0.3.21" - resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz" - integrity sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg== + 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: +language-tags@=1.0.5: version "1.0.5" - resolved "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz" - integrity sha1-0yHbxNowuovzAk4ED6XBRmH5GTo= + 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" -latest-version@5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz" - integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== - dependencies: - package-json "^6.3.0" - levn@^0.4.1: version "0.4.1" - resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + 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" @@ -4738,63 +5379,33 @@ levn@^0.4.1: lie@3.1.1: version "3.1.1" - resolved "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz" - integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4= + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== dependencies: immediate "~3.0.5" lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + 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== -listr-silent-renderer@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz" - integrity sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4= - -listr-update-renderer@^0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz" - integrity sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA== +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: - chalk "^1.1.3" - cli-truncate "^0.2.1" - elegant-spinner "^1.0.1" - figures "^1.7.0" - indent-string "^3.0.0" - log-symbols "^1.0.2" - log-update "^2.3.0" - strip-ansi "^3.0.1" - -listr-verbose-renderer@^0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz" - integrity sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw== - dependencies: - chalk "^2.4.1" - cli-cursor "^2.1.0" - date-fns "^1.27.2" - figures "^2.0.0" - -listr@^0.14.3: - version "0.14.3" - resolved "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz" - integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== - dependencies: - "@samverschueren/stream-to-observable" "^0.3.0" - is-observable "^1.1.0" - is-promise "^2.1.0" - is-stream "^1.1.0" - listr-silent-renderer "^1.1.1" - listr-update-renderer "^0.5.0" - listr-verbose-renderer "^0.5.0" - p-map "^2.0.0" - rxjs "^6.3.3" + 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.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz" + 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" @@ -4802,235 +5413,185 @@ load-json-file@^6.2.0: strip-bom "^4.0.0" type-fest "^0.6.0" -localforage@^1.9.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@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - locate-path@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== dependencies: p-locate "^4.1.0" -lodash-es@^4.17.14, lodash-es@^4.17.15, lodash-es@^4.17.20, lodash-es@^4.17.21: - version "4.17.21" - resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz" - integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== +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.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +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.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" - integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= - -lodash.get@^4: - version "4.4.2" - resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= - -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz" - integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz" - integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz" - integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz" - integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + 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.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + 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.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz" - integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= - lodash.pick@^4.4.0: version "4.4.0" - resolved "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz" - integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= + 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 sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= + integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@~4.17.20: +lodash@^4.17.20, lodash@^4.17.21, lodash@~4.17.0: version "4.17.21" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz" - integrity sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg= - dependencies: - chalk "^1.0.0" - -log-symbols@^4.0.0: +log-symbols@^4.0.0, log-symbols@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz" + 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@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz" - integrity sha1-iDKP19HOeTiykoN0bwsbwSayRwg= +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 "^3.0.0" - cli-cursor "^2.0.0" - wrap-ansi "^3.0.1" + 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.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz" + 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.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" + 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.1: +lower-case-first@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/lower-case-first/-/lower-case-first-2.0.2.tgz" + 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.1, lower-case@^2.0.2: +lower-case@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== dependencies: tslib "^2.0.3" -lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +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.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" + 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.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.7.1.tgz#d6df2c940bb19a01112a04ccc4ff44886a945305" - integrity sha512-pbrQwiMiq+MmI9bl7UjtPT3AK603PV9bogNlr83uC+X9IoxqL5E4k7kU7fMQ0dpRgxgeSMygqUa0IMLQNXLBNA== +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.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" + 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, make-error@^1.1.1: +make-error@^1.1.1: version "1.3.6" - resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" + 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.npmjs.org/map-cache/-/map-cache-0.2.2.tgz" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + 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.npmjs.org/map-obj/-/map-obj-1.0.1.tgz" - integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg== map-obj@^4.0.0: - version "4.2.0" - resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.2.0.tgz" - integrity sha512-NAq0fCmZYGz9UFEQyndp7sisrow4GroyGeKluyKC/chuITZsPyOyC1UJZPJlVFImhXdROIP5xqouRLThT3BbpQ== + 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.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz" + 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.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz" + resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -mdast-util-definitions@^5.0.0: - version "5.1.0" - resolved "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz" - integrity sha512-5hcR7FL2EuZ4q6lLMUK5w4lHT2H3vqL9quPvYZ/Ku5iifrirfMHiGdhxdXMUbUkDmz5I+TYMd7nbaxUhbQkfpQ== +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: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - unist-util-visit "^3.0.0" + unist-util-visit "^2.0.0" mdast-util-find-and-replace@^1.1.0: version "1.1.1" - resolved "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-1.1.1.tgz" + 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" @@ -5039,7 +5600,7 @@ mdast-util-find-and-replace@^1.1.0: mdast-util-from-markdown@^0.8.0: version "0.8.5" - resolved "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz" + 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" @@ -5048,27 +5609,9 @@ mdast-util-from-markdown@^0.8.0: parse-entities "^2.0.0" unist-util-stringify-position "^2.0.0" -mdast-util-from-markdown@^1.0.0: - version "1.0.4" - resolved "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.0.4.tgz" - integrity sha512-BlL42o885QO+6o43ceoc6KBdp/bi9oYyamj0hUbeu730yhP1WDC7m2XYSBfmQkOb0TdoHSAJ3de3SMqse69u+g== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - mdast-util-to-string "^3.1.0" - micromark "^3.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-decode-string "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - parse-entities "^3.0.0" - unist-util-stringify-position "^3.0.0" - uvu "^0.5.0" - mdast-util-gfm-autolink-literal@^0.1.0: version "0.1.3" - resolved "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-0.1.3.tgz" + 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" @@ -5077,14 +5620,14 @@ mdast-util-gfm-autolink-literal@^0.1.0: mdast-util-gfm-strikethrough@^0.2.0: version "0.2.3" - resolved "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-0.2.3.tgz" + 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.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-0.1.6.tgz" + 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" @@ -5092,14 +5635,14 @@ mdast-util-gfm-table@^0.1.0: mdast-util-gfm-task-list-item@^0.1.0: version "0.1.6" - resolved "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-0.1.6.tgz" + 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.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-0.1.2.tgz" + 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" @@ -5108,24 +5651,23 @@ mdast-util-gfm@^0.1.0: mdast-util-gfm-task-list-item "^0.1.0" mdast-util-to-markdown "^0.6.1" -mdast-util-to-hast@^11.0.0: - version "11.3.0" - resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-11.3.0.tgz" - integrity sha512-4o3Cli3hXPmm1LhB+6rqhfsIUBjnKFlIUZvudaermXB+4/KONdd/W4saWWkC+LBLbPMqhFSSTSRgafHsT5fVJw== +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/hast" "^2.0.0" "@types/mdast" "^3.0.0" - "@types/mdurl" "^1.0.0" - mdast-util-definitions "^5.0.0" + "@types/unist" "^2.0.0" + mdast-util-definitions "^4.0.0" mdurl "^1.0.0" - unist-builder "^3.0.0" - unist-util-generated "^2.0.0" - unist-util-position "^4.0.0" - unist-util-visit "^4.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.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz" + 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" @@ -5137,27 +5679,27 @@ mdast-util-to-markdown@^0.6.0, mdast-util-to-markdown@^0.6.1, mdast-util-to-mark mdast-util-to-string@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz#b8cfe6a713e1091cb5b728fc48885a4767f8b97b" integrity sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w== -mdast-util-to-string@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz" - integrity sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA== +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.npmjs.org/mdurl/-/mdurl-1.0.1.tgz" - integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== -memoize-one@^5.0.0: - version "5.1.1" - resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz" - integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== +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@^6.1.0: version "6.1.1" - resolved "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz" + resolved "https://registry.yarnpkg.com/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467" integrity sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg== dependencies: "@types/minimist" "^1.2.0" @@ -5174,7 +5716,7 @@ meow@^6.1.0: meow@^9.0.0: version "9.0.0" - resolved "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz" + resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ== dependencies: "@types/minimist" "^1.2.0" @@ -5190,69 +5732,52 @@ meow@^9.0.0: type-fest "^0.18.0" yargs-parser "^20.2.3" -merge2@^1.3.0: +merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" - resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromark-core-commonmark@^1.0.1: - version "1.0.4" - resolved "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.4.tgz" - integrity sha512-HAtoZisp1M/sQFuw2zoUKGo1pMKod7GSvdM6B2oBU0U2CEN5/C6Tmydmi1rmvEieEhGQsjMyiiSoYgxISNxGFA== - dependencies: - micromark-factory-destination "^1.0.0" - micromark-factory-label "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-factory-title "^1.0.0" - micromark-factory-whitespace "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-classify-character "^1.0.0" - micromark-util-html-tag-name "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - parse-entities "^3.0.0" - uvu "^0.5.0" +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== micromark-extension-gfm-autolink-literal@~0.5.0: - version "0.5.6" - resolved "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-0.5.6.tgz" - integrity sha512-nHbR1NUOVhmlZNsnhE5B7WJzL7Xd8lc888z4AF27IpHMtO3NstclZmbrMI+AcdTPpO1wuGVwlK1Cnq+n8Sxlrw== + 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.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-0.6.5.tgz" + 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.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-0.4.3.tgz" + 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.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-0.3.0.tgz" + 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.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-0.3.3.tgz" + 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.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-0.3.3.tgz" + 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" @@ -5262,266 +5787,82 @@ micromark-extension-gfm@^0.3.0: micromark-extension-gfm-tagfilter "~0.3.0" micromark-extension-gfm-task-list-item "~0.3.0" -micromark-factory-destination@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz" - integrity sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-label@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz" - integrity sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-factory-space@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz" - integrity sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-title@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz" - integrity sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-factory-whitespace@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz#e991e043ad376c1ba52f4e49858ce0794678621c" - integrity sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-character@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.1.0.tgz#d97c54d5742a0d9611a68ca0cd4124331f264d86" - integrity sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-chunked@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz#5b40d83f3d53b84c4c6bce30ed4257e9a4c79d06" - integrity sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g== - dependencies: - micromark-util-symbol "^1.0.0" - -micromark-util-classify-character@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz" - integrity sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-combine-extensions@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz" - integrity sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-decode-numeric-character-reference@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz" - integrity sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w== - dependencies: - micromark-util-symbol "^1.0.0" - -micromark-util-decode-string@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.1.tgz" - integrity sha512-Wf3H6jLaO3iIlHEvblESXaKAr72nK7JtBbLLICPwuZc3eJkMcp4j8rJ5Xv1VbQWMCWWDvKUbVUbE2MfQNznwTA== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-symbol "^1.0.0" - parse-entities "^3.0.0" - -micromark-util-encode@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.0.tgz" - integrity sha512-cJpFVM768h6zkd8qJ1LNRrITfY4gwFt+tziPcIf71Ui8yFzY9wG3snZQqiWVq93PG4Sw6YOtcNiKJfVIs9qfGg== - -micromark-util-html-tag-name@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.0.0.tgz" - integrity sha512-NenEKIshW2ZI/ERv9HtFNsrn3llSPZtY337LID/24WeLqMzeZhBEE6BQ0vS2ZBjshm5n40chKtJ3qjAbVV8S0g== - -micromark-util-normalize-identifier@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz" - integrity sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg== - dependencies: - micromark-util-symbol "^1.0.0" - -micromark-util-resolve-all@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz" - integrity sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw== - dependencies: - micromark-util-types "^1.0.0" - -micromark-util-sanitize-uri@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.0.0.tgz" - integrity sha512-cCxvBKlmac4rxCGx6ejlIviRaMKZc0fWm5HdCHEeDWRSkn44l6NdYVRyU+0nT1XC72EQJMZV8IPHF+jTr56lAg== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-symbol "^1.0.0" - -micromark-util-subtokenize@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz" - integrity sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-util-symbol@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.0.tgz" - integrity sha512-NZA01jHRNCt4KlOROn8/bGi6vvpEmlXld7EHcRH+aYWUfL3Wc8JLUNNlqUMKa0hhz6GrpUWsHtzPmKof57v0gQ== - -micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.1.tgz" - integrity sha512-UT0ylWEEy80RFYzK9pEaugTqaxoD/j0Y9WhHpSyitxd99zjoQz7JJ+iKuhPAgOW2MiPSUAx+c09dcqokeyaROA== - micromark@^2.11.3, micromark@~2.11.0, micromark@~2.11.3: version "2.11.4" - resolved "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz" + 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" -micromark@^3.0.0: - version "3.0.7" - resolved "https://registry.npmjs.org/micromark/-/micromark-3.0.7.tgz" - integrity sha512-67ipZ2CzQVsDyH1kqNLh7dLwe5QMPJwjFBGppW7JCLByaSc6ZufV0ywPOxt13MIDAzzmj3wctDL6Ov5w0fOHXw== +micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== dependencies: - "@types/debug" "^4.0.0" - debug "^4.0.0" - micromark-core-commonmark "^1.0.1" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - parse-entities "^3.0.0" - uvu "^0.5.0" + braces "^3.0.2" + picomatch "^2.3.1" -micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== - dependencies: - braces "^3.0.1" - picomatch "^2.0.5" - -mime-db@1.46.0: - version "1.46.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz" - integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== mime-types@^2.1.12: - version "2.1.29" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz" - integrity sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ== + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: - mime-db "1.46.0" - -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== + mime-db "1.52.0" mimic-fn@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-response@^1.0.0, mimic-response@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - min-document@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" - integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU= + integrity sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ== dependencies: dom-walk "^0.1.0" min-indent@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -mini-create-react-context@^0.4.0: - version "0.4.1" - resolved "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz" - integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ== +minimatch@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" + integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== dependencies: - "@babel/runtime" "^7.12.1" - tiny-warning "^1.0.3" + brace-expansion "^1.1.7" -minimatch@3.0.4, minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== +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" minimist-options@4.1.0, minimist-options@^4.0.2: version "4.1.0" - resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz" + 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.5: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +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, mkdirp@^1.0.4: +mkdirp@^1.0.3: version "1.0.4" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== moment@~2.29.1: @@ -5531,47 +5872,37 @@ moment@~2.29.1: mousetrap-pause@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/mousetrap-pause/-/mousetrap-pause-1.0.0.tgz" - integrity sha1-kcQp8vX5rXFQj6BWG7U74/35qdA= + 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.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz" + resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA== -mpd-parser@0.21.1: - version "0.21.1" - resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.21.1.tgz#4f4834074ed0a8e265d8b04a5d2d7b5045a4fa55" - integrity sha512-BxlSXWbKE1n7eyEPBnTEkrzhS3PdmkkKdM1pgKbPnPOH0WFZIc0sPOWi7m0Uo3Wd2a4Or8Qf4ZbS7+ASqQ49fw== +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.7.2" + "@xmldom/xmldom" "^0.8.3" global "^4.4.0" -mri@^1.1.0: - version "1.2.0" - resolved "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz" - integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - ms@2.1.2: version "2.1.2" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + 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.npmjs.org/ms/-/ms-2.1.3.tgz" + 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.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== mux.js@6.0.1: @@ -5582,47 +5913,56 @@ mux.js@6.0.1: "@babel/runtime" "^7.11.2" global "^4.4.0" -nanoclone@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz" - integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== - nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +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.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + 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.npmjs.org/no-case/-/no-case-3.0.4.tgz" + 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.1, node-fetch@^2.6.1: - version "2.6.1" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +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.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" - integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== -node-releases@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" - integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== +node-releases@^2.0.8: + version "2.0.10" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" + integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== normalize-package-data@^2.5.0: version "2.5.0" - resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz" + 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" @@ -5631,42 +5971,27 @@ normalize-package-data@^2.5.0: validate-npm-package-license "^3.0.1" normalize-package-data@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.2.tgz" - integrity sha512-6CdZocmfGaKnIHPVFhJJZ3GuR8SsLKvDANFp47Jmy51aKIr8akjAWTSxtpI+MBgBFdSMRyo4hMpDlT6dTffgZg== + 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" - resolve "^1.20.0" + 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.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + 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.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" - integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= - -normalize-selector@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/normalize-selector/-/normalize-selector-0.2.0.tgz" - integrity sha1-0LFF62kRicY6eNIB3E/bEpPvDAM= - -normalize-url@^4.1.0: - version "4.5.0" - resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz" - integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== - normalize-url@^4.5.1: version "4.5.1" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" @@ -5674,132 +5999,102 @@ normalize-url@^4.5.1: nullthrows@^1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== -num2fraction@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz" - integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.11.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" - integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== +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-inspect@^1.9.0: - version "1.9.0" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz" - integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== +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.0.12, object-keys@^1.1.1: +object-keys@^1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object-path@^0.11.4: - version "0.11.8" - resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.8.tgz#ed002c02bbdd0070b78a27455e8ae01fc14d4742" - integrity sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA== - -object.assign@^4.1.0, object.assign@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz" - integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== +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.0" - define-properties "^1.1.3" - has-symbols "^1.0.1" + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.2: - version "1.1.3" - resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.3.tgz" - integrity sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - has "^1.0.3" - -object.entries@^1.1.4: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" - integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== +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.3" - es-abstract "^1.19.1" + define-properties "^1.1.4" + es-abstract "^1.20.4" -object.fromentries@^2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.4.tgz" - integrity sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ== +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.3" - es-abstract "^1.18.0-next.2" - has "^1.0.3" + define-properties "^1.1.4" + es-abstract "^1.20.4" -object.hasown@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.0.tgz#7232ed266f34d197d15cac5880232f7a4790afe5" - integrity sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg== +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.3" - es-abstract "^1.19.1" + define-properties "^1.1.4" + es-abstract "^1.20.4" -object.values@^1.1.4, object.values@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" - integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== +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.3" - es-abstract "^1.19.1" + define-properties "^1.1.4" + es-abstract "^1.20.4" -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.0: version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz" - integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= - dependencies: - mimic-fn "^1.0.0" - onetime@^5.1.0: version "5.1.2" - resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" + 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.14.0: - version "0.14.1" - resolved "https://registry.npmjs.org/optimism/-/optimism-0.14.1.tgz" - integrity sha512-7+1lSN+LJEtaj3uBLLFk8uFCFKy3txLvcvln5Dh1szXjF9yghEMeWclmnk0qdtYZ+lcMNyu48RmQQRw+LRYKSQ== +optimism@^0.16.1: + version "0.16.2" + resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.16.2.tgz#519b0c78b3b30954baed0defe5143de7776bf081" + integrity sha512-zWNbgWj+3vLEjZNIh/okkY2EUfX+vB9TJopzIZwT1xxaMqC5hRLLraePod4c5n4He08xuXNH+zhKFFCu390wiQ== dependencies: - "@wry/context" "^0.5.2" - "@wry/trie" "^0.2.1" + "@wry/context" "^0.7.0" + "@wry/trie" "^0.3.0" optionator@^0.9.1: version "0.9.1" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== dependencies: deep-is "^0.1.3" @@ -5809,86 +6104,69 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" -original@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/original/-/original-1.0.2.tgz" - integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== +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: - url-parse "^1.4.3" + 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.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== -p-cancelable@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz" - integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== - -p-limit@3.1.0: +p-limit@3.1.0, p-limit@^3.0.2: version "3.1.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + 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@^1.1.0: - version "1.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - p-limit@^2.2.0: version "2.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" + 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@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - p-locate@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" + 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-map@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== +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-try@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= +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.npmjs.org/p-try/-/p-try-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -package-json@^6.3.0: - version "6.5.0" - resolved "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz" - integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== - dependencies: - got "^9.6.0" - registry-auth-token "^4.0.0" - registry-url "^5.0.0" - semver "^6.2.0" - param-case@^3.0.4: version "3.0.4" - resolved "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== dependencies: dot-case "^3.0.4" @@ -5896,14 +6174,14 @@ param-case@^3.0.4: parent-module@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + 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.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz" + 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" @@ -5913,22 +6191,10 @@ parse-entities@^2.0.0: is-decimal "^1.0.0" is-hexadecimal "^1.0.0" -parse-entities@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-3.0.0.tgz" - integrity sha512-AJlcIFDNPEP33KyJLguv0xJc83BNvjxwpuUIcetyXUsLpVXAUCePJ5kIoYtEN2R1ac0cYaRu/vk9dVFkewHQhQ== - dependencies: - character-entities "^2.0.0" - character-entities-legacy "^2.0.0" - character-reference-invalid "^2.0.0" - is-alphanumerical "^2.0.0" - is-decimal "^2.0.0" - is-hexadecimal "^2.0.0" - parse-filepath@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz" - integrity sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE= + 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" @@ -5936,7 +6202,7 @@ parse-filepath@^1.0.2: parse-json@^5.0.0: version "5.2.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" + 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" @@ -5944,69 +6210,69 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -pascal-case@^3.1.1, pascal-case@^3.1.2: +pascal-case@^3.1.2: version "3.1.2" - resolved "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz" + 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.npmjs.org/path-case/-/path-case-3.0.4.tgz" + 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@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - path-exists@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + 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.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + 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.npmjs.org/path-key/-/path-key-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6, path-parse@^1.0.7: +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.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz" - integrity sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0= + 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.npmjs.org/path-root/-/path-root-0.1.1.tgz" - integrity sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc= + 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.8.0" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== dependencies: isarray "0.0.1" path-type@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== picocolors@^1.0.0: @@ -6014,14 +6280,14 @@ picocolors@^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.0.5, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +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.npmjs.org/pify/-/pify-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== pkcs7@^1.0.4: @@ -6031,107 +6297,48 @@ pkcs7@^1.0.4: dependencies: "@babel/runtime" "^7.5.5" -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz" - integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= - dependencies: - find-up "^2.1.0" - -postcss-html@^0.36.0: - version "0.36.0" - resolved "https://registry.npmjs.org/postcss-html/-/postcss-html-0.36.0.tgz" - integrity sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw== - dependencies: - htmlparser2 "^3.10.0" - -postcss-less@^3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-3.1.4.tgz#369f58642b5928ef898ffbc1a6e93c958304c5ad" - integrity sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA== - dependencies: - postcss "^7.0.14" - postcss-media-query-parser@^0.2.3: version "0.2.3" - resolved "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz" - integrity sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ= + resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" + integrity sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig== postcss-resolve-nested-selector@^0.1.1: version "0.1.1" - resolved "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz" - integrity sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4= + resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" + integrity sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw== -postcss-safe-parser@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz" - integrity sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g== - dependencies: - postcss "^7.0.26" +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-safe-parser@^5.0.2: - version "5.0.2" - resolved "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-5.0.2.tgz" - integrity sha512-jDUfCPJbKOABhwpUKcqCVbbXiloe/QXMcbJ6Iipf3sDIihEzTqRCeMBfRaOHxhBuTYqtASrI1KJWxzztZU4qUQ== - dependencies: - postcss "^8.1.0" +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-sass@^0.4.4: - version "0.4.4" - resolved "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.4.4.tgz" - integrity sha512-BYxnVYx4mQooOhr+zer0qWbSPYnarAy8ZT7hAQtbxtgVf8gy+LSLT/hHGe35h14/pZDTw1DsxdbrwxBN++H+fg== - dependencies: - gonzales-pe "^4.3.0" - postcss "^7.0.21" - -postcss-scss@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/postcss-scss/-/postcss-scss-2.1.1.tgz" - integrity sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA== - dependencies: - postcss "^7.0.6" - -postcss-selector-parser@^6.0.4: - version "6.0.4" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz" - integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== +postcss-selector-parser@^6.0.11: + version "6.0.11" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" + integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== dependencies: cssesc "^3.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" util-deprecate "^1.0.2" -postcss-sorting@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-5.0.1.tgz" - integrity sha512-Y9fUFkIhfrm6i0Ta3n+89j56EFqaNRdUKqXyRp6kvTcSXnmgEjaVowCXH+JBe9+YKWqd4nc28r2sgwnzJalccA== - dependencies: - lodash "^4.17.14" - postcss "^7.0.17" +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-syntax@^0.36.2: - version "0.36.2" - resolved "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz" - integrity sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w== +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-value-parser@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz" - integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== - -postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.31, postcss@^7.0.32, postcss@^7.0.35, postcss@^7.0.6: - version "7.0.35" - resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz" - integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -postcss@^8.1.0, postcss@^8.2.10, postcss@^8.4.13: - version "8.4.16" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c" - integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ== +postcss@^8.4.21: + version "8.4.21" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" + integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== dependencies: nanoid "^3.3.4" picocolors "^1.0.0" @@ -6139,140 +6346,137 @@ postcss@^8.1.0, postcss@^8.2.10, postcss@^8.4.13: prelude-ls@^1.2.1: version "1.2.1" - resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz" - integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= - -prettier@2.2.1: - version "2.2.1" - resolved "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz" - integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== +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 sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -progress@^2.0.0: - version "2.0.3" - resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== promise@^7.1.1: version "7.3.1" - resolved "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz" + 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.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz" + 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.0.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: +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.npmjs.org/prop-types/-/prop-types-15.7.2.tgz" + 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.4: - version "2.0.4" - resolved "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz" - integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg== +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@^6.0.0: - version "6.1.1" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.1.1.tgz#5ca85510a3019726cb9afed4197b7b8ac5926a22" - integrity sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w== +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" proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" +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.1.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== -query-string@6.13.8: - version "6.13.8" - resolved "https://registry.npmjs.org/query-string/-/query-string-6.13.8.tgz" - integrity sha512-jxJzQI2edQPE/NPUOusNjO/ZOGqr1o2OBa/3M00fU76FsLXDVbJDv/p7ng5OdQyorKrkRz1oqfwmbe5MAMePQg== +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: - decode-uri-component "^0.2.0" - split-on-first "^1.0.0" - strict-uri-encode "^2.0.0" + tslib "^2.4.0" -querystringify@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" - integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== +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.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + 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.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== -rc@^1.2.8: - version "1.2.8" - resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== +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: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -react-bootstrap@1.4.3: - version "1.4.3" - resolved "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.4.3.tgz" - integrity sha512-4tYhk26KRnK0myMEp2wvNjOvnHMwWfa6pWFIiCtj9wewYaTxP7TrCf7MwcIMBgUzyX0SJXx6UbbDG0+hObiXNg== - dependencies: - "@babel/runtime" "^7.4.2" + "@babel/runtime" "^7.14.0" "@restart/context" "^2.1.4" - "@restart/hooks" "^0.3.21" - "@types/classnames" "^2.2.10" + "@restart/hooks" "^0.4.7" "@types/invariant" "^2.2.33" "@types/prop-types" "^15.7.3" - "@types/react" ">=16.9.35" - "@types/react-transition-group" "^4.4.0" + "@types/react" ">=16.14.8" + "@types/react-transition-group" "^4.4.1" "@types/warning" "^3.0.0" - classnames "^2.2.6" - dom-helpers "^5.1.2" + 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 "^4.1.0" + react-overlays "^5.1.2" react-transition-group "^4.4.1" - uncontrollable "^7.0.0" + uncontrollable "^7.2.1" warning "^4.0.3" -react-dom@17.0.2: +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== @@ -6283,9 +6487,14 @@ react-dom@17.0.2: react-fast-compare@^2.0.1: version "2.0.4" - resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz" + 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: + 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-fast-compare@^3.1.1: version "3.2.0" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" @@ -6301,160 +6510,155 @@ react-helmet@^6.1.0: react-fast-compare "^3.1.1" react-side-effect "^2.1.0" -react-input-autosize@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz" - integrity sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg== +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: - prop-types "^15.5.8" - -react-intl@^5.10.16: - version "5.13.5" - resolved "https://registry.npmjs.org/react-intl/-/react-intl-5.13.5.tgz" - integrity sha512-Ym6knnC04k070vwe3UDcRHQUDE2rGn1PNfmYNhDHVPL6vbusuFbefjnt8ZC1GEjnfo29WUHn/tkGd9SMudzD+g== - dependencies: - "@formatjs/ecma402-abstract" "1.6.3" - "@formatjs/intl" "1.8.4" - "@formatjs/intl-displaynames" "4.0.11" - "@formatjs/intl-listformat" "5.0.12" + "@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 "9.5.3" - intl-messageformat-parser "6.4.3" - tslib "^2.1.0" + intl-messageformat "10.3.0" + tslib "^2.4.0" -react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: +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.npmjs.org/react-is/-/react-is-16.13.1.tgz" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.0: - version "17.0.2" - resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - react-lifecycles-compat@^3.0.4: version "3.0.4" - resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== -react-markdown@^7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/react-markdown/-/react-markdown-7.1.0.tgz" - integrity sha512-hL8cLLkTydyoKlZWZOjlU6TvMYIw9qKLh1CCzVfnhKt/R/SnKVAqiyugetXY61CkjhBqJl2C5FdU3OwHYo7SrQ== - dependencies: - "@types/hast" "^2.0.0" - "@types/unist" "^2.0.0" - comma-separated-tokens "^2.0.0" - hast-util-whitespace "^2.0.0" - prop-types "^15.0.0" - property-information "^6.0.0" - react-is "^17.0.0" - remark-parse "^10.0.0" - remark-rehype "^9.0.0" - space-separated-tokens "^2.0.0" - style-to-object "^0.3.0" - unified "^10.0.0" - unist-util-visit "^4.0.0" - vfile "^5.0.0" +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@^4.1.0: - version "4.1.1" - resolved "https://registry.npmjs.org/react-overlays/-/react-overlays-4.1.1.tgz" - integrity sha512-WtJifh081e6M24KnvTQoNjQEpz7HoLxqt8TwZM7LOYIkYJ8i/Ly1Xi7RVte87ZVnmqQ4PFaFiNHZhSINPSpdBQ== +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.12.1" - "@popperjs/core" "^2.5.3" - "@restart/hooks" "^0.3.25" + "@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.0.0" + 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.npmjs.org/react-router-bootstrap/-/react-router-bootstrap-0.25.0.tgz" + 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.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz" - integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== +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.1.2" + "@babel/runtime" "^7.12.13" history "^4.9.0" loose-envify "^1.3.1" prop-types "^15.6.2" - react-router "5.2.0" + react-router "5.3.4" tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router-hash-link@^2.3.1: - version "2.4.0" - resolved "https://registry.npmjs.org/react-router-hash-link/-/react-router-hash-link-2.4.0.tgz" - integrity sha512-HGbB9kfODHKsHvMVsPbqDr057V4xg4TNNRaQcezsFMKitwHaaU51cM2+gDyX45y9YLLPbovELz2rpNx2C3Frng== +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.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz" - integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw== +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.1.2" + "@babel/runtime" "^7.12.13" history "^4.9.0" hoist-non-react-statics "^3.1.0" loose-envify "^1.3.1" - mini-create-react-context "^0.4.0" 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@^4.0.2: - version "4.3.0" - resolved "https://registry.npmjs.org/react-select/-/react-select-4.3.0.tgz" - integrity sha512-SBPD1a3TJqE9zoI/jfOLCAoLr/neluaeokjOixr3zZ1vHezkom8K0A9J4QG9IWDqIDE9K/Mv+0y1GjidC2PDtQ== +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.0.0" - "@emotion/react" "^11.1.1" - memoize-one "^5.0.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-input-autosize "^3.0.0" react-transition-group "^4.3.0" + use-isomorphic-layout-effect "^1.1.2" react-side-effect@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.1.tgz#66c5701c3e7560ab4822a4ee2742dee215d72eb3" - integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ== - -react-slick@^0.29.0: - version "0.29.0" - resolved "https://registry.npmjs.org/react-slick/-/react-slick-0.29.0.tgz" - integrity sha512-TGdOKE+ZkJHHeC4aaoH85m8RnFyWqdqRfAGkhd6dirmATXMZWAxOpTLmw2Ll/jPTQ3eEG7ercFr/sbzdeYCJXA== - dependencies: - classnames "^2.2.5" - enquire.js "^2.1.6" - json2mq "^0.2.0" - lodash.debounce "^4.0.8" - resize-observer-polyfill "^1.5.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.1" - resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz" - integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== + 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: +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== @@ -6464,7 +6668,7 @@ react@17.0.2: read-babelrc-up@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/read-babelrc-up/-/read-babelrc-up-1.1.0.tgz" + 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" @@ -6472,7 +6676,7 @@ read-babelrc-up@^1.1.0: read-pkg-up@^7.0.1: version "7.0.1" - resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz" + 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" @@ -6481,7 +6685,7 @@ read-pkg-up@^7.0.1: read-pkg@^5.2.0: version "5.2.0" - resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz" + 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" @@ -6489,218 +6693,182 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^3.1.1: +readable-stream@^3.4.0: version "3.6.0" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz" + 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.5.0: - version "3.5.0" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== +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" -recrawl-sync@^2.0.3: - version "2.2.1" - resolved "https://registry.yarnpkg.com/recrawl-sync/-/recrawl-sync-2.2.1.tgz#cb02c8084c22b3cea103abf46bb88734076ed6bb" - integrity sha512-A2yLDgeXNaduJJMlqyUdIN7fewopnNm/mVeeGytS1d2HLXKpS5EthQ0j8tWeX+as9UXiiwQRwfoslKC+/gjqxg== - dependencies: - "@cush/relative" "^1.0.0" - glob-regex "^0.3.0" - slash "^3.0.0" - tslib "^1.9.3" - redent@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz" + 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" -regenerator-runtime@^0.13.4: - version "0.13.7" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz" - integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== +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" -regexp.prototype.flags@^1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz" - integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== +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-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.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz" - integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== +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== -registry-auth-token@^4.0.0: - version "4.2.1" - resolved "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz" - integrity sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw== +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: - rc "^1.2.8" + "@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" -registry-url@^5.0.0: - version "5.1.0" - resolved "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz" - integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== +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: - rc "^1.2.8" + jsesc "~0.5.0" -relay-compiler@10.1.0: - version "10.1.0" - resolved "https://registry.npmjs.org/relay-compiler/-/relay-compiler-10.1.0.tgz" - integrity sha512-HPqc3N3tNgEgUH5+lTr5lnLbgnsZMt+MRiyS0uAVNhuPY2It0X1ZJG+9qdA3L9IqKFUNwVn6zTO7RArjMZbARQ== +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: - "@babel/core" "^7.0.0" - "@babel/generator" "^7.5.0" - "@babel/parser" "^7.0.0" - "@babel/runtime" "^7.0.0" - "@babel/traverse" "^7.0.0" - "@babel/types" "^7.0.0" - babel-preset-fbjs "^3.3.0" - chalk "^4.0.0" - fb-watchman "^2.0.0" - fbjs "^3.0.0" - glob "^7.1.1" - immutable "~3.7.6" - nullthrows "^1.1.1" - relay-runtime "10.1.0" - signedsource "^1.0.0" - yargs "^15.3.1" + "@mapbox/hast-util-table-cell-style" "^0.2.0" + hast-to-hyperscript "^9.0.0" -relay-runtime@10.1.0: - version "10.1.0" - resolved "https://registry.npmjs.org/relay-runtime/-/relay-runtime-10.1.0.tgz" - integrity sha512-bxznLnQ1ST6APN/cFi7l0FpjbZVchWQjjhj9mAuJBuUqNNCh9uV+UTRhpQF7Q8ycsPp19LHTpVyGhYb0ustuRQ== +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.npmjs.org/remark-gfm/-/remark-gfm-1.0.0.tgz" + 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@^10.0.0: - version "10.0.0" - resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.0.tgz" - integrity sha512-07ei47p2Xl7Bqbn9H2VYQYirnAFJPwdMuypdozWsSbnmrkgA2e2sZLZdnDNrrsxR4onmIzH/J6KXqKxCuqHtPQ== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-from-markdown "^1.0.0" - unified "^10.0.0" - remark-parse@^9.0.0: version "9.0.0" - resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz" + 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@^9.0.0: - version "9.1.0" - resolved "https://registry.npmjs.org/remark-rehype/-/remark-rehype-9.1.0.tgz" - integrity sha512-oLa6YmgAYg19zb0ZrBACh40hpBLteYROaPLhBXzLgjqyHQrN+gVP9N/FJvfzuNNuzCutktkroXEZBrxAxKhh7Q== +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: - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-to-hast "^11.0.0" - unified "^10.0.0" - -remark-stringify@^9.0.0: - version "9.0.1" - resolved "https://registry.npmjs.org/remark-stringify/-/remark-stringify-9.0.1.tgz" - integrity sha512-mWmNg3ZtESvZS8fv5PTvaPckdL4iNlCHTt8/e/8oN08nArHRHjNZMKzA/YW3+p7/lYqIw4nx1XsjCBo/AxNChg== - dependencies: - mdast-util-to-markdown "^0.6.0" - -remark@^13.0.0: - version "13.0.0" - resolved "https://registry.npmjs.org/remark/-/remark-13.0.0.tgz" - integrity sha512-HDz1+IKGtOyWN+QgBiAT0kn+2s6ovOxHyPAFGKVE81VSzJ+mq7RwHFledEvB5F1p4iJvOah/LOKdFuzvRnNLCA== - dependencies: - remark-parse "^9.0.0" - remark-stringify "^9.0.0" - unified "^9.1.0" + mdast-util-to-hast "^10.2.0" remedial@^1.0.7: version "1.0.8" - resolved "https://registry.npmjs.org/remedial/-/remedial-1.0.8.tgz" + 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.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + 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.npmjs.org/remove-trailing-spaces/-/remove-trailing-spaces-1.0.8.tgz" + 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.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -replaceall@^0.1.6: - version "0.1.6" - resolved "https://registry.npmjs.org/replaceall/-/replaceall-0.1.6.tgz" - integrity sha1-gdgax663LX9cSUKt8ml6MiBojY4= + 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.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + 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.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" + 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.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= - resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1: version "1.5.1" - resolved "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz" + 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.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" + 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.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + 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.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz" + 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.20.0, resolve@^1.22.0: +resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.1: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -6709,32 +6877,23 @@ resolve@^1.10.0, resolve@^1.20.0, resolve@^1.22.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^2.0.0-next.3: - version "2.0.0-next.3" - resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz" - integrity sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q== +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.2.0" - path-parse "^1.0.6" + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" -responselike@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz" - integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= - dependencies: - lowercase-keys "^1.0.0" - -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz" - integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= - dependencies: - onetime "^2.0.0" - signal-exit "^3.0.2" +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.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== dependencies: onetime "^5.1.0" @@ -6742,31 +6901,36 @@ restore-cursor@^3.1.0: reusify@^1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" + 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.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" -rollup@^2.59.0: - version "2.61.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.61.1.tgz#1a5491f84543cf9e4caf6c61222d9a3f8f2ba454" - integrity sha512-BbTXlEvB8d+XFbK/7E5doIcRtxWPRiqr0eb5vQ0+2paMM04Ye4PZY5nHOQef2ix24l/L0SpLd5hwcH15QHPdvA== +rollup@^3.10.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.15.0.tgz#6f4105e8c4b8145229657b74ad660b02fbfacc05" + integrity sha512-F9hrCAhnp5/zx/7HYmftvsNBkMfLfk/dXUh73hPSM2E3CRgap65orDNJbLetoiUFwSAk6iHPLvBrZ5iHYvzqsg== optionalDependencies: fsevents "~2.3.2" run-async@^2.4.0: version "2.4.1" - resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" + 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.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + 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" @@ -6774,73 +6938,78 @@ run-parallel@^1.1.9: rust-result@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/rust-result/-/rust-result-1.0.0.tgz#34c75b2e6dc39fe5875e5bdec85b5e0f91536f72" - integrity sha1-NMdbLm3Dn+WHXlveyFteD5FTb3I= + integrity sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA== dependencies: individual "^2.0.0" -rxjs@^6.3.3, rxjs@^6.6.0: - version "6.6.6" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.6.tgz" - integrity sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg== +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 "^1.9.0" - -sade@^1.7.3: - version "1.7.4" - resolved "https://registry.npmjs.org/sade/-/sade-1.7.4.tgz" - integrity sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA== - dependencies: - mri "^1.1.0" + tslib "^2.1.0" safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - 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 sha1-fA9XjPzNEtM6ccDgVBPi7KFx6qw= + 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.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass@^1.32.5: - version "1.32.8" - resolved "https://registry.npmjs.org/sass/-/sass-1.32.8.tgz" - integrity sha512-Sl6mIeGpzjIUZqvKnKETfMf0iDAswD9TNlv13A7aAF3XZlRPMq4VvJWBC2N2DXbp94MQVdNSFG6LfF/iOXrPHQ== +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 ">=2.0.0 <4.0.0" + 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.npmjs.org/scheduler/-/scheduler-0.20.2.tgz" + 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 "3.0.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz" - integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA== + 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.6" - ajv "^6.12.5" - ajv-keywords "^3.5.2" + "@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.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz" + 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" @@ -6849,29 +7018,29 @@ schema-utils@^2.6.6: scuid@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz" + 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", semver@^5.6.0: +"semver@2 || 3 || 4 || 5": version "5.7.1" - resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.2.1, semver@^7.3.4, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== +semver@^7.3.4, semver@^7.3.7, semver@^7.3.8: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== dependencies: lru-cache "^6.0.0" sentence-case@^3.0.4: version "3.0.4" - resolved "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz" + 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" @@ -6880,63 +7049,67 @@ sentence-case@^3.0.4: set-blocking@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + 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.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + 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.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + 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.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + 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.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" + 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.3" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +signal-exit@^3.0.2, signal-exit@^3.0.7: + 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== signedsource@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/signedsource/-/signedsource-1.0.0.tgz" - integrity sha1-HdrOSYF5j5O9gzlzgD2A1S6TrWo= + 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.npmjs.org/slash/-/slash-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@0.0.4: - version "0.0.4" - resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz" - integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= +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.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== dependencies: ansi-styles "^4.0.0" @@ -6950,7 +7123,7 @@ slick-carousel@^1.8.1: snake-case@^3.0.4: version "3.0.4" - resolved "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz" + 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" @@ -6958,42 +7131,42 @@ snake-case@^3.0.4: sort-keys@^4.0.0: version "4.2.0" - resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-4.2.0.tgz" + 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@^1.0.2: +"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.17: - version "0.5.19" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== +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.0: +source-map@^0.5.7: version "0.5.7" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== -source-map@^0.6.0, source-map@^0.6.1: +source-map@^0.6.0: version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -space-separated-tokens@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz" - integrity sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw== +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.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== dependencies: spdx-expression-parse "^3.0.0" @@ -7001,101 +7174,57 @@ spdx-correct@^3.0.0: spdx-exceptions@^2.1.0: version "2.3.0" - resolved "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz" + 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.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz" + 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.7" - resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz" - integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ== + 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== -specificity@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz" - integrity sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg== - -split-on-first@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz" - integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== - -sponge-case@^1.0.0: +sponge-case@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/sponge-case/-/sponge-case-1.0.1.tgz" + 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.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -sse-z@0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/sse-z/-/sse-z-0.3.0.tgz" - integrity sha512-jfcXynl9oAOS9YJ7iqS2JMUEHOlvrRAD+54CENiWnc4xsuVLQVSgmwf7cwOTcBd/uq3XkQKBGojgvEtVXcJ/8w== +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" -"statuses@>= 1.5.0 < 2": - version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -streamsearch@0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz" - integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= - -strict-uri-encode@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz" - integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= +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.npmjs.org/string-convert/-/string-convert-0.2.1.tgz" - integrity sha1-aYLMMEn7tM2F+LJFaLnZvznu/5c= + 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, string-env-interpolation@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz#ad4397ae4ac53fe6c91d1402ad6f6a52862c7152" integrity sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg== -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -string-width@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2: - version "4.2.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz" - integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string-width@^4.2.3: +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== @@ -7104,77 +7233,58 @@ string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string.prototype.matchall@^4.0.5: - version "4.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz#5abb5dabc94c7b0ea2380f65ba610b3a544b15fa" - integrity sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg== +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.3" - es-abstract "^1.19.1" - get-intrinsic "^1.1.1" - has-symbols "^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.3.1" + regexp.prototype.flags "^1.4.3" side-channel "^1.0.4" -string.prototype.replaceall@^1.0.4: - version "1.0.5" - resolved "https://registry.npmjs.org/string.prototype.replaceall/-/string.prototype.replaceall-1.0.5.tgz" - integrity sha512-YUjdWElI9pgKo7mrPOMKHFZxcAa0v1uqoJkMHtlJW63rMkPLkQH71ao2XNkKY2ksHKHC8ZUFwNjN9Vry+QyCvg== +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.3" - es-abstract "^1.18.0-next.2" - get-intrinsic "^1.1.1" - has-symbols "^1.0.1" - is-regex "^1.1.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.4: - version "1.0.4" - resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz" - integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== +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.3" + define-properties "^1.1.4" + es-abstract "^1.20.4" -string.prototype.trimstart@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz" - integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== +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.3" + define-properties "^1.1.4" + es-abstract "^1.20.4" string_decoder@^1.1.1: version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + 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@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -strip-ansi@^6.0.1: +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== @@ -7183,160 +7293,121 @@ strip-ansi@^6.0.1: strip-bom@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + 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.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" + 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.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz" + 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-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - style-search@^0.1.0: version "0.1.0" - resolved "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz" - integrity sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI= + 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.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz" + 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-config-prettier@^8.0.2: - version "8.0.2" - resolved "https://registry.npmjs.org/stylelint-config-prettier/-/stylelint-config-prettier-8.0.2.tgz" - integrity sha512-TN1l93iVTXpF9NJstlvP7nOu9zY2k+mN0NSFQ/VEGz15ZIP9ohdDZTtCWHs5LjctAhSAzaILULGbgiM0ItId3A== - -stylelint-order@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/stylelint-order/-/stylelint-order-4.1.0.tgz" - integrity sha512-sVTikaDvMqg2aJjh4r48jsdfmqLT+nqB1MOsaBnvM3OwLx4S+WXcsxsgk5w18h/OZoxZCxuyXMh61iBHcj9Qiw== +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: - lodash "^4.17.15" - postcss "^7.0.31" - postcss-sorting "^5.0.1" + postcss "^8.4.21" + postcss-sorting "^8.0.1" -stylelint@^13.9.0: - version "13.12.0" - resolved "https://registry.npmjs.org/stylelint/-/stylelint-13.12.0.tgz" - integrity sha512-P8O1xDy41B7O7iXaSlW+UuFbE5+ZWQDb61ndGDxKIt36fMH50DtlQTbwLpFLf8DikceTAb3r6nPrRv30wBlzXw== +stylelint@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.1.0.tgz#24d7cbe06250ceca3b276393bfdeaaaba4356195" + integrity sha512-Tw8OyIiYhxnIHUzgoLlCyWgCUKsPYiP3TDgs7M1VbayS+q5qZly2yxABg+YPe/hFRWiu0cOtptCtpyrn1CrnYw== dependencies: - "@stylelint/postcss-css-in-js" "^0.37.2" - "@stylelint/postcss-markdown" "^0.36.2" - autoprefixer "^9.8.6" - balanced-match "^1.0.0" - chalk "^4.1.0" - cosmiconfig "^7.0.0" - debug "^4.3.1" - execall "^2.0.0" - fast-glob "^3.2.5" - fastest-levenshtein "^1.0.12" + "@csstools/css-parser-algorithms" "^2.0.1" + "@csstools/css-tokenizer" "^2.0.1" + "@csstools/media-query-list-parser" "^2.0.1" + "@csstools/selector-specificity" "^2.1.1" + balanced-match "^2.0.0" + colord "^2.9.3" + cosmiconfig "^8.0.0" + css-functions-list "^3.1.0" + css-tree "^2.3.1" + debug "^4.3.4" + fast-glob "^3.2.12" + fastest-levenshtein "^1.0.16" file-entry-cache "^6.0.1" - get-stdin "^8.0.0" global-modules "^2.0.0" - globby "^11.0.2" + globby "^11.1.0" globjoin "^0.1.4" - html-tags "^3.1.0" - ignore "^5.1.8" + html-tags "^3.2.0" + ignore "^5.2.4" import-lazy "^4.0.0" imurmurhash "^0.1.4" - known-css-properties "^0.21.0" - lodash "^4.17.21" - log-symbols "^4.0.0" + is-plain-object "^5.0.0" + known-css-properties "^0.26.0" mathml-tag-names "^2.1.3" meow "^9.0.0" - micromatch "^4.0.2" - normalize-selector "^0.2.0" - postcss "^7.0.35" - postcss-html "^0.36.0" - postcss-less "^3.1.4" + micromatch "^4.0.5" + normalize-path "^3.0.0" + picocolors "^1.0.0" + postcss "^8.4.21" postcss-media-query-parser "^0.2.3" postcss-resolve-nested-selector "^0.1.1" - postcss-safe-parser "^4.0.2" - postcss-sass "^0.4.4" - postcss-scss "^2.1.1" - postcss-selector-parser "^6.0.4" - postcss-syntax "^0.36.2" - postcss-value-parser "^4.1.0" + postcss-safe-parser "^6.0.0" + postcss-selector-parser "^6.0.11" + postcss-value-parser "^4.2.0" resolve-from "^5.0.0" - slash "^3.0.0" - specificity "^0.4.1" - string-width "^4.2.2" - strip-ansi "^6.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" style-search "^0.1.0" - sugarss "^2.0.0" + supports-hyperlinks "^2.3.0" svg-tags "^1.0.0" - table "^6.0.7" - v8-compile-cache "^2.2.0" - write-file-atomic "^3.0.3" + table "^6.8.1" + v8-compile-cache "^2.3.0" + write-file-atomic "^5.0.0" -stylis@^4.0.3: - version "4.0.8" - resolved "https://registry.npmjs.org/stylis/-/stylis-4.0.8.tgz" - integrity sha512-WCHD2YHu2gp4GN9M8TqD7DZljL/UC5mIFaKyYJRuRyPdnqkTqzTnxCIQ1Z3VgQvz1aPcua5bSS2h0HrcbDUdBg== - -subscriptions-transport-ws@^0.9.18: - version "0.9.18" - resolved "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.18.tgz" - integrity sha512-tztzcBTNoEbuErsVQpTN2xUNN/efAZXyCyL5m3x4t6SKrEiTL2N8SaKWBFWM4u56pL79ULif3zjyeq+oV+nOaA== - dependencies: - backo2 "^1.0.2" - eventemitter3 "^3.1.0" - iterall "^1.2.1" - symbol-observable "^1.0.4" - ws "^5.2.0" - -sugarss@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/sugarss/-/sugarss-2.0.0.tgz" - integrity sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ== - dependencies: - postcss "^7.0.2" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= +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.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + 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@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: +supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + 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@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" + integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== + 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" @@ -7344,126 +7415,117 @@ supports-preserve-symlinks-flag@^1.0.0: svg-tags@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz" - integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q= + resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" + integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA== -swap-case@^2.0.1: +swap-case@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/swap-case/-/swap-case-2.0.2.tgz" + 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@^1.0.4, symbol-observable@^1.1.0: - version "1.2.0" - resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz" - integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== +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== -symbol-observable@^2.0.0: - version "2.0.3" - resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz" - integrity sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA== +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== -sync-fetch@0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.3.0.tgz" - integrity sha512-dJp4qg+x4JwSEW1HibAuMi0IIrBI3wuQr2GimmqB7OXR50wmwzfdusG+p39R9w3R6aFtZ2mzvxvWKQ3Bd/vx3g== - dependencies: - buffer "^5.7.0" - node-fetch "^2.6.1" - -table@^6.0.7: - version "6.0.7" - resolved "https://registry.npmjs.org/table/-/table-6.0.7.tgz" - integrity sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g== - dependencies: - ajv "^7.0.2" - lodash "^4.17.20" - slice-ansi "^4.0.0" - string-width "^4.2.0" - -table@^6.0.9: - version "6.7.2" - resolved "https://registry.yarnpkg.com/table/-/table-6.7.2.tgz#a8d39b9f5966693ca8b0feba270a78722cbaf3b0" - integrity sha512-UFZK67uvyNivLeQbVtkiUs8Uuuxv24aSL4/Vil2PJVtMgU8Lx0CYkP12uCGa3kjyQzOSgV1+z9Wkb82fCGsO0g== +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.clonedeep "^4.5.0" 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.npmjs.org/text-table/-/text-table-0.2.0.tgz" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + 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== -through@^2.3.6: +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.npmjs.org/through/-/through-2.3.8.tgz" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + 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.1.0" - resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz" - integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== + 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, tiny-warning@^1.0.3: +tiny-warning@^1.0.0, tiny-warning@^1.0.2: version "1.0.3" - resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== -title-case@^3.0.2: +title-case@^3.0.3: version "3.0.3" - resolved "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz" + 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.npmjs.org/tmp/-/tmp-0.0.33.tgz" + 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.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= - -to-readable-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz" - integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + 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.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + 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" -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - toposort@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz" - integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== -totalist@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/totalist/-/totalist-2.0.0.tgz#db6f1e19c0fa63e71339bbb8fba89653c18c7eec" - integrity sha512-+Y17F0YzxfACxTyjfhnJQEe7afPA0GSpYlFkl2VFMxYP7jshQf9gXV7cH47EfToBumFThfKBvfAcoUn6fdNeRQ== +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" @@ -7472,159 +7534,168 @@ trim-newlines@^3.0.0: trough@^1.0.0: version "1.0.5" - resolved "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz" + resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== -trough@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/trough/-/trough-2.0.2.tgz" - integrity sha512-FnHq5sTMxC0sk957wHDzRnemFnNBvt/gSY99HzK8F7UP5WAbvP70yX5bd7CjEQkN+TjdxwI7g7lJ6podqrG2/w== - -ts-invariant@^0.6.2: - version "0.6.2" - resolved "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.6.2.tgz" - integrity sha512-hsVurayufl1gXg8CHtgZkB7X0KtA3TrI3xcJ9xkRr8FeJHnM/TIEQkgBq9XkpduyBWWUdlRIR9xWf4Lxq3LJTg== +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: - "@types/ungap__global-this" "^0.3.1" - "@ungap/global-this" "^0.4.2" - tslib "^1.9.3" + tslib "^2.1.0" ts-log@^2.2.3: - version "2.2.3" - resolved "https://registry.npmjs.org/ts-log/-/ts-log-2.2.3.tgz" - integrity sha512-XvB+OdKSJ708Dmf9ore4Uf/q62AYDTzFcAdxc8KNML1mmAWywRFVt/dn1KYJH8Agt5UJNujfM3znU5PxgAzA2w== + 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@^9: - version "9.1.1" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz" - integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== +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" - source-map-support "^0.5.17" + v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tsconfig-paths@^3.11.0, tsconfig-paths@^3.9.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36" - integrity sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA== +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.0" + minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.10.0, tslib@^1.14.1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.10.0, tslib@^1.8.1: version "1.14.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@~2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz" - integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== +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: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== -tslib@~2.0.1: - version "2.0.3" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz" - integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== +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== tsutils@^3.21.0: version "3.21.0" - resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" + 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.npmjs.org/type-check/-/type-check-0.4.0.tgz" + 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.11.0: - version "0.11.0" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz" - integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== - type-fest@^0.13.1: version "0.13.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== type-fest@^0.18.0: version "0.18.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== type-fest@^0.20.2: version "0.20.2" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" + 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.npmjs.org/type-fest/-/type-fest-0.6.0.tgz" + 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.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +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.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" + 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: - version "4.2.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz" - integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== +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== -typescript@~4.4.4: - version "4.4.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c" - integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA== +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@^0.7.18: - version "0.7.24" - resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.24.tgz" - integrity sha512-yo+miGzQx5gakzVK3QFfN0/L9uVhosXBBO7qmnk7c2iw1IhL212wfA3zbnI54B0obGwC/5NWub/iT9sReMx+Fw== +ua-parser-js@^1.0.2: + version "1.0.33" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.33.tgz#f21f01233e90e7ed0f059ceab46eb190ff17f8f4" + integrity sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ== -unbox-primitive@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.0.tgz" - integrity sha512-P/51NX+JXyxK/aigg1/ZgyccdAxm5K1+n8+tvqSntjOivPt19gvm1VC49RWYetsiub8WViUchdxl/KWHHB0kzA== +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: - function-bind "^1.1.1" - has-bigints "^1.0.0" - has-symbols "^1.0.0" - which-boxed-primitive "^1.0.1" - -unbox-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" - integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== - dependencies: - function-bind "^1.1.1" - has-bigints "^1.0.1" - has-symbols "^1.0.2" + 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.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz" - integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= + resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" + integrity sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg== -uncontrollable@^7.0.0: +uncontrollable@^7.2.1: version "7.2.1" - resolved "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz" + resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.2.1.tgz#1fa70ba0c57a14d5f78905d533cf63916dc75738" integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ== dependencies: "@babel/runtime" "^7.6.3" @@ -7632,23 +7703,33 @@ uncontrollable@^7.0.0: invariant "^2.2.4" react-lifecycles-compat "^3.0.4" -unified@^10.0.0: - version "10.1.0" - resolved "https://registry.npmjs.org/unified/-/unified-10.1.0.tgz" - integrity sha512-4U3ru/BRXYYhKbwXV6lU6bufLikoAavTwev89H5UxY8enDFaAT2VXmIXYNm6hb5oHPng/EXr77PVyDFcptbk5g== - dependencies: - "@types/unist" "^2.0.0" - bail "^2.0.0" - extend "^3.0.0" - is-buffer "^2.0.0" - is-plain-obj "^4.0.0" - trough "^2.0.0" - vfile "^5.0.0" +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== -unified@^9.1.0: - version "9.2.1" - resolved "https://registry.npmjs.org/unified/-/unified-9.2.1.tgz" - integrity sha512-juWjuI8Z4xFg8pJbnEZ41b5xjGUWGHqXALmBZ3FC3WX0PIx1CZBIIJ6mXbYMcf6Yw4Fi0rFUTA1cdz/BglbOhA== +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" @@ -7657,104 +7738,72 @@ unified@^9.1.0: trough "^1.0.0" vfile "^4.0.0" -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= +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-builder@^3.0.0: +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.npmjs.org/unist-builder/-/unist-builder-3.0.0.tgz" - integrity sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ== - dependencies: - "@types/unist" "^2.0.0" - -unist-util-find-all-after@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-3.0.2.tgz" - integrity sha512-xaTC/AGZ0rIM2gM28YVRAFPIZpzbpDtU3dRmp7EXlNVA8ziQc4hY3H7BHXM1J49nEmiqc3svnqMReW+PGqbZKQ== - dependencies: - unist-util-is "^4.0.0" - -unist-util-generated@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.0.tgz" - integrity sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw== + 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.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.1.0.tgz#976e5f462a7a5de73d94b706bac1b90671b57797" integrity sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg== -unist-util-is@^5.0.0: - version "5.1.1" - resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.1.1.tgz" - integrity sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ== - -unist-util-position@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.1.tgz" - integrity sha512-mgy/zI9fQ2HlbOtTdr2w9lhVaiFUHWQnZrFF2EUoVOqtAUdzqMtNiD99qA5a1IcjWVR8O6aVYE9u7Z2z1v0SQA== +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.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz" + 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-stringify-position@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.0.tgz" - integrity sha512-SdfAl8fsDclywZpfMDTVDxA2V7LjtRDTOFd44wUJamgl6OlVngsqWjxvermMYf60elWHbxhuRCZml7AnuXCaSA== +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: - "@types/unist" "^2.0.0" + unist-util-is "^3.0.0" unist-util-visit-parents@^3.0.0: version "3.1.1" - resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz" + 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-parents@^4.0.0: - version "4.1.1" - resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz" - integrity sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw== +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: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" + unist-util-visit-parents "^2.0.0" -unist-util-visit-parents@^5.0.0: - version "5.1.0" - resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.0.tgz" - integrity sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg== +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 "^5.0.0" - -unist-util-visit@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-3.1.0.tgz" - integrity sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^4.0.0" - -unist-util-visit@^4.0.0: - version "4.1.0" - resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.0.tgz" - integrity sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^5.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.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz" + 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" @@ -7762,86 +7811,80 @@ universal-cookie@^4.0.4: universalify@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unixify@1.0.0: +unixify@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz" - integrity sha1-OmQcjC/7zk2mg6XHDwOkYpQMIJA= + resolved "https://registry.yarnpkg.com/unixify/-/unixify-1.0.0.tgz#3a641c8c2ffbce4da683a5c70f03a462940c2090" + integrity sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg== dependencies: normalize-path "^2.1.1" -upper-case-first@^2.0.1, upper-case-first@^2.0.2: +update-browserslist-db@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" + integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +upper-case-first@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz" + 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.1, upper-case@^2.0.2: +upper-case@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz" + 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.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz" - integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= - dependencies: - prepend-http "^2.0.0" - -url-parse@^1.4.3: - version "1.5.10" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" - integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - url-toolkit@^2.2.1: - version "2.2.3" - resolved "https://registry.yarnpkg.com/url-toolkit/-/url-toolkit-2.2.3.tgz#78fa901215abbac34182066932220279b804522b" - integrity sha512-Da75SQoxsZ+2wXS56CZBrj2nukQ4nlGUZUP/dqUBG5E1su5GKThgT94Q00x81eVII7AyS1Pn+CtTTZ4Z0pLUtQ== + 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@^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.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -uvu@^0.5.0: - version "0.5.2" - resolved "https://registry.npmjs.org/uvu/-/uvu-0.5.2.tgz" - integrity sha512-m2hLe7I2eROhh+tm3WE5cTo/Cv3WQA7Oc9f7JB6uWv+/zVKvfAm53bMyOoGOSZeQ7Ov2Fu9pLhFr7p07bnT20w== - dependencies: - dequal "^2.0.0" - diff "^5.0.0" - kleur "^4.0.3" - sade "^1.7.3" - totalist "^2.0.0" +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== -v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0: +v8-compile-cache@^2.3.0: version "2.3.0" - resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== -valid-url@1.0.9, valid-url@^1.0.9: - version "1.0.9" - resolved "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz" - integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA= - validate-npm-package-license@^3.0.1: version "3.0.4" - resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz" + 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" @@ -7849,28 +7892,25 @@ validate-npm-package-license@^3.0.1: value-equal@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz" + 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.12, 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.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz" + 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-message@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-3.0.2.tgz" - integrity sha512-UUjZYIOg9lDRwwiBAuezLIsu9KlXntdxwG+nXnjuQAHvBpcX3x0eN8h+I7TkY5nkCXj+cWVp4ZqebtGBvok8ww== - dependencies: - "@types/unist" "^2.0.0" - unist-util-stringify-position "^3.0.0" - vfile@^4.0.0: version "4.2.1" - resolved "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624" integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA== dependencies: "@types/unist" "^2.0.0" @@ -7878,35 +7918,34 @@ vfile@^4.0.0: unist-util-stringify-position "^2.0.0" vfile-message "^2.0.0" -vfile@^5.0.0: - version "5.1.1" - resolved "https://registry.npmjs.org/vfile/-/vfile-5.1.1.tgz" - integrity sha512-sfI+3MnGUodvAE2s3hXCcJxhcymXQgekdgqNf9WMcmWtZU65tPMaml5eGYREJfMJGHr4/0GPZgrN3UMgWjHXSQ== - dependencies: - "@types/unist" "^2.0.0" - is-buffer "^2.0.0" - unist-util-stringify-position "^3.0.0" - vfile-message "^3.0.0" - -"video.js@^6 || ^7", video.js@^7.20.3: - version "7.20.3" - resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.20.3.tgz#5694741346dc683255993e5069daa15d4bacb646" - integrity sha512-JMspxaK74LdfWcv69XWhX4rILywz/eInOVPdKefpQiZJSMD5O8xXYueqACP2Q5yqKstycgmmEKlJzZ+kVmDciw== +"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.14.3" + "@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.7.1" - mpd-parser "0.21.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.4" +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" @@ -7933,52 +7972,92 @@ videojs-vtt.js@^0.15.4: dependencies: global "^4.3.1" -vite-plugin-compression@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/vite-plugin-compression/-/vite-plugin-compression-0.3.5.tgz#1e5338eb43e60128de6d6f22b2aabf0e3dc0c17f" - integrity sha512-W+zKccNTDRYPsM6MzbVGTX2hvnsVrxKfmGD2Z23VgqinFKhtmwGzNNFsnbslXbeTGHXclaE7MO5nm7O1cNY3ZA== +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.2" + debug "^4.3.3" fs-extra "^10.0.0" -vite-tsconfig-paths@^3.3.17: - version "3.3.17" - resolved "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-3.3.17.tgz" - integrity sha512-wx+rfC53moVLxMBj2EApJZgY6HtvWUFVZ4dBxNGYBxSSqU6UaHdKlcOxrfGDxyTGtYEr9beWCryHn18C4EtZkg== +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" - recrawl-sync "^2.0.3" - tsconfig-paths "^3.9.0" + tsconfck "^2.0.1" -vite@^2.9.13: - version "2.9.13" - resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.13.tgz#859cb5d4c316c0d8c6ec9866045c0f7858ca6abc" - integrity sha512-AsOBAaT0AD7Mhe8DuK+/kE4aWYFMx/i0ZNi98hJclxb4e0OhQcZYUrvLjIaQ8e59Ui7txcvKMiJC1yftqpQoDw== +vite@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.1.1.tgz#3b18b81a4e85ce3df5cbdbf4c687d93ebf402e6b" + integrity sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg== dependencies: - esbuild "^0.14.27" - postcss "^8.4.13" - resolve "^1.22.0" - rollup "^2.59.0" + esbuild "^0.16.14" + postcss "^8.4.21" + resolve "^1.22.1" + rollup "^3.10.0" optionalDependencies: fsevents "~2.3.2" -warning@^4.0.0, warning@^4.0.3: +warning@^4.0.0, warning@^4.0.2, warning@^4.0.3: version "4.0.3" - resolved "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz" + 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== + +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== + whatwg-fetch@^3.4.1: version "3.6.2" - resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== -which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2: +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.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" + 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" @@ -7987,41 +8066,55 @@ which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2: 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.npmjs.org/which-module/-/which-module-2.0.0.tgz" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + 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.npmjs.org/which/-/which-1.3.1.tgz" + 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.npmjs.org/which/-/which-2.0.2.tgz" + 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.3" - resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -wrap-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz" - integrity sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo= - dependencies: - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi@^6.2.0: version "6.2.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz" + 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" @@ -8030,7 +8123,7 @@ wrap-ansi@^6.2.0: wrap-ansi@^7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + 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" @@ -8039,12 +8132,12 @@ wrap-ansi@^7.0.0: wrappy@1: version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + 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, write-file-atomic@^3.0.3: +write-file-atomic@^3.0.0: version "3.0.3" - resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz" + 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" @@ -8052,9 +8145,17 @@ write-file-atomic@^3.0.0, write-file-atomic@^3.0.3: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" +write-file-atomic@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.0.tgz#54303f117e109bf3d540261125c8ea5a7320fab0" + integrity sha512-R7NYMnHSlV42K54lwY9lvW6MnSm1HSJqZL3xiSgi9E7//FYaI74r2G0rd+/X6VAMkHEdzxQaU5HUOXWUz5kA/w== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + write-json-file@^4.3.0: version "4.3.0" - resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-4.3.0.tgz" + 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" @@ -8064,64 +8165,67 @@ write-json-file@^4.3.0: sort-keys "^4.0.0" write-file-atomic "^3.0.0" -ws@7.4.4: - version "7.4.4" - resolved "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz" - integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== +ws@8.12.1, ws@^8.12.0: + version "8.12.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.1.tgz#c51e583d79140b5e42e39be48c934131942d4a8f" + integrity sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew== -ws@^5.2.0: - version "5.2.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.3.tgz#05541053414921bc29c63bee14b8b0dd50b07b3d" - integrity sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA== - dependencies: - async-limiter "~1.0.0" - -ws@^7.4.6: - version "7.5.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" - integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== +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.1" - resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz" - integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== + 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.5" - resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz" - integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== + 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.npmjs.org/yallist/-/yallist-4.0.0.tgz" + 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.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz" + resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== -yaml@^1.10.0, yaml@^1.7.2: +yaml@^1.10.0: version "1.10.2" - resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== yargs-parser@^18.1.2, yargs-parser@^18.1.3: version "18.1.3" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz" + 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.2, yargs-parser@^20.2.3: - version "20.2.7" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz" - integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== +yargs-parser@^20.2.3: + 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.npmjs.org/yargs/-/yargs-15.4.1.tgz" + 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" @@ -8136,48 +8240,52 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.1.1: - version "16.2.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== +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 "^7.0.2" + cliui "^8.0.1" escalade "^3.1.1" get-caller-file "^2.0.5" require-directory "^2.1.1" - string-width "^4.2.0" + string-width "^4.2.3" y18n "^5.0.5" - yargs-parser "^20.2.2" + yargs-parser "^21.1.1" yn@3.1.1: version "3.1.1" - resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" + 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.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -yup@^0.32.9: - version "0.32.9" - resolved "https://registry.npmjs.org/yup/-/yup-0.32.9.tgz" - integrity sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg== +yup@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/yup/-/yup-1.0.0.tgz#de4e32f9d2e45b1ab428076fc916c84db861b8ce" + integrity sha512-bRZIyMkoe212ahGJTE32cr2dLkJw53Va+Uw5mzsBKpcef9zCGQ23k/xtpQUfGwdWPKvCIlR8CzFwchs2rm2XpQ== dependencies: - "@babel/runtime" "^7.10.5" - "@types/lodash" "^4.14.165" - lodash "^4.17.20" - lodash-es "^4.17.15" - nanoclone "^0.2.1" - property-expr "^2.0.4" + property-expr "^2.0.5" + tiny-case "^1.0.3" toposort "^2.0.2" + type-fest "^2.19.0" -zen-observable@^0.8.14: +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.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz" + 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.npmjs.org/zwitch/-/zwitch-1.0.5.tgz" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw== diff --git a/vendor/github.com/gorilla/websocket/README.md b/vendor/github.com/gorilla/websocket/README.md index 19aa2e75c..2517a2871 100644 --- a/vendor/github.com/gorilla/websocket/README.md +++ b/vendor/github.com/gorilla/websocket/README.md @@ -6,6 +6,13 @@ Gorilla WebSocket is a [Go](http://golang.org/) implementation of the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. + +--- + +⚠️ **[The Gorilla WebSocket Package is looking for a new maintainer](https://github.com/gorilla/websocket/issues/370)** + +--- + ### Documentation * [API Reference](https://pkg.go.dev/github.com/gorilla/websocket?tab=doc) @@ -30,35 +37,3 @@ The Gorilla WebSocket package passes the server tests in the [Autobahn Test Suite](https://github.com/crossbario/autobahn-testsuite) using the application in the [examples/autobahn subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn). -### Gorilla WebSocket compared with other packages - - - - - - - - - - - - - - - - - - -
    github.com/gorillagolang.org/x/net
    RFC 6455 Features
    Passes Autobahn Test SuiteYesNo
    Receive fragmented messageYesNo, see note 1
    Send close messageYesNo
    Send pings and receive pongsYesNo
    Get the type of a received data messageYesYes, see note 2
    Other Features
    Compression ExtensionsExperimentalNo
    Read message using io.ReaderYesNo, see note 3
    Write message using io.WriteCloserYesNo, see note 3
    - -Notes: - -1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html). -2. The application can get the type of a received data message by implementing - a [Codec marshal](http://godoc.org/golang.org/x/net/websocket#Codec.Marshal) - function. -3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries. - Read returns when the input buffer is full or a frame boundary is - encountered. Each call to Write sends a single frame message. The Gorilla - io.Reader and io.WriteCloser operate on a single WebSocket message. - diff --git a/vendor/github.com/gorilla/websocket/client.go b/vendor/github.com/gorilla/websocket/client.go index 962c06a39..2efd83555 100644 --- a/vendor/github.com/gorilla/websocket/client.go +++ b/vendor/github.com/gorilla/websocket/client.go @@ -48,15 +48,23 @@ func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufS } // A Dialer contains options for connecting to WebSocket server. +// +// It is safe to call Dialer's methods concurrently. type Dialer struct { // NetDial specifies the dial function for creating TCP connections. If // NetDial is nil, net.Dial is used. NetDial func(network, addr string) (net.Conn, error) // NetDialContext specifies the dial function for creating TCP connections. If - // NetDialContext is nil, net.DialContext is used. + // NetDialContext is nil, NetDial is used. NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error) + // NetDialTLSContext specifies the dial function for creating TLS/TCP connections. If + // NetDialTLSContext is nil, NetDialContext is used. + // If NetDialTLSContext is set, Dial assumes the TLS handshake is done there and + // TLSClientConfig is ignored. + NetDialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error) + // Proxy specifies a function to return a proxy for a given // Request. If the function returns a non-nil error, the // request is aborted with the provided error. @@ -65,6 +73,8 @@ type Dialer struct { // TLSClientConfig specifies the TLS configuration to use with tls.Client. // If nil, the default configuration is used. + // If either NetDialTLS or NetDialTLSContext are set, Dial assumes the TLS handshake + // is done there and TLSClientConfig is ignored. TLSClientConfig *tls.Config // HandshakeTimeout specifies the duration for the handshake to complete. @@ -176,7 +186,7 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h } req := &http.Request{ - Method: "GET", + Method: http.MethodGet, URL: u, Proto: "HTTP/1.1", ProtoMajor: 1, @@ -237,13 +247,32 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h // Get network dial function. var netDial func(network, add string) (net.Conn, error) - if d.NetDialContext != nil { - netDial = func(network, addr string) (net.Conn, error) { - return d.NetDialContext(ctx, network, addr) + switch u.Scheme { + case "http": + if d.NetDialContext != nil { + netDial = func(network, addr string) (net.Conn, error) { + return d.NetDialContext(ctx, network, addr) + } + } else if d.NetDial != nil { + netDial = d.NetDial } - } else if d.NetDial != nil { - netDial = d.NetDial - } else { + case "https": + if d.NetDialTLSContext != nil { + netDial = func(network, addr string) (net.Conn, error) { + return d.NetDialTLSContext(ctx, network, addr) + } + } else if d.NetDialContext != nil { + netDial = func(network, addr string) (net.Conn, error) { + return d.NetDialContext(ctx, network, addr) + } + } else if d.NetDial != nil { + netDial = d.NetDial + } + default: + return nil, nil, errMalformedURL + } + + if netDial == nil { netDialer := &net.Dialer{} netDial = func(network, addr string) (net.Conn, error) { return netDialer.DialContext(ctx, network, addr) @@ -304,7 +333,9 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h } }() - if u.Scheme == "https" { + if u.Scheme == "https" && d.NetDialTLSContext == nil { + // If NetDialTLSContext is set, assume that the TLS handshake has already been done + cfg := cloneTLSConfig(d.TLSClientConfig) if cfg.ServerName == "" { cfg.ServerName = hostNoPort @@ -312,11 +343,12 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h tlsConn := tls.Client(netConn, cfg) netConn = tlsConn - var err error - if trace != nil { - err = doHandshakeWithTrace(trace, tlsConn, cfg) - } else { - err = doHandshake(tlsConn, cfg) + if trace != nil && trace.TLSHandshakeStart != nil { + trace.TLSHandshakeStart() + } + err := doHandshake(ctx, tlsConn, cfg) + if trace != nil && trace.TLSHandshakeDone != nil { + trace.TLSHandshakeDone(tlsConn.ConnectionState(), err) } if err != nil { @@ -348,8 +380,8 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h } if resp.StatusCode != 101 || - !strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") || - !strings.EqualFold(resp.Header.Get("Connection"), "upgrade") || + !tokenListContainsValue(resp.Header, "Upgrade", "websocket") || + !tokenListContainsValue(resp.Header, "Connection", "upgrade") || resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) { // Before closing the network connection on return from this // function, slurp up some of the response to aid application @@ -382,14 +414,9 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h return conn, resp, nil } -func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) error { - if err := tlsConn.Handshake(); err != nil { - return err +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + if cfg == nil { + return &tls.Config{} } - if !cfg.InsecureSkipVerify { - if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { - return err - } - } - return nil + return cfg.Clone() } diff --git a/vendor/github.com/gorilla/websocket/client_clone.go b/vendor/github.com/gorilla/websocket/client_clone.go deleted file mode 100644 index 4f0d94372..000000000 --- a/vendor/github.com/gorilla/websocket/client_clone.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build go1.8 - -package websocket - -import "crypto/tls" - -func cloneTLSConfig(cfg *tls.Config) *tls.Config { - if cfg == nil { - return &tls.Config{} - } - return cfg.Clone() -} diff --git a/vendor/github.com/gorilla/websocket/client_clone_legacy.go b/vendor/github.com/gorilla/websocket/client_clone_legacy.go deleted file mode 100644 index babb007fb..000000000 --- a/vendor/github.com/gorilla/websocket/client_clone_legacy.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !go1.8 - -package websocket - -import "crypto/tls" - -// cloneTLSConfig clones all public fields except the fields -// SessionTicketsDisabled and SessionTicketKey. This avoids copying the -// sync.Mutex in the sync.Once and makes it safe to call cloneTLSConfig on a -// config in active use. -func cloneTLSConfig(cfg *tls.Config) *tls.Config { - if cfg == nil { - return &tls.Config{} - } - return &tls.Config{ - Rand: cfg.Rand, - Time: cfg.Time, - Certificates: cfg.Certificates, - NameToCertificate: cfg.NameToCertificate, - GetCertificate: cfg.GetCertificate, - RootCAs: cfg.RootCAs, - NextProtos: cfg.NextProtos, - ServerName: cfg.ServerName, - ClientAuth: cfg.ClientAuth, - ClientCAs: cfg.ClientCAs, - InsecureSkipVerify: cfg.InsecureSkipVerify, - CipherSuites: cfg.CipherSuites, - PreferServerCipherSuites: cfg.PreferServerCipherSuites, - ClientSessionCache: cfg.ClientSessionCache, - MinVersion: cfg.MinVersion, - MaxVersion: cfg.MaxVersion, - CurvePreferences: cfg.CurvePreferences, - } -} diff --git a/vendor/github.com/gorilla/websocket/conn.go b/vendor/github.com/gorilla/websocket/conn.go index ca46d2f79..331eebc85 100644 --- a/vendor/github.com/gorilla/websocket/conn.go +++ b/vendor/github.com/gorilla/websocket/conn.go @@ -13,6 +13,7 @@ import ( "math/rand" "net" "strconv" + "strings" "sync" "time" "unicode/utf8" @@ -401,6 +402,12 @@ func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error return nil } +func (c *Conn) writeBufs(bufs ...[]byte) error { + b := net.Buffers(bufs) + _, err := b.WriteTo(c.conn) + return err +} + // WriteControl writes a control message with the given deadline. The allowed // message types are CloseMessage, PingMessage and PongMessage. func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) error { @@ -794,47 +801,69 @@ func (c *Conn) advanceFrame() (int, error) { } // 2. Read and parse first two bytes of frame header. + // To aid debugging, collect and report all errors in the first two bytes + // of the header. + + var errors []string p, err := c.read(2) if err != nil { return noFrame, err } - final := p[0]&finalBit != 0 frameType := int(p[0] & 0xf) + final := p[0]&finalBit != 0 + rsv1 := p[0]&rsv1Bit != 0 + rsv2 := p[0]&rsv2Bit != 0 + rsv3 := p[0]&rsv3Bit != 0 mask := p[1]&maskBit != 0 c.setReadRemaining(int64(p[1] & 0x7f)) c.readDecompress = false - if c.newDecompressionReader != nil && (p[0]&rsv1Bit) != 0 { - c.readDecompress = true - p[0] &^= rsv1Bit + if rsv1 { + if c.newDecompressionReader != nil { + c.readDecompress = true + } else { + errors = append(errors, "RSV1 set") + } } - if rsv := p[0] & (rsv1Bit | rsv2Bit | rsv3Bit); rsv != 0 { - return noFrame, c.handleProtocolError("unexpected reserved bits 0x" + strconv.FormatInt(int64(rsv), 16)) + if rsv2 { + errors = append(errors, "RSV2 set") + } + + if rsv3 { + errors = append(errors, "RSV3 set") } switch frameType { case CloseMessage, PingMessage, PongMessage: if c.readRemaining > maxControlFramePayloadSize { - return noFrame, c.handleProtocolError("control frame length > 125") + errors = append(errors, "len > 125 for control") } if !final { - return noFrame, c.handleProtocolError("control frame not final") + errors = append(errors, "FIN not set on control") } case TextMessage, BinaryMessage: if !c.readFinal { - return noFrame, c.handleProtocolError("message start before final message frame") + errors = append(errors, "data before FIN") } c.readFinal = final case continuationFrame: if c.readFinal { - return noFrame, c.handleProtocolError("continuation after final message frame") + errors = append(errors, "continuation after FIN") } c.readFinal = final default: - return noFrame, c.handleProtocolError("unknown opcode " + strconv.Itoa(frameType)) + errors = append(errors, "bad opcode "+strconv.Itoa(frameType)) + } + + if mask != c.isServer { + errors = append(errors, "bad MASK") + } + + if len(errors) > 0 { + return noFrame, c.handleProtocolError(strings.Join(errors, ", ")) } // 3. Read and parse frame length as per @@ -872,10 +901,6 @@ func (c *Conn) advanceFrame() (int, error) { // 4. Handle frame masking. - if mask != c.isServer { - return noFrame, c.handleProtocolError("incorrect mask flag") - } - if mask { c.readMaskPos = 0 p, err := c.read(len(c.readMaskKey)) @@ -935,7 +960,7 @@ func (c *Conn) advanceFrame() (int, error) { if len(payload) >= 2 { closeCode = int(binary.BigEndian.Uint16(payload)) if !isValidReceivedCloseCode(closeCode) { - return noFrame, c.handleProtocolError("invalid close code") + return noFrame, c.handleProtocolError("bad close code " + strconv.Itoa(closeCode)) } closeText = string(payload[2:]) if !utf8.ValidString(closeText) { @@ -952,7 +977,11 @@ func (c *Conn) advanceFrame() (int, error) { } func (c *Conn) handleProtocolError(message string) error { - c.WriteControl(CloseMessage, FormatCloseMessage(CloseProtocolError, message), time.Now().Add(writeWait)) + data := FormatCloseMessage(CloseProtocolError, message) + if len(data) > maxControlFramePayloadSize { + data = data[:maxControlFramePayloadSize] + } + c.WriteControl(CloseMessage, data, time.Now().Add(writeWait)) return errors.New("websocket: " + message) } diff --git a/vendor/github.com/gorilla/websocket/conn_write.go b/vendor/github.com/gorilla/websocket/conn_write.go deleted file mode 100644 index a509a21f8..000000000 --- a/vendor/github.com/gorilla/websocket/conn_write.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build go1.8 - -package websocket - -import "net" - -func (c *Conn) writeBufs(bufs ...[]byte) error { - b := net.Buffers(bufs) - _, err := b.WriteTo(c.conn) - return err -} diff --git a/vendor/github.com/gorilla/websocket/conn_write_legacy.go b/vendor/github.com/gorilla/websocket/conn_write_legacy.go deleted file mode 100644 index 37edaff5a..000000000 --- a/vendor/github.com/gorilla/websocket/conn_write_legacy.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !go1.8 - -package websocket - -func (c *Conn) writeBufs(bufs ...[]byte) error { - for _, buf := range bufs { - if len(buf) > 0 { - if _, err := c.conn.Write(buf); err != nil { - return err - } - } - } - return nil -} diff --git a/vendor/github.com/gorilla/websocket/mask.go b/vendor/github.com/gorilla/websocket/mask.go index 577fce9ef..d0742bf2a 100644 --- a/vendor/github.com/gorilla/websocket/mask.go +++ b/vendor/github.com/gorilla/websocket/mask.go @@ -2,6 +2,7 @@ // this source code is governed by a BSD-style license that can be found in the // LICENSE file. +//go:build !appengine // +build !appengine package websocket diff --git a/vendor/github.com/gorilla/websocket/mask_safe.go b/vendor/github.com/gorilla/websocket/mask_safe.go index 2aac060e5..36250ca7c 100644 --- a/vendor/github.com/gorilla/websocket/mask_safe.go +++ b/vendor/github.com/gorilla/websocket/mask_safe.go @@ -2,6 +2,7 @@ // this source code is governed by a BSD-style license that can be found in the // LICENSE file. +//go:build appengine // +build appengine package websocket diff --git a/vendor/github.com/gorilla/websocket/proxy.go b/vendor/github.com/gorilla/websocket/proxy.go index e87a8c9f0..e0f466b72 100644 --- a/vendor/github.com/gorilla/websocket/proxy.go +++ b/vendor/github.com/gorilla/websocket/proxy.go @@ -48,7 +48,7 @@ func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) } connectReq := &http.Request{ - Method: "CONNECT", + Method: http.MethodConnect, URL: &url.URL{Opaque: addr}, Host: addr, Header: connectHeader, diff --git a/vendor/github.com/gorilla/websocket/server.go b/vendor/github.com/gorilla/websocket/server.go index 887d55891..24d53b38a 100644 --- a/vendor/github.com/gorilla/websocket/server.go +++ b/vendor/github.com/gorilla/websocket/server.go @@ -23,6 +23,8 @@ func (e HandshakeError) Error() string { return e.message } // Upgrader specifies parameters for upgrading an HTTP connection to a // WebSocket connection. +// +// It is safe to call Upgrader's methods concurrently. type Upgrader struct { // HandshakeTimeout specifies the duration for the handshake to complete. HandshakeTimeout time.Duration @@ -115,8 +117,8 @@ func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header // Upgrade upgrades the HTTP server connection to the WebSocket protocol. // // The responseHeader is included in the response to the client's upgrade -// request. Use the responseHeader to specify cookies (Set-Cookie) and the -// application negotiated subprotocol (Sec-WebSocket-Protocol). +// request. Use the responseHeader to specify cookies (Set-Cookie). To specify +// subprotocols supported by the server, set Upgrader.Subprotocols directly. // // If the upgrade fails, then Upgrade replies to the client with an HTTP error // response. @@ -131,7 +133,7 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header") } - if r.Method != "GET" { + if r.Method != http.MethodGet { return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET") } diff --git a/vendor/github.com/gorilla/websocket/tls_handshake.go b/vendor/github.com/gorilla/websocket/tls_handshake.go new file mode 100644 index 000000000..a62b68ccb --- /dev/null +++ b/vendor/github.com/gorilla/websocket/tls_handshake.go @@ -0,0 +1,21 @@ +//go:build go1.17 +// +build go1.17 + +package websocket + +import ( + "context" + "crypto/tls" +) + +func doHandshake(ctx context.Context, tlsConn *tls.Conn, cfg *tls.Config) error { + if err := tlsConn.HandshakeContext(ctx); err != nil { + return err + } + if !cfg.InsecureSkipVerify { + if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/gorilla/websocket/tls_handshake_116.go b/vendor/github.com/gorilla/websocket/tls_handshake_116.go new file mode 100644 index 000000000..e1b2b44f6 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/tls_handshake_116.go @@ -0,0 +1,21 @@ +//go:build !go1.17 +// +build !go1.17 + +package websocket + +import ( + "context" + "crypto/tls" +) + +func doHandshake(ctx context.Context, tlsConn *tls.Conn, cfg *tls.Config) error { + if err := tlsConn.Handshake(); err != nil { + return err + } + if !cfg.InsecureSkipVerify { + if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/gorilla/websocket/trace.go b/vendor/github.com/gorilla/websocket/trace.go deleted file mode 100644 index 834f122a0..000000000 --- a/vendor/github.com/gorilla/websocket/trace.go +++ /dev/null @@ -1,19 +0,0 @@ -// +build go1.8 - -package websocket - -import ( - "crypto/tls" - "net/http/httptrace" -) - -func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { - if trace.TLSHandshakeStart != nil { - trace.TLSHandshakeStart() - } - err := doHandshake(tlsConn, cfg) - if trace.TLSHandshakeDone != nil { - trace.TLSHandshakeDone(tlsConn.ConnectionState(), err) - } - return err -} diff --git a/vendor/github.com/gorilla/websocket/trace_17.go b/vendor/github.com/gorilla/websocket/trace_17.go deleted file mode 100644 index 77d05a0b5..000000000 --- a/vendor/github.com/gorilla/websocket/trace_17.go +++ /dev/null @@ -1,12 +0,0 @@ -// +build !go1.8 - -package websocket - -import ( - "crypto/tls" - "net/http/httptrace" -) - -func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { - return doHandshake(tlsConn, cfg) -} diff --git a/vendor/github.com/matryer/moq/.goreleaser.yml b/vendor/github.com/matryer/moq/.goreleaser.yml index c67f3571f..f0d7e0999 100644 --- a/vendor/github.com/matryer/moq/.goreleaser.yml +++ b/vendor/github.com/matryer/moq/.goreleaser.yml @@ -20,8 +20,6 @@ archives: windows: Windows 386: i386 amd64: x86_64 -universal_binaries: - - replace: false checksum: name_template: 'checksums.txt' snapshot: diff --git a/vendor/github.com/matryer/moq/internal/registry/var.go b/vendor/github.com/matryer/moq/internal/registry/var.go index 081a17c75..abb0d53be 100644 --- a/vendor/github.com/matryer/moq/internal/registry/var.go +++ b/vendor/github.com/matryer/moq/internal/registry/var.go @@ -49,12 +49,7 @@ func varName(vr *types.Var, suffix string) string { switch name { case "mock", "callInfo", "break", "default", "func", "interface", "select", "case", "defer", "go", "map", "struct", "chan", "else", "goto", "package", "switch", "const", "fallthrough", "if", "range", "type", "continue", "for", - "import", "return", "var", - // avoid shadowing basic types - "string", "bool", "byte", "rune", "uintptr", - "int", "int8", "int16", "int32", "int64", - "uint", "uint8", "uint16", "uint32", "uint64", - "float32", "float64", "complex64", "complex128": + "import", "return", "var": name += "MoqParam" } diff --git a/vendor/github.com/matryer/moq/releasing.md b/vendor/github.com/matryer/moq/releasing.md index d81081c60..8943476d6 100644 --- a/vendor/github.com/matryer/moq/releasing.md +++ b/vendor/github.com/matryer/moq/releasing.md @@ -26,11 +26,3 @@ Then: ```bash GITHUB_TOKEN=xxx goreleaser --rm-dist ``` - -## Testing - -To test and verify changes to Go Releaser config, use the following: - -```bash -goreleaser --snapshot --skip-publish --rm-dist -``` diff --git a/vendor/github.com/mitchellh/mapstructure/CHANGELOG.md b/vendor/github.com/mitchellh/mapstructure/CHANGELOG.md index 38a099162..c75823490 100644 --- a/vendor/github.com/mitchellh/mapstructure/CHANGELOG.md +++ b/vendor/github.com/mitchellh/mapstructure/CHANGELOG.md @@ -1,3 +1,16 @@ +## 1.5.0 + +* New option `IgnoreUntaggedFields` to ignore decoding to any fields + without `mapstructure` (or the configured tag name) set [GH-277] +* New option `ErrorUnset` which makes it an error if any fields + in a target struct are not set by the decoding process. [GH-225] +* New function `OrComposeDecodeHookFunc` to help compose decode hooks. [GH-240] +* Decoding to slice from array no longer crashes [GH-265] +* Decode nested struct pointers to map [GH-271] +* Fix issue where `,squash` was ignored if `Squash` option was set. [GH-280] +* Fix issue where fields with `,omitempty` would sometimes decode + into a map with an empty string key [GH-281] + ## 1.4.3 * Fix cases where `json.Number` didn't decode properly [GH-261] diff --git a/vendor/github.com/mitchellh/mapstructure/decode_hooks.go b/vendor/github.com/mitchellh/mapstructure/decode_hooks.go index 4d4bbc733..3a754ca72 100644 --- a/vendor/github.com/mitchellh/mapstructure/decode_hooks.go +++ b/vendor/github.com/mitchellh/mapstructure/decode_hooks.go @@ -77,6 +77,28 @@ func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc { } } +// OrComposeDecodeHookFunc executes all input hook functions until one of them returns no error. In that case its value is returned. +// If all hooks return an error, OrComposeDecodeHookFunc returns an error concatenating all error messages. +func OrComposeDecodeHookFunc(ff ...DecodeHookFunc) DecodeHookFunc { + return func(a, b reflect.Value) (interface{}, error) { + var allErrs string + var out interface{} + var err error + + for _, f := range ff { + out, err = DecodeHookExec(f, a, b) + if err != nil { + allErrs += err.Error() + "\n" + continue + } + + return out, nil + } + + return nil, errors.New(allErrs) + } +} + // StringToSliceHookFunc returns a DecodeHookFunc that converts // string to []string by splitting on the given sep. func StringToSliceHookFunc(sep string) DecodeHookFunc { diff --git a/vendor/github.com/mitchellh/mapstructure/mapstructure.go b/vendor/github.com/mitchellh/mapstructure/mapstructure.go index 6b81b0067..1efb22ac3 100644 --- a/vendor/github.com/mitchellh/mapstructure/mapstructure.go +++ b/vendor/github.com/mitchellh/mapstructure/mapstructure.go @@ -122,7 +122,7 @@ // field value is zero and a numeric type, the field is empty, and it won't // be encoded into the destination type. // -// type Source { +// type Source struct { // Age int `mapstructure:",omitempty"` // } // @@ -215,6 +215,12 @@ type DecoderConfig struct { // (extra keys). ErrorUnused bool + // If ErrorUnset is true, then it is an error for there to exist + // fields in the result that were not set in the decoding process + // (extra fields). This only applies to decoding to a struct. This + // will affect all nested structs as well. + ErrorUnset bool + // ZeroFields, if set to true, will zero fields before writing them. // For example, a map will be emptied before decoded values are put in // it. If this is false, a map will be merged. @@ -259,6 +265,10 @@ type DecoderConfig struct { // defaults to "mapstructure" TagName string + // IgnoreUntaggedFields ignores all struct fields without explicit + // TagName, comparable to `mapstructure:"-"` as default behaviour. + IgnoreUntaggedFields bool + // MatchName is the function used to match the map key to the struct // field name or tag. Defaults to `strings.EqualFold`. This can be used // to implement case-sensitive tag values, support snake casing, etc. @@ -284,6 +294,11 @@ type Metadata struct { // Unused is a slice of keys that were found in the raw value but // weren't decoded since there was no matching field in the result interface Unused []string + + // Unset is a slice of field names that were found in the result interface + // but weren't set in the decoding process since there was no matching value + // in the input + Unset []string } // Decode takes an input structure and uses reflection to translate it to @@ -375,6 +390,10 @@ func NewDecoder(config *DecoderConfig) (*Decoder, error) { if config.Metadata.Unused == nil { config.Metadata.Unused = make([]string, 0) } + + if config.Metadata.Unset == nil { + config.Metadata.Unset = make([]string, 0) + } } if config.TagName == "" { @@ -906,9 +925,15 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re tagValue := f.Tag.Get(d.config.TagName) keyName := f.Name + if tagValue == "" && d.config.IgnoreUntaggedFields { + continue + } + // If Squash is set in the config, we squash the field down. squash := d.config.Squash && v.Kind() == reflect.Struct && f.Anonymous + v = dereferencePtrToStructIfNeeded(v, d.config.TagName) + // Determine the name of the key in the map if index := strings.Index(tagValue, ","); index != -1 { if tagValue[:index] == "-" { @@ -920,7 +945,7 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re } // If "squash" is specified in the tag, we squash the field down. - squash = !squash && strings.Index(tagValue[index+1:], "squash") != -1 + squash = squash || strings.Index(tagValue[index+1:], "squash") != -1 if squash { // When squashing, the embedded type can be a pointer to a struct. if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct { @@ -932,7 +957,9 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re return fmt.Errorf("cannot squash non-struct type '%s'", v.Type()) } } - keyName = tagValue[:index] + if keyNameTagValue := tagValue[:index]; keyNameTagValue != "" { + keyName = keyNameTagValue + } } else if len(tagValue) > 0 { if tagValue == "-" { continue @@ -1088,7 +1115,7 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) } // If the input value is nil, then don't allocate since empty != nil - if dataVal.IsNil() { + if dataValKind != reflect.Array && dataVal.IsNil() { return nil } @@ -1250,6 +1277,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e dataValKeysUnused[dataValKey.Interface()] = struct{}{} } + targetValKeysUnused := make(map[interface{}]struct{}) errors := make([]string, 0) // This slice will keep track of all the structs we'll be decoding. @@ -1354,7 +1382,8 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e if !rawMapVal.IsValid() { // There was no matching key in the map for the value in - // the struct. Just ignore. + // the struct. Remember it for potential errors and metadata. + targetValKeysUnused[fieldName] = struct{}{} continue } } @@ -1414,6 +1443,17 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e errors = appendErrors(errors, err) } + if d.config.ErrorUnset && len(targetValKeysUnused) > 0 { + keys := make([]string, 0, len(targetValKeysUnused)) + for rawKey := range targetValKeysUnused { + keys = append(keys, rawKey.(string)) + } + sort.Strings(keys) + + err := fmt.Errorf("'%s' has unset fields: %s", name, strings.Join(keys, ", ")) + errors = appendErrors(errors, err) + } + if len(errors) > 0 { return &Error{errors} } @@ -1428,6 +1468,14 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e d.config.Metadata.Unused = append(d.config.Metadata.Unused, key) } + for rawKey := range targetValKeysUnused { + key := rawKey.(string) + if name != "" { + key = name + "." + key + } + + d.config.Metadata.Unset = append(d.config.Metadata.Unset, key) + } } return nil @@ -1465,3 +1513,28 @@ func getKind(val reflect.Value) reflect.Kind { return kind } } + +func isStructTypeConvertibleToMap(typ reflect.Type, checkMapstructureTags bool, tagName string) bool { + for i := 0; i < typ.NumField(); i++ { + f := typ.Field(i) + if f.PkgPath == "" && !checkMapstructureTags { // check for unexported fields + return true + } + if checkMapstructureTags && f.Tag.Get(tagName) != "" { // check for mapstructure tags inside + return true + } + } + return false +} + +func dereferencePtrToStructIfNeeded(v reflect.Value, tagName string) reflect.Value { + if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { + return v + } + deref := v.Elem() + derefT := deref.Type() + if isStructTypeConvertibleToMap(derefT, true, tagName) { + return deref + } + return v +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare.go b/vendor/github.com/stretchr/testify/assert/assertion_compare.go index 41649d267..3bb22a971 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_compare.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare.go @@ -3,6 +3,7 @@ package assert import ( "fmt" "reflect" + "time" ) type CompareType int @@ -30,6 +31,8 @@ var ( float64Type = reflect.TypeOf(float64(1)) stringType = reflect.TypeOf("") + + timeType = reflect.TypeOf(time.Time{}) ) func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { @@ -299,6 +302,27 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { return compareLess, true } } + // Check for known struct types we can check for compare results. + case reflect.Struct: + { + // All structs enter here. We're not interested in most types. + if !canConvert(obj1Value, timeType) { + break + } + + // time.Time can compared! + timeObj1, ok := obj1.(time.Time) + if !ok { + timeObj1 = obj1Value.Convert(timeType).Interface().(time.Time) + } + + timeObj2, ok := obj2.(time.Time) + if !ok { + timeObj2 = obj2Value.Convert(timeType).Interface().(time.Time) + } + + return compare(timeObj1.UnixNano(), timeObj2.UnixNano(), reflect.Int64) + } } return compareEqual, false @@ -310,7 +334,10 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { // assert.Greater(t, float64(2), float64(1)) // assert.Greater(t, "b", "a") func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) } // GreaterOrEqual asserts that the first element is greater than or equal to the second @@ -320,7 +347,10 @@ func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface // assert.GreaterOrEqual(t, "b", "a") // assert.GreaterOrEqual(t, "b", "b") func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) } // Less asserts that the first element is less than the second @@ -329,7 +359,10 @@ func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...in // assert.Less(t, float64(1), float64(2)) // assert.Less(t, "a", "b") func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) } // LessOrEqual asserts that the first element is less than or equal to the second @@ -339,7 +372,10 @@ func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) // assert.LessOrEqual(t, "a", "b") // assert.LessOrEqual(t, "b", "b") func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) } // Positive asserts that the specified element is positive @@ -347,8 +383,11 @@ func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...inter // assert.Positive(t, 1) // assert.Positive(t, 1.23) func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } zero := reflect.Zero(reflect.TypeOf(e)) - return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs) + return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs...) } // Negative asserts that the specified element is negative @@ -356,8 +395,11 @@ func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { // assert.Negative(t, -1) // assert.Negative(t, -1.23) func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } zero := reflect.Zero(reflect.TypeOf(e)) - return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs) + return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs...) } func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go b/vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go new file mode 100644 index 000000000..df22c47fc --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go @@ -0,0 +1,16 @@ +//go:build go1.17 +// +build go1.17 + +// TODO: once support for Go 1.16 is dropped, this file can be +// merged/removed with assertion_compare_go1.17_test.go and +// assertion_compare_legacy.go + +package assert + +import "reflect" + +// Wrapper around reflect.Value.CanConvert, for compatability +// reasons. +func canConvert(value reflect.Value, to reflect.Type) bool { + return value.CanConvert(to) +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go b/vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go new file mode 100644 index 000000000..1701af2a3 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go @@ -0,0 +1,16 @@ +//go:build !go1.17 +// +build !go1.17 + +// TODO: once support for Go 1.16 is dropped, this file can be +// merged/removed with assertion_compare_go1.17_test.go and +// assertion_compare_can_convert.go + +package assert + +import "reflect" + +// Older versions of Go does not have the reflect.Value.CanConvert +// method. +func canConvert(value reflect.Value, to reflect.Type) bool { + return false +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_format.go b/vendor/github.com/stretchr/testify/assert/assertion_format.go index 4dfd1229a..27e2420ed 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_format.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_format.go @@ -123,6 +123,18 @@ func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...int return ErrorAs(t, err, target, append([]interface{}{msg}, args...)...) } +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// assert.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted") +func ErrorContainsf(t TestingT, theError error, contains string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return ErrorContains(t, theError, contains, append([]interface{}{msg}, args...)...) +} + // ErrorIsf asserts that at least one of the errors in err's chain matches target. // This is a wrapper for errors.Is. func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go b/vendor/github.com/stretchr/testify/assert/assertion_forward.go index 25337a6f0..d9ea368d0 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_forward.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_forward.go @@ -222,6 +222,30 @@ func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args .. return ErrorAsf(a.t, err, target, msg, args...) } +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContains(err, expectedErrorSubString) +func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorContains(a.t, theError, contains, msgAndArgs...) +} + +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContainsf(err, expectedErrorSubString, "error message %s", "formatted") +func (a *Assertions) ErrorContainsf(theError error, contains string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorContainsf(a.t, theError, contains, msg, args...) +} + // ErrorIs asserts that at least one of the errors in err's chain matches target. // This is a wrapper for errors.Is. func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) bool { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_order.go b/vendor/github.com/stretchr/testify/assert/assertion_order.go index 1c3b47182..759448783 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_order.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_order.go @@ -50,7 +50,7 @@ func isOrdered(t TestingT, object interface{}, allowedComparesResults []CompareT // assert.IsIncreasing(t, []float{1, 2}) // assert.IsIncreasing(t, []string{"a", "b"}) func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) } // IsNonIncreasing asserts that the collection is not increasing @@ -59,7 +59,7 @@ func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonIncreasing(t, []float{2, 1}) // assert.IsNonIncreasing(t, []string{"b", "a"}) func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) } // IsDecreasing asserts that the collection is decreasing @@ -68,7 +68,7 @@ func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) // assert.IsDecreasing(t, []float{2, 1}) // assert.IsDecreasing(t, []string{"b", "a"}) func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) } // IsNonDecreasing asserts that the collection is not decreasing @@ -77,5 +77,5 @@ func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonDecreasing(t, []float{1, 2}) // assert.IsNonDecreasing(t, []string{"a", "b"}) func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) } diff --git a/vendor/github.com/stretchr/testify/assert/assertions.go b/vendor/github.com/stretchr/testify/assert/assertions.go index bcac4401f..0357b2231 100644 --- a/vendor/github.com/stretchr/testify/assert/assertions.go +++ b/vendor/github.com/stretchr/testify/assert/assertions.go @@ -718,10 +718,14 @@ func NotEqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...inte // return (false, false) if impossible. // return (true, false) if element was not found. // return (true, true) if element was found. -func includeElement(list interface{}, element interface{}) (ok, found bool) { +func containsElement(list interface{}, element interface{}) (ok, found bool) { listValue := reflect.ValueOf(list) - listKind := reflect.TypeOf(list).Kind() + listType := reflect.TypeOf(list) + if listType == nil { + return false, false + } + listKind := listType.Kind() defer func() { if e := recover(); e != nil { ok = false @@ -764,7 +768,7 @@ func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bo h.Helper() } - ok, found := includeElement(s, contains) + ok, found := containsElement(s, contains) if !ok { return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", s), msgAndArgs...) } @@ -787,7 +791,7 @@ func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) h.Helper() } - ok, found := includeElement(s, contains) + ok, found := containsElement(s, contains) if !ok { return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...) } @@ -831,7 +835,7 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok for i := 0; i < subsetValue.Len(); i++ { element := subsetValue.Index(i).Interface() - ok, found := includeElement(list, element) + ok, found := containsElement(list, element) if !ok { return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) } @@ -852,7 +856,7 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) h.Helper() } if subset == nil { - return Fail(t, fmt.Sprintf("nil is the empty set which is a subset of every set"), msgAndArgs...) + return Fail(t, "nil is the empty set which is a subset of every set", msgAndArgs...) } subsetValue := reflect.ValueOf(subset) @@ -875,7 +879,7 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) for i := 0; i < subsetValue.Len(); i++ { element := subsetValue.Index(i).Interface() - ok, found := includeElement(list, element) + ok, found := containsElement(list, element) if !ok { return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) } @@ -1000,27 +1004,21 @@ func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool { type PanicTestFunc func() // didPanic returns true if the function passed to it panics. Otherwise, it returns false. -func didPanic(f PanicTestFunc) (bool, interface{}, string) { - - didPanic := false - var message interface{} - var stack string - func() { - - defer func() { - if message = recover(); message != nil { - didPanic = true - stack = string(debug.Stack()) - } - }() - - // call the target function - f() +func didPanic(f PanicTestFunc) (didPanic bool, message interface{}, stack string) { + didPanic = true + defer func() { + message = recover() + if didPanic { + stack = string(debug.Stack()) + } }() - return didPanic, message, stack + // call the target function + f() + didPanic = false + return } // Panics asserts that the code inside the specified PanicTestFunc panics. @@ -1161,11 +1159,15 @@ func InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs bf, bok := toFloat(actual) if !aok || !bok { - return Fail(t, fmt.Sprintf("Parameters must be numerical"), msgAndArgs...) + return Fail(t, "Parameters must be numerical", msgAndArgs...) + } + + if math.IsNaN(af) && math.IsNaN(bf) { + return true } if math.IsNaN(af) { - return Fail(t, fmt.Sprintf("Expected must not be NaN"), msgAndArgs...) + return Fail(t, "Expected must not be NaN", msgAndArgs...) } if math.IsNaN(bf) { @@ -1188,7 +1190,7 @@ func InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAn if expected == nil || actual == nil || reflect.TypeOf(actual).Kind() != reflect.Slice || reflect.TypeOf(expected).Kind() != reflect.Slice { - return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + return Fail(t, "Parameters must be slice", msgAndArgs...) } actualSlice := reflect.ValueOf(actual) @@ -1250,8 +1252,12 @@ func InDeltaMapValues(t TestingT, expected, actual interface{}, delta float64, m func calcRelativeError(expected, actual interface{}) (float64, error) { af, aok := toFloat(expected) - if !aok { - return 0, fmt.Errorf("expected value %q cannot be converted to float", expected) + bf, bok := toFloat(actual) + if !aok || !bok { + return 0, fmt.Errorf("Parameters must be numerical") + } + if math.IsNaN(af) && math.IsNaN(bf) { + return 0, nil } if math.IsNaN(af) { return 0, errors.New("expected value must not be NaN") @@ -1259,10 +1265,6 @@ func calcRelativeError(expected, actual interface{}) (float64, error) { if af == 0 { return 0, fmt.Errorf("expected value must have a value other than zero to calculate the relative error") } - bf, bok := toFloat(actual) - if !bok { - return 0, fmt.Errorf("actual value %q cannot be converted to float", actual) - } if math.IsNaN(bf) { return 0, errors.New("actual value must not be NaN") } @@ -1298,7 +1300,7 @@ func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, m if expected == nil || actual == nil || reflect.TypeOf(actual).Kind() != reflect.Slice || reflect.TypeOf(expected).Kind() != reflect.Slice { - return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + return Fail(t, "Parameters must be slice", msgAndArgs...) } actualSlice := reflect.ValueOf(actual) @@ -1375,6 +1377,27 @@ func EqualError(t TestingT, theError error, errString string, msgAndArgs ...inte return true } +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// assert.ErrorContains(t, err, expectedErrorSubString) +func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if !Error(t, theError, msgAndArgs...) { + return false + } + + actual := theError.Error() + if !strings.Contains(actual, contains) { + return Fail(t, fmt.Sprintf("Error %#v does not contain %#v", actual, contains), msgAndArgs...) + } + + return true +} + // matchRegexp return true if a specified regexp matches a string. func matchRegexp(rx interface{}, str interface{}) bool { @@ -1588,12 +1611,17 @@ func diff(expected interface{}, actual interface{}) string { } var e, a string - if et != reflect.TypeOf("") { - e = spewConfig.Sdump(expected) - a = spewConfig.Sdump(actual) - } else { + + switch et { + case reflect.TypeOf(""): e = reflect.ValueOf(expected).String() a = reflect.ValueOf(actual).String() + case reflect.TypeOf(time.Time{}): + e = spewConfigStringerEnabled.Sdump(expected) + a = spewConfigStringerEnabled.Sdump(actual) + default: + e = spewConfig.Sdump(expected) + a = spewConfig.Sdump(actual) } diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ @@ -1625,6 +1653,14 @@ var spewConfig = spew.ConfigState{ MaxDepth: 10, } +var spewConfigStringerEnabled = spew.ConfigState{ + Indent: " ", + DisablePointerAddresses: true, + DisableCapacities: true, + SortKeys: true, + MaxDepth: 10, +} + type tHelper interface { Helper() } diff --git a/vendor/github.com/stretchr/testify/mock/mock.go b/vendor/github.com/stretchr/testify/mock/mock.go index e2e6a2d23..853da6cce 100644 --- a/vendor/github.com/stretchr/testify/mock/mock.go +++ b/vendor/github.com/stretchr/testify/mock/mock.go @@ -221,6 +221,14 @@ type Mock struct { mutex sync.Mutex } +// String provides a %v format string for Mock. +// Note: this is used implicitly by Arguments.Diff if a Mock is passed. +// It exists because go's default %v formatting traverses the struct +// without acquiring the mutex, which is detected by go test -race. +func (m *Mock) String() string { + return fmt.Sprintf("%[1]T<%[1]p>", m) +} + // TestData holds any data that might be useful for testing. Testify ignores // this data completely allowing you to do whatever you like with it. func (m *Mock) TestData() objx.Map { @@ -720,7 +728,7 @@ func (f argumentMatcher) Matches(argument interface{}) bool { } func (f argumentMatcher) String() string { - return fmt.Sprintf("func(%s) bool", f.fn.Type().In(0).Name()) + return fmt.Sprintf("func(%s) bool", f.fn.Type().In(0).String()) } // MatchedBy can be used to match a mock call based on only certain properties diff --git a/vendor/github.com/urfave/cli/v2/.gitignore b/vendor/github.com/urfave/cli/v2/.gitignore index afdca418c..c04fcc538 100644 --- a/vendor/github.com/urfave/cli/v2/.gitignore +++ b/vendor/github.com/urfave/cli/v2/.gitignore @@ -1,9 +1,10 @@ *.coverprofile *.orig -node_modules/ vendor .idea internal/*/built-example coverage.txt +/.local/ +/site/ *.exe diff --git a/vendor/github.com/urfave/cli/v2/CODE_OF_CONDUCT.md b/vendor/github.com/urfave/cli/v2/CODE_OF_CONDUCT.md index 41ba294f6..9fee14807 100644 --- a/vendor/github.com/urfave/cli/v2/CODE_OF_CONDUCT.md +++ b/vendor/github.com/urfave/cli/v2/CODE_OF_CONDUCT.md @@ -55,11 +55,12 @@ further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting Dan Buch at dan@meatballhat.com. All complaints will be -reviewed and investigated and will result in a response that is deemed necessary -and appropriate to the circumstances. The project team is obligated to maintain -confidentiality with regard to the reporter of an incident. Further details of -specific enforcement policies may be posted separately. +reported by contacting urfave-governance@googlegroups.com, a members-only group +that is world-postable. All complaints will be reviewed and investigated and +will result in a response that is deemed necessary and appropriate to the +circumstances. The project team is obligated to maintain confidentiality with +regard to the reporter of an incident. Further details of specific enforcement +policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other diff --git a/vendor/github.com/urfave/cli/v2/LICENSE b/vendor/github.com/urfave/cli/v2/LICENSE index 42a597e29..2c84c78a1 100644 --- a/vendor/github.com/urfave/cli/v2/LICENSE +++ b/vendor/github.com/urfave/cli/v2/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 Jeremy Saenz & Contributors +Copyright (c) 2022 urfave/cli maintainers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/vendor/github.com/urfave/cli/v2/Makefile b/vendor/github.com/urfave/cli/v2/Makefile new file mode 100644 index 000000000..3b0e5e0bb --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/Makefile @@ -0,0 +1,40 @@ +# NOTE: this Makefile is meant to provide a simplified entry point for humans to +# run all of the critical steps to verify one's changes are harmonious in +# nature. Keeping target bodies to one line each and abstaining from make magic +# are very important so that maintainers and contributors can focus their +# attention on files that are primarily Go. + +.PHONY: all +all: generate vet tag-test test check-binary-size tag-check-binary-size gfmrun v2diff + +# NOTE: this is a special catch-all rule to run any of the commands +# defined in internal/build/build.go with optional arguments passed +# via GFLAGS (global flags) and FLAGS (command-specific flags), e.g.: +# +# $ make test GFLAGS='--packages cli' +%: + go run internal/build/build.go $(GFLAGS) $* $(FLAGS) + +.PHONY: tag-test +tag-test: + go run internal/build/build.go -tags urfave_cli_no_docs test + +.PHONY: tag-check-binary-size +tag-check-binary-size: + go run internal/build/build.go -tags urfave_cli_no_docs check-binary-size + +.PHONY: gfmrun +gfmrun: + go run internal/build/build.go gfmrun docs/v2/manual.md + +.PHONY: docs +docs: + mkdocs build + +.PHONY: docs-deps +docs-deps: + pip install -r mkdocs-requirements.txt + +.PHONY: serve-docs +serve-docs: + mkdocs serve diff --git a/vendor/github.com/urfave/cli/v2/README.md b/vendor/github.com/urfave/cli/v2/README.md index 2b74f1f2c..eaed35630 100644 --- a/vendor/github.com/urfave/cli/v2/README.md +++ b/vendor/github.com/urfave/cli/v2/README.md @@ -1,70 +1,19 @@ -cli -=== +# cli [![GoDoc](https://godoc.org/github.com/urfave/cli?status.svg)](https://pkg.go.dev/github.com/urfave/cli/v2) [![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-urfave-cli) [![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli) -[![codecov](https://codecov.io/gh/urfave/cli/branch/master/graph/badge.svg)](https://codecov.io/gh/urfave/cli) +[![codecov](https://codecov.io/gh/urfave/cli/branch/main/graph/badge.svg)](https://codecov.io/gh/urfave/cli) cli is a simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way. -## Usage Documentation +## Documentation -Usage documentation exists for each major version. Don't know what version you're on? You're probably using the version from the `master` branch, which is currently `v2`. +More documentation is available in [`./docs`](./docs) or the hosted +documentation site at . -- `v2` - [./docs/v2/manual.md](./docs/v2/manual.md) -- `v1` - [./docs/v1/manual.md](./docs/v1/manual.md) +## License -Guides for migrating to newer versions: - -- `v1-to-v2` - [./docs/migrate-v1-to-v2.md](./docs/migrate-v1-to-v2.md) - -## Installation - -Using this package requires a working Go environment. [See the install instructions for Go](http://golang.org/doc/install.html). - -Go Modules are required when using this package. [See the go blog guide on using Go Modules](https://blog.golang.org/using-go-modules). - -### Using `v2` releases - -``` -$ GO111MODULE=on go get github.com/urfave/cli/v2 -``` - -```go -... -import ( - "github.com/urfave/cli/v2" // imports as package "cli" -) -... -``` - -### Using `v1` releases - -``` -$ GO111MODULE=on go get github.com/urfave/cli -``` - -```go -... -import ( - "github.com/urfave/cli" -) -... -``` - -### GOPATH - -Make sure your `PATH` includes the `$GOPATH/bin` directory so your commands can -be easily used: -``` -export PATH=$PATH:$GOPATH/bin -``` - -### Supported platforms - -cli is tested against multiple versions of Go on Linux, and against the latest -released version of Go on OS X and Windows. This project uses Github Actions for -builds. To see our currently supported go versions and platforms, look at the [./.github/workflows/cli.yml](https://github.com/urfave/cli/blob/master/.github/workflows/cli.yml). +See [`LICENSE`](./LICENSE) diff --git a/vendor/github.com/urfave/cli/v2/app.go b/vendor/github.com/urfave/cli/v2/app.go index 5c616e6f5..333bd57b0 100644 --- a/vendor/github.com/urfave/cli/v2/app.go +++ b/vendor/github.com/urfave/cli/v2/app.go @@ -11,13 +11,19 @@ import ( "time" ) +const suggestDidYouMeanTemplate = "Did you mean %q?" + var ( - changeLogURL = "https://github.com/urfave/cli/blob/master/docs/CHANGELOG.md" + changeLogURL = "https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md" appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL) contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you." errInvalidActionType = NewExitError("ERROR invalid Action type. "+ fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+ fmt.Sprintf("See %s", appActionDeprecationURL), 2) + + SuggestFlag SuggestFlagFunc = suggestFlag + SuggestCommand SuggestCommandFunc = suggestCommand + SuggestDidYouMeanTemplate string = suggestDidYouMeanTemplate ) // App is the main structure of a cli application. It is recommended that @@ -52,6 +58,8 @@ type App struct { HideVersion bool // categories contains the categorized commands and is populated on app startup categories CommandCategories + // flagCategories contains the categorized flags and is populated on app startup + flagCategories FlagCategories // An action to execute when the shell completion flag is set BashComplete BashCompleteFunc // An action to execute before any subcommands are run, but after the context is ready @@ -94,10 +102,16 @@ type App struct { // single-character bool arguments into one // i.e. foobar -o -v -> foobar -ov UseShortOptionHandling bool + // Enable suggestions for commands and flags + Suggest bool didSetup bool } +type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string + +type SuggestCommandFunc func(commands []*Command, provided string) string + // Tries to find out when this binary was compiled. // Returns the current time if it fails to find it. func compileTime() time.Time { @@ -181,6 +195,8 @@ func (a *App) Setup() { if c.HelpName == "" { c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) } + + c.flagCategories = newFlagCategoriesFromFlags(c.Flags) newCommands = append(newCommands, c) } a.Commands = newCommands @@ -205,6 +221,13 @@ func (a *App) Setup() { } sort.Sort(a.categories.(*commandCategories)) + a.flagCategories = newFlagCategories() + for _, fl := range a.Flags { + if cf, ok := fl.(CategorizableFlag); ok { + a.flagCategories.AddFlag(cf.GetCategory(), cf) + } + } + if a.Metadata == nil { a.Metadata = make(map[string]interface{}) } @@ -245,48 +268,53 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) { err = parseIter(set, a, arguments[1:], shellComplete) nerr := normalizeFlags(a.Flags, set) - context := NewContext(a, set, &Context{Context: ctx}) + cCtx := NewContext(a, set, &Context{Context: ctx}) if nerr != nil { _, _ = fmt.Fprintln(a.Writer, nerr) - _ = ShowAppHelp(context) + _ = ShowAppHelp(cCtx) return nerr } - context.shellComplete = shellComplete + cCtx.shellComplete = shellComplete - if checkCompletions(context) { + if checkCompletions(cCtx) { return nil } if err != nil { if a.OnUsageError != nil { - err := a.OnUsageError(context, err, false) - a.handleExitCoder(context, err) + err := a.OnUsageError(cCtx, err, false) + a.handleExitCoder(cCtx, err) return err } _, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) - _ = ShowAppHelp(context) + if a.Suggest { + if suggestion, err := a.suggestFlagFromError(err, ""); err == nil { + fmt.Fprintf(a.Writer, suggestion) + } + } + _ = ShowAppHelp(cCtx) return err } - if !a.HideHelp && checkHelp(context) { - _ = ShowAppHelp(context) + if !a.HideHelp && checkHelp(cCtx) { + _ = ShowAppHelp(cCtx) return nil } - if !a.HideVersion && checkVersion(context) { - ShowVersion(context) + if !a.HideVersion && checkVersion(cCtx) { + ShowVersion(cCtx) return nil } - cerr := context.checkRequiredFlags(a.Flags) + cerr := cCtx.checkRequiredFlags(a.Flags) if cerr != nil { - _ = ShowAppHelp(context) + _ = ShowAppHelp(cCtx) return cerr } if a.After != nil { defer func() { - if afterErr := a.After(context); afterErr != nil { + if afterErr := a.After(cCtx); afterErr != nil { if err != nil { err = newMultiError(err, afterErr) } else { @@ -297,20 +325,20 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) { } if a.Before != nil { - beforeErr := a.Before(context) + beforeErr := a.Before(cCtx) if beforeErr != nil { - a.handleExitCoder(context, beforeErr) + a.handleExitCoder(cCtx, beforeErr) err = beforeErr return err } } - args := context.Args() + args := cCtx.Args() if args.Present() { name := args.First() c := a.Command(name) if c != nil { - return c.Run(context) + return c.Run(cCtx) } } @@ -319,12 +347,35 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) { } // Run default Action - err = a.Action(context) + err = a.Action(cCtx) - a.handleExitCoder(context, err) + a.handleExitCoder(cCtx, err) return err } +func (a *App) suggestFlagFromError(err error, command string) (string, error) { + flag, parseErr := flagFromError(err) + if parseErr != nil { + return "", err + } + + flags := a.Flags + if command != "" { + cmd := a.Command(command) + if cmd == nil { + return "", err + } + flags = cmd.Flags + } + + suggestion := SuggestFlag(flags, flag, a.HideHelp) + if len(suggestion) == 0 { + return "", err + } + + return fmt.Sprintf(SuggestDidYouMeanTemplate+"\n\n", suggestion), nil +} + // RunAndExitOnError calls .Run() and exits non-zero if an error was returned // // Deprecated: instead you should return an error that fulfills cli.ExitCoder @@ -359,55 +410,60 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { err = parseIter(set, a, ctx.Args().Tail(), ctx.shellComplete) nerr := normalizeFlags(a.Flags, set) - context := NewContext(a, set, ctx) + cCtx := NewContext(a, set, ctx) if nerr != nil { _, _ = fmt.Fprintln(a.Writer, nerr) _, _ = fmt.Fprintln(a.Writer) if len(a.Commands) > 0 { - _ = ShowSubcommandHelp(context) + _ = ShowSubcommandHelp(cCtx) } else { - _ = ShowCommandHelp(ctx, context.Args().First()) + _ = ShowCommandHelp(ctx, cCtx.Args().First()) } return nerr } - if checkCompletions(context) { + if checkCompletions(cCtx) { return nil } if err != nil { if a.OnUsageError != nil { - err = a.OnUsageError(context, err, true) - a.handleExitCoder(context, err) + err = a.OnUsageError(cCtx, err, true) + a.handleExitCoder(cCtx, err) return err } _, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) - _ = ShowSubcommandHelp(context) + if a.Suggest { + if suggestion, err := a.suggestFlagFromError(err, cCtx.Command.Name); err == nil { + fmt.Fprintf(a.Writer, suggestion) + } + } + _ = ShowSubcommandHelp(cCtx) return err } if len(a.Commands) > 0 { - if checkSubcommandHelp(context) { + if checkSubcommandHelp(cCtx) { return nil } } else { - if checkCommandHelp(ctx, context.Args().First()) { + if checkCommandHelp(ctx, cCtx.Args().First()) { return nil } } - cerr := context.checkRequiredFlags(a.Flags) + cerr := cCtx.checkRequiredFlags(a.Flags) if cerr != nil { - _ = ShowSubcommandHelp(context) + _ = ShowSubcommandHelp(cCtx) return cerr } if a.After != nil { defer func() { - afterErr := a.After(context) + afterErr := a.After(cCtx) if afterErr != nil { - a.handleExitCoder(context, err) + a.handleExitCoder(cCtx, err) if err != nil { err = newMultiError(err, afterErr) } else { @@ -418,27 +474,27 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } if a.Before != nil { - beforeErr := a.Before(context) + beforeErr := a.Before(cCtx) if beforeErr != nil { - a.handleExitCoder(context, beforeErr) + a.handleExitCoder(cCtx, beforeErr) err = beforeErr return err } } - args := context.Args() + args := cCtx.Args() if args.Present() { name := args.First() c := a.Command(name) if c != nil { - return c.Run(context) + return c.Run(cCtx) } } // Run default Action - err = a.Action(context) + err = a.Action(cCtx) - a.handleExitCoder(context, err) + a.handleExitCoder(cCtx, err) return err } @@ -481,6 +537,14 @@ func (a *App) VisibleCommands() []*Command { return ret } +// VisibleFlagCategories returns a slice containing all the categories with the flags they contain +func (a *App) VisibleFlagCategories() []VisibleFlagCategory { + if a.flagCategories == nil { + return []VisibleFlagCategory{} + } + return a.flagCategories.VisibleCategories() +} + // VisibleFlags returns a slice of the Flags with Hidden=false func (a *App) VisibleFlags() []Flag { return visibleFlags(a.Flags) @@ -498,9 +562,9 @@ func (a *App) appendCommand(c *Command) { } } -func (a *App) handleExitCoder(context *Context, err error) { +func (a *App) handleExitCoder(cCtx *Context, err error) { if a.ExitErrHandler != nil { - a.ExitErrHandler(context, err) + a.ExitErrHandler(cCtx, err) } else { HandleExitCoder(err) } @@ -525,14 +589,14 @@ func (a *Author) String() string { // HandleAction attempts to figure out which Action signature was used. If // it's an ActionFunc or a func with the legacy signature for Action, the func // is run! -func HandleAction(action interface{}, context *Context) (err error) { +func HandleAction(action interface{}, cCtx *Context) (err error) { switch a := action.(type) { case ActionFunc: - return a(context) + return a(cCtx) case func(*Context) error: - return a(context) + return a(cCtx) case func(*Context): // deprecated function signature - a(context) + a(cCtx) return nil } diff --git a/vendor/github.com/urfave/cli/v2/category.go b/vendor/github.com/urfave/cli/v2/category.go index 867e3908c..8bf325e20 100644 --- a/vendor/github.com/urfave/cli/v2/category.go +++ b/vendor/github.com/urfave/cli/v2/category.go @@ -1,10 +1,12 @@ package cli +import "sort" + // CommandCategories interface allows for category manipulation type CommandCategories interface { // AddCommand adds a command to a category, creating a new category if necessary. AddCommand(category string, command *Command) - // categories returns a copy of the category slice + // Categories returns a slice of categories sorted by name Categories() []CommandCategory } @@ -77,3 +79,93 @@ func (c *commandCategory) VisibleCommands() []*Command { } return ret } + +// FlagCategories interface allows for category manipulation +type FlagCategories interface { + // AddFlags adds a flag to a category, creating a new category if necessary. + AddFlag(category string, fl Flag) + // VisibleCategories returns a slice of visible flag categories sorted by name + VisibleCategories() []VisibleFlagCategory +} + +type defaultFlagCategories struct { + m map[string]*defaultVisibleFlagCategory +} + +func newFlagCategories() FlagCategories { + return &defaultFlagCategories{ + m: map[string]*defaultVisibleFlagCategory{}, + } +} + +func newFlagCategoriesFromFlags(fs []Flag) FlagCategories { + fc := newFlagCategories() + for _, fl := range fs { + if cf, ok := fl.(CategorizableFlag); ok { + fc.AddFlag(cf.GetCategory(), cf) + } + } + + return fc +} + +func (f *defaultFlagCategories) AddFlag(category string, fl Flag) { + if _, ok := f.m[category]; !ok { + f.m[category] = &defaultVisibleFlagCategory{name: category, m: map[string]Flag{}} + } + + f.m[category].m[fl.String()] = fl +} + +func (f *defaultFlagCategories) VisibleCategories() []VisibleFlagCategory { + catNames := []string{} + for name := range f.m { + catNames = append(catNames, name) + } + + sort.Strings(catNames) + + ret := make([]VisibleFlagCategory, len(catNames)) + for i, name := range catNames { + ret[i] = f.m[name] + } + + return ret +} + +// VisibleFlagCategory is a category containing flags. +type VisibleFlagCategory interface { + // Name returns the category name string + Name() string + // Flags returns a slice of VisibleFlag sorted by name + Flags() []VisibleFlag +} + +type defaultVisibleFlagCategory struct { + name string + m map[string]Flag +} + +func (fc *defaultVisibleFlagCategory) Name() string { + return fc.name +} + +func (fc *defaultVisibleFlagCategory) Flags() []VisibleFlag { + vfNames := []string{} + for flName, fl := range fc.m { + if vf, ok := fl.(VisibleFlag); ok { + if vf.IsVisible() { + vfNames = append(vfNames, flName) + } + } + } + + sort.Strings(vfNames) + + ret := make([]VisibleFlag, len(vfNames)) + for i, flName := range vfNames { + ret[i] = fc.m[flName].(VisibleFlag) + } + + return ret +} diff --git a/vendor/github.com/urfave/cli/v2/cli.go b/vendor/github.com/urfave/cli/v2/cli.go index 62a5bc22d..2a11c5ad4 100644 --- a/vendor/github.com/urfave/cli/v2/cli.go +++ b/vendor/github.com/urfave/cli/v2/cli.go @@ -20,4 +20,4 @@ // } package cli -//go:generate go run flag-gen/main.go flag-gen/assets_vfsdata.go +//go:generate go run internal/genflags/cmd/genflags/main.go diff --git a/vendor/github.com/urfave/cli/v2/command.go b/vendor/github.com/urfave/cli/v2/command.go index 3477686bb..2cafd8e0e 100644 --- a/vendor/github.com/urfave/cli/v2/command.go +++ b/vendor/github.com/urfave/cli/v2/command.go @@ -38,7 +38,8 @@ type Command struct { // List of child commands Subcommands []*Command // List of flags to parse - Flags []Flag + Flags []Flag + flagCategories FlagCategories // Treat all flags as normal arguments if true SkipFlagParsing bool // Boolean to hide built-in help command and help flag @@ -105,39 +106,44 @@ func (c *Command) Run(ctx *Context) (err error) { set, err := c.parseFlags(ctx.Args(), ctx.shellComplete) - context := NewContext(ctx.App, set, ctx) - context.Command = c - if checkCommandCompletions(context, c.Name) { + cCtx := NewContext(ctx.App, set, ctx) + cCtx.Command = c + if checkCommandCompletions(cCtx, c.Name) { return nil } if err != nil { if c.OnUsageError != nil { - err = c.OnUsageError(context, err, false) - context.App.handleExitCoder(context, err) + err = c.OnUsageError(cCtx, err, false) + cCtx.App.handleExitCoder(cCtx, err) return err } - _, _ = fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) - _, _ = fmt.Fprintln(context.App.Writer) - _ = ShowCommandHelp(context, c.Name) + _, _ = fmt.Fprintln(cCtx.App.Writer, "Incorrect Usage:", err.Error()) + _, _ = fmt.Fprintln(cCtx.App.Writer) + if ctx.App.Suggest { + if suggestion, err := ctx.App.suggestFlagFromError(err, c.Name); err == nil { + fmt.Fprintf(cCtx.App.Writer, suggestion) + } + } + _ = ShowCommandHelp(cCtx, c.Name) return err } - if checkCommandHelp(context, c.Name) { + if checkCommandHelp(cCtx, c.Name) { return nil } - cerr := context.checkRequiredFlags(c.Flags) + cerr := cCtx.checkRequiredFlags(c.Flags) if cerr != nil { - _ = ShowCommandHelp(context, c.Name) + _ = ShowCommandHelp(cCtx, c.Name) return cerr } if c.After != nil { defer func() { - afterErr := c.After(context) + afterErr := c.After(cCtx) if afterErr != nil { - context.App.handleExitCoder(context, err) + cCtx.App.handleExitCoder(cCtx, err) if err != nil { err = newMultiError(err, afterErr) } else { @@ -148,9 +154,9 @@ func (c *Command) Run(ctx *Context) (err error) { } if c.Before != nil { - err = c.Before(context) + err = c.Before(cCtx) if err != nil { - context.App.handleExitCoder(context, err) + cCtx.App.handleExitCoder(cCtx, err) return err } } @@ -159,11 +165,11 @@ func (c *Command) Run(ctx *Context) (err error) { c.Action = helpSubcommand.Action } - context.Command = c - err = c.Action(context) + cCtx.Command = c + err = c.Action(cCtx) if err != nil { - context.App.handleExitCoder(context, err) + cCtx.App.handleExitCoder(cCtx, err) } return err } @@ -249,6 +255,7 @@ func (c *Command) startApp(ctx *Context) error { app.ErrWriter = ctx.App.ErrWriter app.ExitErrHandler = ctx.App.ExitErrHandler app.UseShortOptionHandling = ctx.App.UseShortOptionHandling + app.Suggest = ctx.App.Suggest app.categories = newCommandCategories() for _, command := range c.Subcommands { @@ -280,6 +287,14 @@ func (c *Command) startApp(ctx *Context) error { return app.RunAsSubcommand(ctx) } +// VisibleFlagCategories returns a slice containing all the visible flag categories with the flags they contain +func (c *Command) VisibleFlagCategories() []VisibleFlagCategory { + if c.flagCategories == nil { + return []VisibleFlagCategory{} + } + return c.flagCategories.VisibleCategories() +} + // VisibleFlags returns a slice of the Flags with Hidden=false func (c *Command) VisibleFlags() []Flag { return visibleFlags(c.Flags) diff --git a/vendor/github.com/urfave/cli/v2/context.go b/vendor/github.com/urfave/cli/v2/context.go index da090e825..6b497ed20 100644 --- a/vendor/github.com/urfave/cli/v2/context.go +++ b/vendor/github.com/urfave/cli/v2/context.go @@ -40,18 +40,18 @@ func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context { } // NumFlags returns the number of flags set -func (c *Context) NumFlags() int { - return c.flagSet.NFlag() +func (cCtx *Context) NumFlags() int { + return cCtx.flagSet.NFlag() } // Set sets a context flag to a value. -func (c *Context) Set(name, value string) error { - return c.flagSet.Set(name, value) +func (cCtx *Context) Set(name, value string) error { + return cCtx.flagSet.Set(name, value) } // IsSet determines if the flag was actually set -func (c *Context) IsSet(name string) bool { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) IsSet(name string) bool { + if fs := cCtx.lookupFlagSet(name); fs != nil { isSet := false fs.Visit(func(f *flag.Flag) { if f.Name == name { @@ -62,7 +62,7 @@ func (c *Context) IsSet(name string) bool { return true } - f := c.lookupFlag(name) + f := cCtx.lookupFlag(name) if f == nil { return false } @@ -74,28 +74,28 @@ func (c *Context) IsSet(name string) bool { } // LocalFlagNames returns a slice of flag names used in this context. -func (c *Context) LocalFlagNames() []string { +func (cCtx *Context) LocalFlagNames() []string { var names []string - c.flagSet.Visit(makeFlagNameVisitor(&names)) + cCtx.flagSet.Visit(makeFlagNameVisitor(&names)) return names } // FlagNames returns a slice of flag names used by the this context and all of // its parent contexts. -func (c *Context) FlagNames() []string { +func (cCtx *Context) FlagNames() []string { var names []string - for _, ctx := range c.Lineage() { - ctx.flagSet.Visit(makeFlagNameVisitor(&names)) + for _, pCtx := range cCtx.Lineage() { + pCtx.flagSet.Visit(makeFlagNameVisitor(&names)) } return names } // Lineage returns *this* context and all of its ancestor contexts in order from // child to parent -func (c *Context) Lineage() []*Context { +func (cCtx *Context) Lineage() []*Context { var lineage []*Context - for cur := c; cur != nil; cur = cur.parentContext { + for cur := cCtx; cur != nil; cur = cur.parentContext { lineage = append(lineage, cur) } @@ -103,26 +103,26 @@ func (c *Context) Lineage() []*Context { } // Value returns the value of the flag corresponding to `name` -func (c *Context) Value(name string) interface{} { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) Value(name string) interface{} { + if fs := cCtx.lookupFlagSet(name); fs != nil { return fs.Lookup(name).Value.(flag.Getter).Get() } return nil } // Args returns the command line arguments associated with the context. -func (c *Context) Args() Args { - ret := args(c.flagSet.Args()) +func (cCtx *Context) Args() Args { + ret := args(cCtx.flagSet.Args()) return &ret } // NArg returns the number of the command line arguments. -func (c *Context) NArg() int { - return c.Args().Len() +func (cCtx *Context) NArg() int { + return cCtx.Args().Len() } -func (ctx *Context) lookupFlag(name string) Flag { - for _, c := range ctx.Lineage() { +func (cCtx *Context) lookupFlag(name string) Flag { + for _, c := range cCtx.Lineage() { if c.Command == nil { continue } @@ -136,8 +136,8 @@ func (ctx *Context) lookupFlag(name string) Flag { } } - if ctx.App != nil { - for _, f := range ctx.App.Flags { + if cCtx.App != nil { + for _, f := range cCtx.App.Flags { for _, n := range f.Names() { if n == name { return f @@ -149,8 +149,8 @@ func (ctx *Context) lookupFlag(name string) Flag { return nil } -func (ctx *Context) lookupFlagSet(name string) *flag.FlagSet { - for _, c := range ctx.Lineage() { +func (cCtx *Context) lookupFlagSet(name string) *flag.FlagSet { + for _, c := range cCtx.Lineage() { if c.flagSet == nil { continue } @@ -162,7 +162,7 @@ func (ctx *Context) lookupFlagSet(name string) *flag.FlagSet { return nil } -func (context *Context) checkRequiredFlags(flags []Flag) requiredFlagsErr { +func (cCtx *Context) checkRequiredFlags(flags []Flag) requiredFlagsErr { var missingFlags []string for _, f := range flags { if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() { @@ -174,7 +174,7 @@ func (context *Context) checkRequiredFlags(flags []Flag) requiredFlagsErr { flagName = key } - if context.IsSet(strings.TrimSpace(key)) { + if cCtx.IsSet(strings.TrimSpace(key)) { flagPresent = true } } diff --git a/vendor/github.com/urfave/cli/v2/docs.go b/vendor/github.com/urfave/cli/v2/docs.go index 9f82fc6b0..8b1c9c8a2 100644 --- a/vendor/github.com/urfave/cli/v2/docs.go +++ b/vendor/github.com/urfave/cli/v2/docs.go @@ -1,3 +1,6 @@ +//go:build !urfave_cli_no_docs +// +build !urfave_cli_no_docs + package cli import ( @@ -80,14 +83,14 @@ func prepareCommands(commands []*Command, level int) []string { usageText, ) - flags := prepareArgsWithValues(command.Flags) + flags := prepareArgsWithValues(command.VisibleFlags()) if len(flags) > 0 { prepared += fmt.Sprintf("\n%s", strings.Join(flags, "\n")) } coms = append(coms, prepared) - // recursevly iterate subcommands + // recursively iterate subcommands if len(command.Subcommands) > 0 { coms = append( coms, diff --git a/vendor/github.com/urfave/cli/v2/fish.go b/vendor/github.com/urfave/cli/v2/fish.go index 588e070ea..eec3253cd 100644 --- a/vendor/github.com/urfave/cli/v2/fish.go +++ b/vendor/github.com/urfave/cli/v2/fish.go @@ -95,7 +95,7 @@ func (a *App) prepareFishCommands(commands []*Command, allCommands *[]string, pr completions = append(completions, completion.String()) completions = append( completions, - a.prepareFishFlags(command.Flags, command.Names())..., + a.prepareFishFlags(command.VisibleFlags(), command.Names())..., ) // recursevly iterate subcommands diff --git a/vendor/github.com/urfave/cli/v2/flag-spec.yaml b/vendor/github.com/urfave/cli/v2/flag-spec.yaml new file mode 100644 index 000000000..d85fa30bd --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag-spec.yaml @@ -0,0 +1,50 @@ +# NOTE: this file is used by the tool defined in +# ./internal/genflags/cmd/genflags/main.go which uses the +# `genflags.Spec` type that maps to this file structure. + +flag_types: + bool: {} + float64: {} + int64: {} + int: {} + time.Duration: {} + uint64: {} + uint: {} + + string: + struct_fields: + - { name: TakesFile, type: bool } + Generic: + struct_fields: + - { name: TakesFile, type: bool } + Path: + struct_fields: + - { name: TakesFile, type: bool } + + Float64Slice: + value_pointer: true + skip_interfaces: + - fmt.Stringer + Int64Slice: + value_pointer: true + skip_interfaces: + - fmt.Stringer + IntSlice: + value_pointer: true + skip_interfaces: + - fmt.Stringer + StringSlice: + value_pointer: true + skip_interfaces: + - fmt.Stringer + struct_fields: + - { name: TakesFile, type: bool } + Timestamp: + value_pointer: true + struct_fields: + - { name: Layout, type: string } + + # TODO: enable UintSlice + # UintSlice: {} + # TODO: enable Uint64Slice once #1334 lands + # Uint64Slice: {} diff --git a/vendor/github.com/urfave/cli/v2/flag.go b/vendor/github.com/urfave/cli/v2/flag.go index 49682f299..dbed577cd 100644 --- a/vendor/github.com/urfave/cli/v2/flag.go +++ b/vendor/github.com/urfave/cli/v2/flag.go @@ -5,7 +5,6 @@ import ( "flag" "fmt" "io/ioutil" - "reflect" "regexp" "runtime" "strconv" @@ -117,6 +116,12 @@ type DocGenerationFlag interface { // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. GetValue() string + + // GetDefaultText returns the default text for this flag + GetDefaultText() string + + // GetEnvVars returns the env vars for this flag + GetEnvVars() []string } // VisibleFlag is an interface that allows to check if a flag is visible @@ -127,6 +132,14 @@ type VisibleFlag interface { IsVisible() bool } +// CategorizableFlag is an interface that allows us to potentially +// use a flag in a categorized representation. +type CategorizableFlag interface { + VisibleFlag + + GetCategory() string +} + func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { set := flag.NewFlagSet(name, flag.ContinueOnError) @@ -238,7 +251,7 @@ func prefixedNames(names []string, placeholder string) string { func withEnvHint(envVars []string, str string) string { envText := "" - if envVars != nil && len(envVars) > 0 { + if len(envVars) > 0 { prefix := "$" suffix := "" sep := ", $" @@ -253,7 +266,7 @@ func withEnvHint(envVars []string, str string) string { return str + envText } -func flagNames(name string, aliases []string) []string { +func FlagNames(name string, aliases []string) []string { var ret []string for _, part := range append([]string{name}, aliases...) { @@ -267,17 +280,6 @@ func flagNames(name string, aliases []string) []string { return ret } -func flagStringSliceField(f Flag, name string) []string { - fv := flagValue(f) - field := fv.FieldByName(name) - - if field.IsValid() { - return field.Interface().([]string) - } - - return []string{} -} - func withFileHint(filePath, str string) string { fileText := "" if filePath != "" { @@ -286,68 +288,34 @@ func withFileHint(filePath, str string) string { return str + fileText } -func flagValue(f Flag) reflect.Value { - fv := reflect.ValueOf(f) - for fv.Kind() == reflect.Ptr { - fv = reflect.Indirect(fv) - } - return fv -} - func formatDefault(format string) string { return " (default: " + format + ")" } func stringifyFlag(f Flag) string { - fv := flagValue(f) - - switch f := f.(type) { - case *IntSliceFlag: - return withEnvHint(flagStringSliceField(f, "EnvVars"), - stringifyIntSliceFlag(f)) - case *Int64SliceFlag: - return withEnvHint(flagStringSliceField(f, "EnvVars"), - stringifyInt64SliceFlag(f)) - case *Float64SliceFlag: - return withEnvHint(flagStringSliceField(f, "EnvVars"), - stringifyFloat64SliceFlag(f)) - case *StringSliceFlag: - return withEnvHint(flagStringSliceField(f, "EnvVars"), - stringifyStringSliceFlag(f)) + // enforce DocGeneration interface on flags to avoid reflection + df, ok := f.(DocGenerationFlag) + if !ok { + return "" } - placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String()) - - needsPlaceholder := false - defaultValueString := "" - val := fv.FieldByName("Value") - if val.IsValid() { - needsPlaceholder = val.Kind() != reflect.Bool - defaultValueString = fmt.Sprintf(formatDefault("%v"), val.Interface()) - - if val.Kind() == reflect.String && val.String() != "" { - defaultValueString = fmt.Sprintf(formatDefault("%q"), val.String()) - } - } - - helpText := fv.FieldByName("DefaultText") - if helpText.IsValid() && helpText.String() != "" { - needsPlaceholder = val.Kind() != reflect.Bool - defaultValueString = fmt.Sprintf(formatDefault("%s"), helpText.String()) - } - - if defaultValueString == formatDefault("") { - defaultValueString = "" - } + placeholder, usage := unquoteUsage(df.GetUsage()) + needsPlaceholder := df.TakesValue() if needsPlaceholder && placeholder == "" { placeholder = defaultPlaceholder } + defaultValueString := "" + + if s := df.GetDefaultText(); s != "" { + defaultValueString = fmt.Sprintf(formatDefault("%s"), s) + } + usageWithDefault := strings.TrimSpace(usage + defaultValueString) - return withEnvHint(flagStringSliceField(f, "EnvVars"), - fmt.Sprintf("%s\t%s", prefixedNames(f.Names(), placeholder), usageWithDefault)) + return withEnvHint(df.GetEnvVars(), + fmt.Sprintf("%s\t%s", prefixedNames(df.Names(), placeholder), usageWithDefault)) } func stringifyIntSliceFlag(f *IntSliceFlag) string { @@ -426,19 +394,26 @@ func hasFlag(flags []Flag, fl Flag) bool { return false } -func flagFromEnvOrFile(envVars []string, filePath string) (val string, ok bool) { +// Return the first value from a list of environment variables and files +// (which may or may not exist), a description of where the value was found, +// and a boolean which is true if a value was found. +func flagFromEnvOrFile(envVars []string, filePath string) (value string, fromWhere string, found bool) { for _, envVar := range envVars { envVar = strings.TrimSpace(envVar) - if val, ok := syscall.Getenv(envVar); ok { - return val, true + if value, found := syscall.Getenv(envVar); found { + return value, fmt.Sprintf("environment variable %q", envVar), true } } for _, fileVar := range strings.Split(filePath, ",") { if fileVar != "" { if data, err := ioutil.ReadFile(fileVar); err == nil { - return string(data), true + return string(data), fmt.Sprintf("file %q", filePath), true } } } - return "", false + return "", "", false +} + +func flagSplitMultiValues(val string) []string { + return strings.Split(val, ",") } diff --git a/vendor/github.com/urfave/cli/v2/flag_bool.go b/vendor/github.com/urfave/cli/v2/flag_bool.go index 8bd582094..b21d5163c 100644 --- a/vendor/github.com/urfave/cli/v2/flag_bool.go +++ b/vendor/github.com/urfave/cli/v2/flag_bool.go @@ -6,42 +6,6 @@ import ( "strconv" ) -// BoolFlag is a flag with type bool -type BoolFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value bool - DefaultText string - Destination *bool - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *BoolFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *BoolFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *BoolFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *BoolFlag) IsRequired() bool { - return f.Required -} - // TakesValue returns true of the flag takes a value, otherwise false func (f *BoolFlag) TakesValue() bool { return false @@ -52,25 +16,38 @@ func (f *BoolFlag) GetUsage() string { return f.Usage } +// GetCategory returns the category for the flag +func (f *BoolFlag) GetCategory() string { + return f.Category +} + // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *BoolFlag) GetValue() string { return "" } -// IsVisible returns true if the flag is not hidden, otherwise false -func (f *BoolFlag) IsVisible() bool { - return !f.Hidden +// GetDefaultText returns the default text for this flag +func (f *BoolFlag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + return fmt.Sprintf("%v", f.Value) +} + +// GetEnvVars returns the env vars for this flag +func (f *BoolFlag) GetEnvVars() []string { + return f.EnvVars } // Apply populates the flag given the flag set and environment func (f *BoolFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { valBool, err := strconv.ParseBool(val) if err != nil { - return fmt.Errorf("could not parse %q as bool value for flag %s: %s", val, f.Name, err) + return fmt.Errorf("could not parse %q as bool value from %s for flag %s: %s", val, source, f.Name, err) } f.Value = valBool @@ -89,10 +66,15 @@ func (f *BoolFlag) Apply(set *flag.FlagSet) error { return nil } +// Get returns the flag’s value in the given Context. +func (f *BoolFlag) Get(ctx *Context) bool { + return ctx.Bool(f.Name) +} + // Bool looks up the value of a local BoolFlag, returns // false if not found -func (c *Context) Bool(name string) bool { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) Bool(name string) bool { + if fs := cCtx.lookupFlagSet(name); fs != nil { return lookupBool(name, fs) } return false diff --git a/vendor/github.com/urfave/cli/v2/flag_duration.go b/vendor/github.com/urfave/cli/v2/flag_duration.go index 28f397805..5178c6ae1 100644 --- a/vendor/github.com/urfave/cli/v2/flag_duration.go +++ b/vendor/github.com/urfave/cli/v2/flag_duration.go @@ -6,42 +6,6 @@ import ( "time" ) -// DurationFlag is a flag with type time.Duration (see https://golang.org/pkg/time/#ParseDuration) -type DurationFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value time.Duration - DefaultText string - Destination *time.Duration - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *DurationFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *DurationFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *DurationFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *DurationFlag) IsRequired() bool { - return f.Required -} - // TakesValue returns true of the flag takes a value, otherwise false func (f *DurationFlag) TakesValue() bool { return true @@ -52,25 +16,38 @@ func (f *DurationFlag) GetUsage() string { return f.Usage } +// GetCategory returns the category for the flag +func (f *DurationFlag) GetCategory() string { + return f.Category +} + // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *DurationFlag) GetValue() string { return f.Value.String() } -// IsVisible returns true if the flag is not hidden, otherwise false -func (f *DurationFlag) IsVisible() bool { - return !f.Hidden +// GetDefaultText returns the default text for this flag +func (f *DurationFlag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + return f.GetValue() +} + +// GetEnvVars returns the env vars for this flag +func (f *DurationFlag) GetEnvVars() []string { + return f.EnvVars } // Apply populates the flag given the flag set and environment func (f *DurationFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { valDuration, err := time.ParseDuration(val) if err != nil { - return fmt.Errorf("could not parse %q as duration value for flag %s: %s", val, f.Name, err) + return fmt.Errorf("could not parse %q as duration value from %s for flag %s: %s", val, source, f.Name, err) } f.Value = valDuration @@ -88,10 +65,15 @@ func (f *DurationFlag) Apply(set *flag.FlagSet) error { return nil } +// Get returns the flag’s value in the given Context. +func (f *DurationFlag) Get(ctx *Context) time.Duration { + return ctx.Duration(f.Name) +} + // Duration looks up the value of a local DurationFlag, returns // 0 if not found -func (c *Context) Duration(name string) time.Duration { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) Duration(name string) time.Duration { + if fs := cCtx.lookupFlagSet(name); fs != nil { return lookupDuration(name, fs) } return 0 diff --git a/vendor/github.com/urfave/cli/v2/flag_float64.go b/vendor/github.com/urfave/cli/v2/flag_float64.go index 352f02bd3..2d31739bc 100644 --- a/vendor/github.com/urfave/cli/v2/flag_float64.go +++ b/vendor/github.com/urfave/cli/v2/flag_float64.go @@ -6,42 +6,6 @@ import ( "strconv" ) -// Float64Flag is a flag with type float64 -type Float64Flag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value float64 - DefaultText string - Destination *float64 - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *Float64Flag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *Float64Flag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *Float64Flag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *Float64Flag) IsRequired() bool { - return f.Required -} - // TakesValue returns true of the flag takes a value, otherwise false func (f *Float64Flag) TakesValue() bool { return true @@ -52,24 +16,37 @@ func (f *Float64Flag) GetUsage() string { return f.Usage } +// GetCategory returns the category for the flag +func (f *Float64Flag) GetCategory() string { + return f.Category +} + // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *Float64Flag) GetValue() string { - return fmt.Sprintf("%f", f.Value) + return fmt.Sprintf("%v", f.Value) } -// IsVisible returns true if the flag is not hidden, otherwise false -func (f *Float64Flag) IsVisible() bool { - return !f.Hidden +// GetDefaultText returns the default text for this flag +func (f *Float64Flag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + return f.GetValue() +} + +// GetEnvVars returns the env vars for this flag +func (f *Float64Flag) GetEnvVars() []string { + return f.EnvVars } // Apply populates the flag given the flag set and environment func (f *Float64Flag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { valFloat, err := strconv.ParseFloat(val, 64) if err != nil { - return fmt.Errorf("could not parse %q as float64 value for flag %s: %s", val, f.Name, err) + return fmt.Errorf("could not parse %q as float64 value from %s for flag %s: %s", val, source, f.Name, err) } f.Value = valFloat @@ -88,10 +65,15 @@ func (f *Float64Flag) Apply(set *flag.FlagSet) error { return nil } +// Get returns the flag’s value in the given Context. +func (f *Float64Flag) Get(ctx *Context) float64 { + return ctx.Float64(f.Name) +} + // Float64 looks up the value of a local Float64Flag, returns // 0 if not found -func (c *Context) Float64(name string) float64 { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) Float64(name string) float64 { + if fs := cCtx.lookupFlagSet(name); fs != nil { return lookupFloat64(name, fs) } return 0 diff --git a/vendor/github.com/urfave/cli/v2/flag_float64_slice.go b/vendor/github.com/urfave/cli/v2/flag_float64_slice.go index 385732e17..bc347ccdb 100644 --- a/vendor/github.com/urfave/cli/v2/flag_float64_slice.go +++ b/vendor/github.com/urfave/cli/v2/flag_float64_slice.go @@ -43,12 +43,14 @@ func (f *Float64Slice) Set(value string) error { return nil } - tmp, err := strconv.ParseFloat(value, 64) - if err != nil { - return err - } + for _, s := range flagSplitMultiValues(value) { + tmp, err := strconv.ParseFloat(strings.TrimSpace(s), 64) + if err != nil { + return err + } - f.slice = append(f.slice, tmp) + f.slice = append(f.slice, tmp) + } return nil } @@ -73,39 +75,10 @@ func (f *Float64Slice) Get() interface{} { return *f } -// Float64SliceFlag is a flag with type *Float64Slice -type Float64SliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value *Float64Slice - DefaultText string - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *Float64SliceFlag) IsSet() bool { - return f.HasBeenSet -} - // String returns a readable representation of this value // (for usage defaults) func (f *Float64SliceFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *Float64SliceFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *Float64SliceFlag) IsRequired() bool { - return f.Required + return withEnvHint(f.GetEnvVars(), stringifyFloat64SliceFlag(f)) } // TakesValue returns true if the flag takes a value, otherwise false @@ -118,6 +91,11 @@ func (f *Float64SliceFlag) GetUsage() string { return f.Usage } +// GetCategory returns the category for the flag +func (f *Float64SliceFlag) GetCategory() string { + return f.Category +} + // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *Float64SliceFlag) GetValue() string { @@ -127,20 +105,28 @@ func (f *Float64SliceFlag) GetValue() string { return "" } -// IsVisible returns true if the flag is not hidden, otherwise false -func (f *Float64SliceFlag) IsVisible() bool { - return !f.Hidden +// GetDefaultText returns the default text for this flag +func (f *Float64SliceFlag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + return f.GetValue() +} + +// GetEnvVars returns the env vars for this flag +func (f *Float64SliceFlag) GetEnvVars() []string { + return f.EnvVars } // Apply populates the flag given the flag set and environment func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { f.Value = &Float64Slice{} - for _, s := range strings.Split(val, ",") { + for _, s := range flagSplitMultiValues(val) { if err := f.Value.Set(strings.TrimSpace(s)); err != nil { - return fmt.Errorf("could not parse %q as float64 slice value for flag %s: %s", f.Value, f.Name, err) + return fmt.Errorf("could not parse %q as float64 slice value from %s for flag %s: %s", f.Value, source, f.Name, err) } } @@ -162,10 +148,15 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { return nil } +// Get returns the flag’s value in the given Context. +func (f *Float64SliceFlag) Get(ctx *Context) []float64 { + return ctx.Float64Slice(f.Name) +} + // Float64Slice looks up the value of a local Float64SliceFlag, returns // nil if not found -func (c *Context) Float64Slice(name string) []float64 { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) Float64Slice(name string) []float64 { + if fs := cCtx.lookupFlagSet(name); fs != nil { return lookupFloat64Slice(name, fs) } return nil diff --git a/vendor/github.com/urfave/cli/v2/flag_generic.go b/vendor/github.com/urfave/cli/v2/flag_generic.go index fdf586d12..680eeb9d7 100644 --- a/vendor/github.com/urfave/cli/v2/flag_generic.go +++ b/vendor/github.com/urfave/cli/v2/flag_generic.go @@ -11,42 +11,6 @@ type Generic interface { String() string } -// GenericFlag is a flag with type Generic -type GenericFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - TakesFile bool - Value Generic - DefaultText string - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *GenericFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *GenericFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *GenericFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *GenericFlag) IsRequired() bool { - return f.Required -} - // TakesValue returns true of the flag takes a value, otherwise false func (f *GenericFlag) TakesValue() bool { return true @@ -57,6 +21,11 @@ func (f *GenericFlag) GetUsage() string { return f.Usage } +// GetCategory returns the category for the flag +func (f *GenericFlag) GetCategory() string { + return f.Category +} + // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *GenericFlag) GetValue() string { @@ -66,18 +35,26 @@ func (f *GenericFlag) GetValue() string { return "" } -// IsVisible returns true if the flag is not hidden, otherwise false -func (f *GenericFlag) IsVisible() bool { - return !f.Hidden +// GetDefaultText returns the default text for this flag +func (f *GenericFlag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + return f.GetValue() +} + +// GetEnvVars returns the env vars for this flag +func (f *GenericFlag) GetEnvVars() []string { + return f.EnvVars } // Apply takes the flagset and calls Set on the generic flag with the value // provided by the user for parsing by the flag func (f GenericFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { if err := f.Value.Set(val); err != nil { - return fmt.Errorf("could not parse %q as value for flag %s: %s", val, f.Name, err) + return fmt.Errorf("could not parse %q from %s as value for flag %s: %s", val, source, f.Name, err) } f.HasBeenSet = true @@ -91,10 +68,15 @@ func (f GenericFlag) Apply(set *flag.FlagSet) error { return nil } +// Get returns the flag’s value in the given Context. +func (f *GenericFlag) Get(ctx *Context) interface{} { + return ctx.Generic(f.Name) +} + // Generic looks up the value of a local GenericFlag, returns // nil if not found -func (c *Context) Generic(name string) interface{} { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) Generic(name string) interface{} { + if fs := cCtx.lookupFlagSet(name); fs != nil { return lookupGeneric(name, fs) } return nil diff --git a/vendor/github.com/urfave/cli/v2/flag_int.go b/vendor/github.com/urfave/cli/v2/flag_int.go index 1ebe3569c..c70b88985 100644 --- a/vendor/github.com/urfave/cli/v2/flag_int.go +++ b/vendor/github.com/urfave/cli/v2/flag_int.go @@ -6,42 +6,6 @@ import ( "strconv" ) -// IntFlag is a flag with type int -type IntFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value int - DefaultText string - Destination *int - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *IntFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *IntFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *IntFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *IntFlag) IsRequired() bool { - return f.Required -} - // TakesValue returns true of the flag takes a value, otherwise false func (f *IntFlag) TakesValue() bool { return true @@ -52,25 +16,38 @@ func (f *IntFlag) GetUsage() string { return f.Usage } +// GetCategory returns the category for the flag +func (f *IntFlag) GetCategory() string { + return f.Category +} + // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *IntFlag) GetValue() string { return fmt.Sprintf("%d", f.Value) } -// IsVisible returns true if the flag is not hidden, otherwise false -func (f *IntFlag) IsVisible() bool { - return !f.Hidden +// GetDefaultText returns the default text for this flag +func (f *IntFlag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + return f.GetValue() +} + +// GetEnvVars returns the env vars for this flag +func (f *IntFlag) GetEnvVars() []string { + return f.EnvVars } // Apply populates the flag given the flag set and environment func (f *IntFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { valInt, err := strconv.ParseInt(val, 0, 64) if err != nil { - return fmt.Errorf("could not parse %q as int value for flag %s: %s", val, f.Name, err) + return fmt.Errorf("could not parse %q as int value from %s for flag %s: %s", val, source, f.Name, err) } f.Value = int(valInt) @@ -89,10 +66,15 @@ func (f *IntFlag) Apply(set *flag.FlagSet) error { return nil } +// Get returns the flag’s value in the given Context. +func (f *IntFlag) Get(ctx *Context) int { + return ctx.Int(f.Name) +} + // Int looks up the value of a local IntFlag, returns // 0 if not found -func (c *Context) Int(name string) int { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) Int(name string) int { + if fs := cCtx.lookupFlagSet(name); fs != nil { return lookupInt(name, fs) } return 0 diff --git a/vendor/github.com/urfave/cli/v2/flag_int64.go b/vendor/github.com/urfave/cli/v2/flag_int64.go index ecf0e9ef6..5e7038cfb 100644 --- a/vendor/github.com/urfave/cli/v2/flag_int64.go +++ b/vendor/github.com/urfave/cli/v2/flag_int64.go @@ -6,42 +6,6 @@ import ( "strconv" ) -// Int64Flag is a flag with type int64 -type Int64Flag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value int64 - DefaultText string - Destination *int64 - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *Int64Flag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *Int64Flag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *Int64Flag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *Int64Flag) IsRequired() bool { - return f.Required -} - // TakesValue returns true of the flag takes a value, otherwise false func (f *Int64Flag) TakesValue() bool { return true @@ -52,25 +16,38 @@ func (f *Int64Flag) GetUsage() string { return f.Usage } +// GetCategory returns the category for the flag +func (f *Int64Flag) GetCategory() string { + return f.Category +} + // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *Int64Flag) GetValue() string { return fmt.Sprintf("%d", f.Value) } -// IsVisible returns true if the flag is not hidden, otherwise false -func (f *Int64Flag) IsVisible() bool { - return !f.Hidden +// GetDefaultText returns the default text for this flag +func (f *Int64Flag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + return f.GetValue() +} + +// GetEnvVars returns the env vars for this flag +func (f *Int64Flag) GetEnvVars() []string { + return f.EnvVars } // Apply populates the flag given the flag set and environment func (f *Int64Flag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { valInt, err := strconv.ParseInt(val, 0, 64) if err != nil { - return fmt.Errorf("could not parse %q as int value for flag %s: %s", val, f.Name, err) + return fmt.Errorf("could not parse %q as int value from %s for flag %s: %s", val, source, f.Name, err) } f.Value = valInt @@ -88,10 +65,15 @@ func (f *Int64Flag) Apply(set *flag.FlagSet) error { return nil } +// Get returns the flag’s value in the given Context. +func (f *Int64Flag) Get(ctx *Context) int64 { + return ctx.Int64(f.Name) +} + // Int64 looks up the value of a local Int64Flag, returns // 0 if not found -func (c *Context) Int64(name string) int64 { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) Int64(name string) int64 { + if fs := cCtx.lookupFlagSet(name); fs != nil { return lookupInt64(name, fs) } return 0 diff --git a/vendor/github.com/urfave/cli/v2/flag_int64_slice.go b/vendor/github.com/urfave/cli/v2/flag_int64_slice.go index f5c693963..5f3d5cd4e 100644 --- a/vendor/github.com/urfave/cli/v2/flag_int64_slice.go +++ b/vendor/github.com/urfave/cli/v2/flag_int64_slice.go @@ -43,12 +43,14 @@ func (i *Int64Slice) Set(value string) error { return nil } - tmp, err := strconv.ParseInt(value, 0, 64) - if err != nil { - return err - } + for _, s := range flagSplitMultiValues(value) { + tmp, err := strconv.ParseInt(strings.TrimSpace(s), 0, 64) + if err != nil { + return err + } - i.slice = append(i.slice, tmp) + i.slice = append(i.slice, tmp) + } return nil } @@ -74,39 +76,10 @@ func (i *Int64Slice) Get() interface{} { return *i } -// Int64SliceFlag is a flag with type *Int64Slice -type Int64SliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value *Int64Slice - DefaultText string - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *Int64SliceFlag) IsSet() bool { - return f.HasBeenSet -} - // String returns a readable representation of this value // (for usage defaults) func (f *Int64SliceFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *Int64SliceFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *Int64SliceFlag) IsRequired() bool { - return f.Required + return withEnvHint(f.GetEnvVars(), stringifyInt64SliceFlag(f)) } // TakesValue returns true of the flag takes a value, otherwise false @@ -115,10 +88,15 @@ func (f *Int64SliceFlag) TakesValue() bool { } // GetUsage returns the usage string for the flag -func (f Int64SliceFlag) GetUsage() string { +func (f *Int64SliceFlag) GetUsage() string { return f.Usage } +// GetCategory returns the category for the flag +func (f *Int64SliceFlag) GetCategory() string { + return f.Category +} + // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *Int64SliceFlag) GetValue() string { @@ -128,19 +106,27 @@ func (f *Int64SliceFlag) GetValue() string { return "" } -// IsVisible returns true if the flag is not hidden, otherwise false -func (f *Int64SliceFlag) IsVisible() bool { - return !f.Hidden +// GetDefaultText returns the default text for this flag +func (f *Int64SliceFlag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + return f.GetValue() +} + +// GetEnvVars returns the env vars for this flag +func (f *Int64SliceFlag) GetEnvVars() []string { + return f.EnvVars } // Apply populates the flag given the flag set and environment func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { f.Value = &Int64Slice{} - for _, s := range strings.Split(val, ",") { + for _, s := range flagSplitMultiValues(val) { if err := f.Value.Set(strings.TrimSpace(s)); err != nil { - return fmt.Errorf("could not parse %q as int64 slice value for flag %s: %s", val, f.Name, err) + return fmt.Errorf("could not parse %q as int64 slice value from %s for flag %s: %s", val, source, f.Name, err) } } @@ -161,10 +147,15 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error { return nil } +// Get returns the flag’s value in the given Context. +func (f *Int64SliceFlag) Get(ctx *Context) []int64 { + return ctx.Int64Slice(f.Name) +} + // Int64Slice looks up the value of a local Int64SliceFlag, returns // nil if not found -func (c *Context) Int64Slice(name string) []int64 { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) Int64Slice(name string) []int64 { + if fs := cCtx.lookupFlagSet(name); fs != nil { return lookupInt64Slice(name, fs) } return nil diff --git a/vendor/github.com/urfave/cli/v2/flag_int_slice.go b/vendor/github.com/urfave/cli/v2/flag_int_slice.go index 94c668e9f..2ddf80596 100644 --- a/vendor/github.com/urfave/cli/v2/flag_int_slice.go +++ b/vendor/github.com/urfave/cli/v2/flag_int_slice.go @@ -54,12 +54,14 @@ func (i *IntSlice) Set(value string) error { return nil } - tmp, err := strconv.ParseInt(value, 0, 64) - if err != nil { - return err - } + for _, s := range flagSplitMultiValues(value) { + tmp, err := strconv.ParseInt(strings.TrimSpace(s), 0, 64) + if err != nil { + return err + } - i.slice = append(i.slice, int(tmp)) + i.slice = append(i.slice, int(tmp)) + } return nil } @@ -85,39 +87,10 @@ func (i *IntSlice) Get() interface{} { return *i } -// IntSliceFlag is a flag with type *IntSlice -type IntSliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value *IntSlice - DefaultText string - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *IntSliceFlag) IsSet() bool { - return f.HasBeenSet -} - // String returns a readable representation of this value // (for usage defaults) func (f *IntSliceFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *IntSliceFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *IntSliceFlag) IsRequired() bool { - return f.Required + return withEnvHint(f.GetEnvVars(), stringifyIntSliceFlag(f)) } // TakesValue returns true of the flag takes a value, otherwise false @@ -126,10 +99,15 @@ func (f *IntSliceFlag) TakesValue() bool { } // GetUsage returns the usage string for the flag -func (f IntSliceFlag) GetUsage() string { +func (f *IntSliceFlag) GetUsage() string { return f.Usage } +// GetCategory returns the category for the flag +func (f *IntSliceFlag) GetCategory() string { + return f.Category +} + // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *IntSliceFlag) GetValue() string { @@ -139,19 +117,27 @@ func (f *IntSliceFlag) GetValue() string { return "" } -// IsVisible returns true if the flag is not hidden, otherwise false -func (f *IntSliceFlag) IsVisible() bool { - return !f.Hidden +// GetDefaultText returns the default text for this flag +func (f *IntSliceFlag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + return f.GetValue() +} + +// GetEnvVars returns the env vars for this flag +func (f *IntSliceFlag) GetEnvVars() []string { + return f.EnvVars } // Apply populates the flag given the flag set and environment func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { f.Value = &IntSlice{} - for _, s := range strings.Split(val, ",") { + for _, s := range flagSplitMultiValues(val) { if err := f.Value.Set(strings.TrimSpace(s)); err != nil { - return fmt.Errorf("could not parse %q as int slice value for flag %s: %s", val, f.Name, err) + return fmt.Errorf("could not parse %q as int slice value from %s for flag %s: %s", val, source, f.Name, err) } } @@ -172,10 +158,15 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { return nil } +// Get returns the flag’s value in the given Context. +func (f *IntSliceFlag) Get(ctx *Context) []int { + return ctx.IntSlice(f.Name) +} + // IntSlice looks up the value of a local IntSliceFlag, returns // nil if not found -func (c *Context) IntSlice(name string) []int { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) IntSlice(name string) []int { + if fs := cCtx.lookupFlagSet(name); fs != nil { return lookupIntSlice(name, fs) } return nil diff --git a/vendor/github.com/urfave/cli/v2/flag_path.go b/vendor/github.com/urfave/cli/v2/flag_path.go index b3aa9191d..7c87a8900 100644 --- a/vendor/github.com/urfave/cli/v2/flag_path.go +++ b/vendor/github.com/urfave/cli/v2/flag_path.go @@ -1,42 +1,11 @@ package cli -import "flag" +import ( + "flag" + "fmt" +) -type PathFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - TakesFile bool - Value string - DefaultText string - Destination *string - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *PathFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *PathFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *PathFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *PathFlag) IsRequired() bool { - return f.Required -} +type Path = string // TakesValue returns true of the flag takes a value, otherwise false func (f *PathFlag) TakesValue() bool { @@ -48,20 +17,36 @@ func (f *PathFlag) GetUsage() string { return f.Usage } +// GetCategory returns the category for the flag +func (f *PathFlag) GetCategory() string { + return f.Category +} + // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *PathFlag) GetValue() string { return f.Value } -// IsVisible returns true if the flag is not hidden, otherwise false -func (f *PathFlag) IsVisible() bool { - return !f.Hidden +// GetDefaultText returns the default text for this flag +func (f *PathFlag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + if f.Value == "" { + return f.Value + } + return fmt.Sprintf("%q", f.Value) +} + +// GetEnvVars returns the env vars for this flag +func (f *PathFlag) GetEnvVars() []string { + return f.EnvVars } // Apply populates the flag given the flag set and environment func (f *PathFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val, _, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { f.Value = val f.HasBeenSet = true } @@ -77,10 +62,15 @@ func (f *PathFlag) Apply(set *flag.FlagSet) error { return nil } +// Get returns the flag’s value in the given Context. +func (f *PathFlag) Get(ctx *Context) string { + return ctx.Path(f.Name) +} + // Path looks up the value of a local PathFlag, returns // "" if not found -func (c *Context) Path(name string) string { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) Path(name string) string { + if fs := cCtx.lookupFlagSet(name); fs != nil { return lookupPath(name, fs) } diff --git a/vendor/github.com/urfave/cli/v2/flag_string.go b/vendor/github.com/urfave/cli/v2/flag_string.go index aad4c43a9..c8da38f92 100644 --- a/vendor/github.com/urfave/cli/v2/flag_string.go +++ b/vendor/github.com/urfave/cli/v2/flag_string.go @@ -1,43 +1,9 @@ package cli -import "flag" - -// StringFlag is a flag with type string -type StringFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - TakesFile bool - Value string - DefaultText string - Destination *string - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *StringFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *StringFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *StringFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *StringFlag) IsRequired() bool { - return f.Required -} +import ( + "flag" + "fmt" +) // TakesValue returns true of the flag takes a value, otherwise false func (f *StringFlag) TakesValue() bool { @@ -49,20 +15,36 @@ func (f *StringFlag) GetUsage() string { return f.Usage } +// GetCategory returns the category for the flag +func (f *StringFlag) GetCategory() string { + return f.Category +} + // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *StringFlag) GetValue() string { return f.Value } -// IsVisible returns true if the flag is not hidden, otherwise false -func (f *StringFlag) IsVisible() bool { - return !f.Hidden +// GetDefaultText returns the default text for this flag +func (f *StringFlag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + if f.Value == "" { + return f.Value + } + return fmt.Sprintf("%q", f.Value) +} + +// GetEnvVars returns the env vars for this flag +func (f *StringFlag) GetEnvVars() []string { + return f.EnvVars } // Apply populates the flag given the flag set and environment func (f *StringFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val, _, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { f.Value = val f.HasBeenSet = true } @@ -78,10 +60,15 @@ func (f *StringFlag) Apply(set *flag.FlagSet) error { return nil } +// Get returns the flag’s value in the given Context. +func (f *StringFlag) Get(ctx *Context) string { + return ctx.String(f.Name) +} + // String looks up the value of a local StringFlag, returns // "" if not found -func (c *Context) String(name string) string { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) String(name string) string { + if fs := cCtx.lookupFlagSet(name); fs != nil { return lookupString(name, fs) } return "" diff --git a/vendor/github.com/urfave/cli/v2/flag_string_slice.go b/vendor/github.com/urfave/cli/v2/flag_string_slice.go index 526964310..599f42c7f 100644 --- a/vendor/github.com/urfave/cli/v2/flag_string_slice.go +++ b/vendor/github.com/urfave/cli/v2/flag_string_slice.go @@ -42,7 +42,9 @@ func (s *StringSlice) Set(value string) error { return nil } - s.slice = append(s.slice, value) + for _, t := range flagSplitMultiValues(value) { + s.slice = append(s.slice, strings.TrimSpace(t)) + } return nil } @@ -68,41 +70,10 @@ func (s *StringSlice) Get() interface{} { return *s } -// StringSliceFlag is a flag with type *StringSlice -type StringSliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - TakesFile bool - Value *StringSlice - DefaultText string - HasBeenSet bool - Destination *StringSlice -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *StringSliceFlag) IsSet() bool { - return f.HasBeenSet -} - // String returns a readable representation of this value // (for usage defaults) func (f *StringSliceFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *StringSliceFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *StringSliceFlag) IsRequired() bool { - return f.Required + return withEnvHint(f.GetEnvVars(), stringifyStringSliceFlag(f)) } // TakesValue returns true of the flag takes a value, otherwise false @@ -115,6 +86,11 @@ func (f *StringSliceFlag) GetUsage() string { return f.Usage } +// GetCategory returns the category for the flag +func (f *StringSliceFlag) GetCategory() string { + return f.Category +} + // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *StringSliceFlag) GetValue() string { @@ -124,9 +100,17 @@ func (f *StringSliceFlag) GetValue() string { return "" } -// IsVisible returns true if the flag is not hidden, otherwise false -func (f *StringSliceFlag) IsVisible() bool { - return !f.Hidden +// GetDefaultText returns the default text for this flag +func (f *StringSliceFlag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + return f.GetValue() +} + +// GetEnvVars returns the env vars for this flag +func (f *StringSliceFlag) GetEnvVars() []string { + return f.EnvVars } // Apply populates the flag given the flag set and environment @@ -138,7 +122,7 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { } - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if f.Value == nil { f.Value = &StringSlice{} } @@ -147,9 +131,9 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { destination = f.Destination } - for _, s := range strings.Split(val, ",") { + for _, s := range flagSplitMultiValues(val) { if err := destination.Set(strings.TrimSpace(s)); err != nil { - return fmt.Errorf("could not parse %q as string value for flag %s: %s", val, f.Name, err) + return fmt.Errorf("could not parse %q as string value from %s for flag %s: %s", val, source, f.Name, err) } } @@ -173,10 +157,15 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { return nil } +// Get returns the flag’s value in the given Context. +func (f *StringSliceFlag) Get(ctx *Context) []string { + return ctx.StringSlice(f.Name) +} + // StringSlice looks up the value of a local StringSliceFlag, returns // nil if not found -func (c *Context) StringSlice(name string) []string { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) StringSlice(name string) []string { + if fs := cCtx.lookupFlagSet(name); fs != nil { return lookupStringSlice(name, fs) } return nil diff --git a/vendor/github.com/urfave/cli/v2/flag_timestamp.go b/vendor/github.com/urfave/cli/v2/flag_timestamp.go index 7458a79b6..052247795 100644 --- a/vendor/github.com/urfave/cli/v2/flag_timestamp.go +++ b/vendor/github.com/urfave/cli/v2/flag_timestamp.go @@ -58,43 +58,6 @@ func (t *Timestamp) Get() interface{} { return *t } -// TimestampFlag is a flag with type time -type TimestampFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Layout string - Value *Timestamp - DefaultText string - HasBeenSet bool - Destination *Timestamp -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *TimestampFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *TimestampFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *TimestampFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *TimestampFlag) IsRequired() bool { - return f.Required -} - // TakesValue returns true of the flag takes a value, otherwise false func (f *TimestampFlag) TakesValue() bool { return true @@ -105,6 +68,11 @@ func (f *TimestampFlag) GetUsage() string { return f.Usage } +// GetCategory returns the category for the flag +func (f *TimestampFlag) GetCategory() string { + return f.Category +} + // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *TimestampFlag) GetValue() string { @@ -114,9 +82,17 @@ func (f *TimestampFlag) GetValue() string { return "" } -// IsVisible returns true if the flag is not hidden, otherwise false -func (f *TimestampFlag) IsVisible() bool { - return !f.Hidden +// GetDefaultText returns the default text for this flag +func (f *TimestampFlag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + return f.GetValue() +} + +// GetEnvVars returns the env vars for this flag +func (f *TimestampFlag) GetEnvVars() []string { + return f.EnvVars } // Apply populates the flag given the flag set and environment @@ -133,9 +109,9 @@ func (f *TimestampFlag) Apply(set *flag.FlagSet) error { f.Destination.SetLayout(f.Layout) } - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if err := f.Value.Set(val); err != nil { - return fmt.Errorf("could not parse %q as timestamp value for flag %s: %s", val, f.Name, err) + return fmt.Errorf("could not parse %q as timestamp value from %s for flag %s: %s", val, source, f.Name, err) } f.HasBeenSet = true } @@ -151,9 +127,14 @@ func (f *TimestampFlag) Apply(set *flag.FlagSet) error { return nil } +// Get returns the flag’s value in the given Context. +func (f *TimestampFlag) Get(ctx *Context) *time.Time { + return ctx.Timestamp(f.Name) +} + // Timestamp gets the timestamp from a flag name -func (c *Context) Timestamp(name string) *time.Time { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) Timestamp(name string) *time.Time { + if fs := cCtx.lookupFlagSet(name); fs != nil { return lookupTimestamp(name, fs) } return nil diff --git a/vendor/github.com/urfave/cli/v2/flag_uint.go b/vendor/github.com/urfave/cli/v2/flag_uint.go index 23a70a7f9..6092b1ad6 100644 --- a/vendor/github.com/urfave/cli/v2/flag_uint.go +++ b/vendor/github.com/urfave/cli/v2/flag_uint.go @@ -6,42 +6,6 @@ import ( "strconv" ) -// UintFlag is a flag with type uint -type UintFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value uint - DefaultText string - Destination *uint - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *UintFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *UintFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *UintFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *UintFlag) IsRequired() bool { - return f.Required -} - // TakesValue returns true of the flag takes a value, otherwise false func (f *UintFlag) TakesValue() bool { return true @@ -52,18 +16,18 @@ func (f *UintFlag) GetUsage() string { return f.Usage } -// IsVisible returns true if the flag is not hidden, otherwise false -func (f *UintFlag) IsVisible() bool { - return !f.Hidden +// GetCategory returns the category for the flag +func (f *UintFlag) GetCategory() string { + return f.Category } // Apply populates the flag given the flag set and environment func (f *UintFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { valInt, err := strconv.ParseUint(val, 0, 64) if err != nil { - return fmt.Errorf("could not parse %q as uint value for flag %s: %s", val, f.Name, err) + return fmt.Errorf("could not parse %q as uint value from %s for flag %s: %s", val, source, f.Name, err) } f.Value = uint(valInt) @@ -88,10 +52,28 @@ func (f *UintFlag) GetValue() string { return fmt.Sprintf("%d", f.Value) } +// GetDefaultText returns the default text for this flag +func (f *UintFlag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + return f.GetValue() +} + +// GetEnvVars returns the env vars for this flag +func (f *UintFlag) GetEnvVars() []string { + return f.EnvVars +} + +// Get returns the flag’s value in the given Context. +func (f *UintFlag) Get(ctx *Context) uint { + return ctx.Uint(f.Name) +} + // Uint looks up the value of a local UintFlag, returns // 0 if not found -func (c *Context) Uint(name string) uint { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) Uint(name string) uint { + if fs := cCtx.lookupFlagSet(name); fs != nil { return lookupUint(name, fs) } return 0 diff --git a/vendor/github.com/urfave/cli/v2/flag_uint64.go b/vendor/github.com/urfave/cli/v2/flag_uint64.go index a2df024e1..a37f30d9f 100644 --- a/vendor/github.com/urfave/cli/v2/flag_uint64.go +++ b/vendor/github.com/urfave/cli/v2/flag_uint64.go @@ -6,42 +6,6 @@ import ( "strconv" ) -// Uint64Flag is a flag with type uint64 -type Uint64Flag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value uint64 - DefaultText string - Destination *uint64 - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *Uint64Flag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *Uint64Flag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *Uint64Flag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *Uint64Flag) IsRequired() bool { - return f.Required -} - // TakesValue returns true of the flag takes a value, otherwise false func (f *Uint64Flag) TakesValue() bool { return true @@ -52,18 +16,18 @@ func (f *Uint64Flag) GetUsage() string { return f.Usage } -// IsVisible returns true if the flag is not hidden, otherwise false -func (f *Uint64Flag) IsVisible() bool { - return !f.Hidden +// GetCategory returns the category for the flag +func (f *Uint64Flag) GetCategory() string { + return f.Category } // Apply populates the flag given the flag set and environment func (f *Uint64Flag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { valInt, err := strconv.ParseUint(val, 0, 64) if err != nil { - return fmt.Errorf("could not parse %q as uint64 value for flag %s: %s", val, f.Name, err) + return fmt.Errorf("could not parse %q as uint64 value from %s for flag %s: %s", val, source, f.Name, err) } f.Value = valInt @@ -88,10 +52,28 @@ func (f *Uint64Flag) GetValue() string { return fmt.Sprintf("%d", f.Value) } +// GetDefaultText returns the default text for this flag +func (f *Uint64Flag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + return f.GetValue() +} + +// GetEnvVars returns the env vars for this flag +func (f *Uint64Flag) GetEnvVars() []string { + return f.EnvVars +} + +// Get returns the flag’s value in the given Context. +func (f *Uint64Flag) Get(ctx *Context) uint64 { + return ctx.Uint64(f.Name) +} + // Uint64 looks up the value of a local Uint64Flag, returns // 0 if not found -func (c *Context) Uint64(name string) uint64 { - if fs := c.lookupFlagSet(name); fs != nil { +func (cCtx *Context) Uint64(name string) uint64 { + if fs := cCtx.lookupFlagSet(name); fs != nil { return lookupUint64(name, fs) } return 0 diff --git a/vendor/github.com/urfave/cli/v2/funcs.go b/vendor/github.com/urfave/cli/v2/funcs.go index 842b4aa99..0a9b22c94 100644 --- a/vendor/github.com/urfave/cli/v2/funcs.go +++ b/vendor/github.com/urfave/cli/v2/funcs.go @@ -21,11 +21,11 @@ type CommandNotFoundFunc func(*Context, string) // customized usage error messages. This function is able to replace the // original error messages. If this function is not set, the "Incorrect usage" // is displayed and the execution is interrupted. -type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error +type OnUsageErrorFunc func(cCtx *Context, err error, isSubcommand bool) error // ExitErrHandlerFunc is executed if provided in order to handle exitError values // returned by Actions and Before/After functions. -type ExitErrHandlerFunc func(context *Context, err error) +type ExitErrHandlerFunc func(cCtx *Context, err error) // FlagStringFunc is used by the help generation to display a flag, which is // expected to be a single line. diff --git a/vendor/github.com/urfave/cli/v2/godoc-current.txt b/vendor/github.com/urfave/cli/v2/godoc-current.txt new file mode 100644 index 000000000..d94e80dce --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/godoc-current.txt @@ -0,0 +1,2204 @@ +package cli // import "github.com/urfave/cli/v2" + +Package cli provides a minimal framework for creating and organizing command +line Go applications. cli is designed to be easy to understand and write, +the most simple cli application can be written as follows: + + func main() { + (&cli.App{}).Run(os.Args) + } + +Of course this application does not do much, so let's make this an actual +application: + + func main() { + app := &cli.App{ + Name: "greet", + Usage: "say a greeting", + Action: func(c *cli.Context) error { + fmt.Println("Greetings") + return nil + }, + } + + app.Run(os.Args) + } + +VARIABLES + +var ( + SuggestFlag SuggestFlagFunc = suggestFlag + SuggestCommand SuggestCommandFunc = suggestCommand + SuggestDidYouMeanTemplate string = suggestDidYouMeanTemplate +) +var AppHelpTemplate = `NAME: + {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} + +USAGE: + {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} + +VERSION: + {{.Version}}{{end}}{{end}}{{if .Description}} + +DESCRIPTION: + {{.Description | nindent 3 | trim}}{{end}}{{if len .Authors}} + +AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: + {{range $index, $author := .Authors}}{{if $index}} + {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} + +COMMANDS:{{range .VisibleCategories}}{{if .Name}} + {{.Name}}:{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlagCategories}} + +GLOBAL OPTIONS:{{range .VisibleFlagCategories}} + {{if .Name}}{{.Name}} + {{end}}{{range .Flags}}{{.}} + {{end}}{{end}}{{else}}{{if .VisibleFlags}} + +GLOBAL OPTIONS: + {{range $index, $option := .VisibleFlags}}{{if $index}} + {{end}}{{$option}}{{end}}{{end}}{{end}}{{if .Copyright}} + +COPYRIGHT: + {{.Copyright}}{{end}} +` + AppHelpTemplate is the text template for the Default help topic. cli.go uses + text/template to render templates. You can render custom help text by + setting this variable. + +var CommandHelpTemplate = `NAME: + {{.HelpName}} - {{.Usage}} + +USAGE: + {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} + +CATEGORY: + {{.Category}}{{end}}{{if .Description}} + +DESCRIPTION: + {{.Description | nindent 3 | trim}}{{end}}{{if .VisibleFlagCategories}} + +OPTIONS:{{range .VisibleFlagCategories}} + {{if .Name}}{{.Name}} + {{end}}{{range .Flags}}{{.}} + {{end}}{{end}}{{else}}{{if .VisibleFlags}} + +OPTIONS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}}{{end}} +` + CommandHelpTemplate is the text template for the command help topic. cli.go + uses text/template to render templates. You can render custom help text by + setting this variable. + +var ErrWriter io.Writer = os.Stderr + ErrWriter is used to write errors to the user. This can be anything + implementing the io.Writer interface and defaults to os.Stderr. + +var FishCompletionTemplate = `# {{ .App.Name }} fish shell completion + +function __fish_{{ .App.Name }}_no_subcommand --description 'Test if there has been any subcommand yet' + for i in (commandline -opc) + if contains -- $i{{ range $v := .AllCommands }} {{ $v }}{{ end }} + return 1 + end + end + return 0 +end + +{{ range $v := .Completions }}{{ $v }} +{{ end }}` +var MarkdownDocTemplate = `{{if gt .SectionNum 0}}% {{ .App.Name }} {{ .SectionNum }} + +{{end}}# NAME + +{{ .App.Name }}{{ if .App.Usage }} - {{ .App.Usage }}{{ end }} + +# SYNOPSIS + +{{ .App.Name }} +{{ if .SynopsisArgs }} +` + "```" + ` +{{ range $v := .SynopsisArgs }}{{ $v }}{{ end }}` + "```" + ` +{{ end }}{{ if .App.Description }} +# DESCRIPTION + +{{ .App.Description }} +{{ end }} +**Usage**: + +` + "```" + `{{ if .App.UsageText }} +{{ .App.UsageText }} +{{ else }} +{{ .App.Name }} [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] +{{ end }}` + "```" + ` +{{ if .GlobalArgs }} +# GLOBAL OPTIONS +{{ range $v := .GlobalArgs }} +{{ $v }}{{ end }} +{{ end }}{{ if .Commands }} +# COMMANDS +{{ range $v := .Commands }} +{{ $v }}{{ end }}{{ end }}` +var OsExiter = os.Exit + OsExiter is the function used when the app exits. If not set defaults to + os.Exit. + +var SubcommandHelpTemplate = `NAME: + {{.HelpName}} - {{.Usage}} + +USAGE: + {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} + +DESCRIPTION: + {{.Description | nindent 3 | trim}}{{end}} + +COMMANDS:{{range .VisibleCategories}}{{if .Name}} + {{.Name}}:{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} + +OPTIONS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}} +` + SubcommandHelpTemplate is the text template for the subcommand help topic. + cli.go uses text/template to render templates. You can render custom help + text by setting this variable. + +var VersionPrinter = printVersion + VersionPrinter prints the version for the App + +var HelpPrinter helpPrinter = printHelp + HelpPrinter is a function that writes the help output. If not set + explicitly, this calls HelpPrinterCustom using only the default template + functions. + + If custom logic for printing help is required, this function can be + overridden. If the ExtraInfo field is defined on an App, this function + should not be modified, as HelpPrinterCustom will be used directly in order + to capture the extra information. + +var HelpPrinterCustom helpPrinterCustom = printHelpCustom + HelpPrinterCustom is a function that writes the help output. It is used as + the default implementation of HelpPrinter, and may be called directly if the + ExtraInfo field is set on an App. + + +FUNCTIONS + +func DefaultAppComplete(cCtx *Context) + DefaultAppComplete prints the list of subcommands as the default app + completion method + +func DefaultCompleteWithFlags(cmd *Command) func(cCtx *Context) +func FlagNames(name string, aliases []string) []string +func HandleAction(action interface{}, cCtx *Context) (err error) + HandleAction attempts to figure out which Action signature was used. If it's + an ActionFunc or a func with the legacy signature for Action, the func is + run! + +func HandleExitCoder(err error) + HandleExitCoder handles errors implementing ExitCoder by printing their + message and calling OsExiter with the given exit code. + + If the given error instead implements MultiError, each error will be checked + for the ExitCoder interface, and OsExiter will be called with the last exit + code found, or exit code 1 if no ExitCoder is found. + + This function is the default error-handling behavior for an App. + +func ShowAppHelp(cCtx *Context) error + ShowAppHelp is an action that displays the help. + +func ShowAppHelpAndExit(c *Context, exitCode int) + ShowAppHelpAndExit - Prints the list of subcommands for the app and exits + with exit code. + +func ShowCommandCompletions(ctx *Context, command string) + ShowCommandCompletions prints the custom completions for a given command + +func ShowCommandHelp(ctx *Context, command string) error + ShowCommandHelp prints help for the given command + +func ShowCommandHelpAndExit(c *Context, command string, code int) + ShowCommandHelpAndExit - exits with code after showing help + +func ShowCompletions(cCtx *Context) + ShowCompletions prints the lists of commands within a given context + +func ShowSubcommandHelp(cCtx *Context) error + ShowSubcommandHelp prints help for the given subcommand + +func ShowSubcommandHelpAndExit(c *Context, exitCode int) + ShowSubcommandHelpAndExit - Prints help for the given subcommand and exits + with exit code. + +func ShowVersion(cCtx *Context) + ShowVersion prints the version number of the App + + +TYPES + +type ActionFunc func(*Context) error + ActionFunc is the action to execute when no subcommands are specified + +type AfterFunc func(*Context) error + AfterFunc is an action to execute after any subcommands are run, but after + the subcommand has finished it is run even if Action() panics + +type App struct { + // The name of the program. Defaults to path.Base(os.Args[0]) + Name string + // Full name of command for help, defaults to Name + HelpName string + // Description of the program. + Usage string + // Text to override the USAGE section of help + UsageText string + // Description of the program argument format. + ArgsUsage string + // Version of the program + Version string + // Description of the program + Description string + // List of commands to execute + Commands []*Command + // List of flags to parse + Flags []Flag + // Boolean to enable bash completion commands + EnableBashCompletion bool + // Boolean to hide built-in help command and help flag + HideHelp bool + // Boolean to hide built-in help command but keep help flag. + // Ignored if HideHelp is true. + HideHelpCommand bool + // Boolean to hide built-in version flag and the VERSION section of help + HideVersion bool + + // An action to execute when the shell completion flag is set + BashComplete BashCompleteFunc + // An action to execute before any subcommands are run, but after the context is ready + // If a non-nil error is returned, no subcommands are run + Before BeforeFunc + // An action to execute after any subcommands are run, but after the subcommand has finished + // It is run even if Action() panics + After AfterFunc + // The action to execute when no subcommands are specified + Action ActionFunc + // Execute this function if the proper command cannot be found + CommandNotFound CommandNotFoundFunc + // Execute this function if a usage error occurs + OnUsageError OnUsageErrorFunc + // Compilation date + Compiled time.Time + // List of all authors who contributed + Authors []*Author + // Copyright of the binary if any + Copyright string + // Reader reader to write input to (useful for tests) + Reader io.Reader + // Writer writer to write output to + Writer io.Writer + // ErrWriter writes error output + ErrWriter io.Writer + // ExitErrHandler processes any error encountered while running an App before + // it is returned to the caller. If no function is provided, HandleExitCoder + // is used as the default behavior. + ExitErrHandler ExitErrHandlerFunc + // Other custom info + Metadata map[string]interface{} + // Carries a function which returns app specific info. + ExtraInfo func() map[string]string + // CustomAppHelpTemplate the text template for app help topic. + // cli.go uses text/template to render templates. You can + // render custom help text by setting this variable. + CustomAppHelpTemplate string + // Boolean to enable short-option handling so user can combine several + // single-character bool arguments into one + // i.e. foobar -o -v -> foobar -ov + UseShortOptionHandling bool + // Enable suggestions for commands and flags + Suggest bool + + // Has unexported fields. +} + App is the main structure of a cli application. It is recommended that an + app be created with the cli.NewApp() function + +func NewApp() *App + NewApp creates a new cli Application with some reasonable defaults for Name, + Usage, Version and Action. + +func (a *App) Command(name string) *Command + Command returns the named command on App. Returns nil if the command does + not exist + +func (a *App) Run(arguments []string) (err error) + Run is the entry point to the cli app. Parses the arguments slice and routes + to the proper flag/args combination + +func (a *App) RunAndExitOnError() + RunAndExitOnError calls .Run() and exits non-zero if an error was returned + + Deprecated: instead you should return an error that fulfills cli.ExitCoder + to cli.App.Run. This will cause the application to exit with the given error + code in the cli.ExitCoder + +func (a *App) RunAsSubcommand(ctx *Context) (err error) + RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() + to generate command-specific flags + +func (a *App) RunContext(ctx context.Context, arguments []string) (err error) + RunContext is like Run except it takes a Context that will be passed to its + commands and sub-commands. Through this, you can propagate timeouts and + cancellation requests + +func (a *App) Setup() + Setup runs initialization code to ensure all data structures are ready for + `Run` or inspection prior to `Run`. It is internally called by `Run`, but + will return early if setup has already happened. + +func (a *App) ToFishCompletion() (string, error) + ToFishCompletion creates a fish completion string for the `*App` The + function errors if either parsing or writing of the string fails. + +func (a *App) ToMan() (string, error) + ToMan creates a man page string for the `*App` The function errors if either + parsing or writing of the string fails. + +func (a *App) ToManWithSection(sectionNumber int) (string, error) + ToMan creates a man page string with section number for the `*App` The + function errors if either parsing or writing of the string fails. + +func (a *App) ToMarkdown() (string, error) + ToMarkdown creates a markdown string for the `*App` The function errors if + either parsing or writing of the string fails. + +func (a *App) VisibleCategories() []CommandCategory + VisibleCategories returns a slice of categories and commands that are + Hidden=false + +func (a *App) VisibleCommands() []*Command + VisibleCommands returns a slice of the Commands with Hidden=false + +func (a *App) VisibleFlagCategories() []VisibleFlagCategory + VisibleFlagCategories returns a slice containing all the categories with the + flags they contain + +func (a *App) VisibleFlags() []Flag + VisibleFlags returns a slice of the Flags with Hidden=false + +type Args interface { + // Get returns the nth argument, or else a blank string + Get(n int) string + // First returns the first argument, or else a blank string + First() string + // Tail returns the rest of the arguments (not the first one) + // or else an empty string slice + Tail() []string + // Len returns the length of the wrapped slice + Len() int + // Present checks if there are any arguments present + Present() bool + // Slice returns a copy of the internal slice + Slice() []string +} + +type Author struct { + Name string // The Authors name + Email string // The Authors email +} + Author represents someone who has contributed to a cli project. + +func (a *Author) String() string + String makes Author comply to the Stringer interface, to allow an easy print + in the templating process + +type BashCompleteFunc func(*Context) + BashCompleteFunc is an action to execute when the shell completion flag is + set + +type BeforeFunc func(*Context) error + BeforeFunc is an action to execute before any subcommands are run, but after + the context is ready if a non-nil error is returned, no subcommands are run + +type BoolFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value bool + Destination *bool + + Aliases []string + EnvVars []string +} + BoolFlag is a flag with type bool + +func (f *BoolFlag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *BoolFlag) Get(ctx *Context) bool + Get returns the flag’s value in the given Context. + +func (f *BoolFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *BoolFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *BoolFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *BoolFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *BoolFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *BoolFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *BoolFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *BoolFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *BoolFlag) Names() []string + Names returns the names of the flag + +func (f *BoolFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *BoolFlag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + +type CategorizableFlag interface { + VisibleFlag + + GetCategory() string +} + CategorizableFlag is an interface that allows us to potentially use a flag + in a categorized representation. + +type Command struct { + // The name of the command + Name string + // A list of aliases for the command + Aliases []string + // A short description of the usage of this command + Usage string + // Custom text to show on USAGE section of help + UsageText string + // A longer explanation of how the command works + Description string + // A short description of the arguments of this command + ArgsUsage string + // The category the command is part of + Category string + // The function to call when checking for bash command completions + BashComplete BashCompleteFunc + // An action to execute before any sub-subcommands are run, but after the context is ready + // If a non-nil error is returned, no sub-subcommands are run + Before BeforeFunc + // An action to execute after any subcommands are run, but after the subcommand has finished + // It is run even if Action() panics + After AfterFunc + // The function to call when this command is invoked + Action ActionFunc + // Execute this function if a usage error occurs. + OnUsageError OnUsageErrorFunc + // List of child commands + Subcommands []*Command + // List of flags to parse + Flags []Flag + + // Treat all flags as normal arguments if true + SkipFlagParsing bool + // Boolean to hide built-in help command and help flag + HideHelp bool + // Boolean to hide built-in help command but keep help flag + // Ignored if HideHelp is true. + HideHelpCommand bool + // Boolean to hide this command from help or completion + Hidden bool + // Boolean to enable short-option handling so user can combine several + // single-character bool arguments into one + // i.e. foobar -o -v -> foobar -ov + UseShortOptionHandling bool + + // Full name of command for help, defaults to full command name, including parent commands. + HelpName string + + // CustomHelpTemplate the text template for the command help topic. + // cli.go uses text/template to render templates. You can + // render custom help text by setting this variable. + CustomHelpTemplate string + // Has unexported fields. +} + Command is a subcommand for a cli.App. + +func (c *Command) FullName() string + FullName returns the full name of the command. For subcommands this ensures + that parent commands are part of the command path + +func (c *Command) HasName(name string) bool + HasName returns true if Command.Name matches given name + +func (c *Command) Names() []string + Names returns the names including short names and aliases. + +func (c *Command) Run(ctx *Context) (err error) + Run invokes the command given the context, parses ctx.Args() to generate + command-specific flags + +func (c *Command) VisibleFlagCategories() []VisibleFlagCategory + VisibleFlagCategories returns a slice containing all the visible flag + categories with the flags they contain + +func (c *Command) VisibleFlags() []Flag + VisibleFlags returns a slice of the Flags with Hidden=false + +type CommandCategories interface { + // AddCommand adds a command to a category, creating a new category if necessary. + AddCommand(category string, command *Command) + // Categories returns a slice of categories sorted by name + Categories() []CommandCategory +} + CommandCategories interface allows for category manipulation + +type CommandCategory interface { + // Name returns the category name string + Name() string + // VisibleCommands returns a slice of the Commands with Hidden=false + VisibleCommands() []*Command +} + CommandCategory is a category containing commands. + +type CommandNotFoundFunc func(*Context, string) + CommandNotFoundFunc is executed if the proper command cannot be found + +type Commands []*Command + +type CommandsByName []*Command + +func (c CommandsByName) Len() int + +func (c CommandsByName) Less(i, j int) bool + +func (c CommandsByName) Swap(i, j int) + +type Context struct { + context.Context + App *App + Command *Command + + // Has unexported fields. +} + Context is a type that is passed through to each Handler action in a cli + application. Context can be used to retrieve context-specific args and + parsed command-line options. + +func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context + NewContext creates a new context. For use in when invoking an App or Command + action. + +func (cCtx *Context) Args() Args + Args returns the command line arguments associated with the context. + +func (cCtx *Context) Bool(name string) bool + Bool looks up the value of a local BoolFlag, returns false if not found + +func (cCtx *Context) Duration(name string) time.Duration + Duration looks up the value of a local DurationFlag, returns 0 if not found + +func (cCtx *Context) FlagNames() []string + FlagNames returns a slice of flag names used by the this context and all of + its parent contexts. + +func (cCtx *Context) Float64(name string) float64 + Float64 looks up the value of a local Float64Flag, returns 0 if not found + +func (cCtx *Context) Float64Slice(name string) []float64 + Float64Slice looks up the value of a local Float64SliceFlag, returns nil if + not found + +func (cCtx *Context) Generic(name string) interface{} + Generic looks up the value of a local GenericFlag, returns nil if not found + +func (cCtx *Context) Int(name string) int + Int looks up the value of a local IntFlag, returns 0 if not found + +func (cCtx *Context) Int64(name string) int64 + Int64 looks up the value of a local Int64Flag, returns 0 if not found + +func (cCtx *Context) Int64Slice(name string) []int64 + Int64Slice looks up the value of a local Int64SliceFlag, returns nil if not + found + +func (cCtx *Context) IntSlice(name string) []int + IntSlice looks up the value of a local IntSliceFlag, returns nil if not + found + +func (cCtx *Context) IsSet(name string) bool + IsSet determines if the flag was actually set + +func (cCtx *Context) Lineage() []*Context + Lineage returns *this* context and all of its ancestor contexts in order + from child to parent + +func (cCtx *Context) LocalFlagNames() []string + LocalFlagNames returns a slice of flag names used in this context. + +func (cCtx *Context) NArg() int + NArg returns the number of the command line arguments. + +func (cCtx *Context) NumFlags() int + NumFlags returns the number of flags set + +func (cCtx *Context) Path(name string) string + Path looks up the value of a local PathFlag, returns "" if not found + +func (cCtx *Context) Set(name, value string) error + Set sets a context flag to a value. + +func (cCtx *Context) String(name string) string + String looks up the value of a local StringFlag, returns "" if not found + +func (cCtx *Context) StringSlice(name string) []string + StringSlice looks up the value of a local StringSliceFlag, returns nil if + not found + +func (cCtx *Context) Timestamp(name string) *time.Time + Timestamp gets the timestamp from a flag name + +func (cCtx *Context) Uint(name string) uint + Uint looks up the value of a local UintFlag, returns 0 if not found + +func (cCtx *Context) Uint64(name string) uint64 + Uint64 looks up the value of a local Uint64Flag, returns 0 if not found + +func (cCtx *Context) Value(name string) interface{} + Value returns the value of the flag corresponding to `name` + +type DocGenerationFlag interface { + Flag + + // TakesValue returns true if the flag takes a value, otherwise false + TakesValue() bool + + // GetUsage returns the usage string for the flag + GetUsage() string + + // GetValue returns the flags value as string representation and an empty + // string if the flag takes no value at all. + GetValue() string + + // GetDefaultText returns the default text for this flag + GetDefaultText() string + + // GetEnvVars returns the env vars for this flag + GetEnvVars() []string +} + DocGenerationFlag is an interface that allows documentation generation for + the flag + +type DurationFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value time.Duration + Destination *time.Duration + + Aliases []string + EnvVars []string +} + DurationFlag is a flag with type time.Duration + +func (f *DurationFlag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *DurationFlag) Get(ctx *Context) time.Duration + Get returns the flag’s value in the given Context. + +func (f *DurationFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *DurationFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *DurationFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *DurationFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *DurationFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *DurationFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *DurationFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *DurationFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *DurationFlag) Names() []string + Names returns the names of the flag + +func (f *DurationFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *DurationFlag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + +type ErrorFormatter interface { + Format(s fmt.State, verb rune) +} + ErrorFormatter is the interface that will suitably format the error output + +type ExitCoder interface { + error + ExitCode() int +} + ExitCoder is the interface checked by `App` and `Command` for a custom exit + code + +func Exit(message interface{}, exitCode int) ExitCoder + Exit wraps a message and exit code into an error, which by default is + handled with a call to os.Exit during default error handling. + + This is the simplest way to trigger a non-zero exit code for an App without + having to call os.Exit manually. During testing, this behavior can be + avoided by overiding the ExitErrHandler function on an App or the + package-global OsExiter function. + +func NewExitError(message interface{}, exitCode int) ExitCoder + NewExitError calls Exit to create a new ExitCoder. + + Deprecated: This function is a duplicate of Exit and will eventually be + removed. + +type ExitErrHandlerFunc func(cCtx *Context, err error) + ExitErrHandlerFunc is executed if provided in order to handle exitError + values returned by Actions and Before/After functions. + +type Flag interface { + fmt.Stringer + // Apply Flag settings to the given flag set + Apply(*flag.FlagSet) error + Names() []string + IsSet() bool +} + Flag is a common interface related to parsing flags in cli. For more + advanced flag parsing techniques, it is recommended that this interface be + implemented. + +var BashCompletionFlag Flag = &BoolFlag{ + Name: "generate-bash-completion", + Hidden: true, +} + BashCompletionFlag enables bash-completion for all commands and subcommands + +var HelpFlag Flag = &BoolFlag{ + Name: "help", + Aliases: []string{"h"}, + Usage: "show help", +} + HelpFlag prints the help for all commands and subcommands. Set to nil to + disable the flag. The subcommand will still be added unless HideHelp or + HideHelpCommand is set to true. + +var VersionFlag Flag = &BoolFlag{ + Name: "version", + Aliases: []string{"v"}, + Usage: "print the version", +} + VersionFlag prints the version for the application + +type FlagCategories interface { + // AddFlags adds a flag to a category, creating a new category if necessary. + AddFlag(category string, fl Flag) + // VisibleCategories returns a slice of visible flag categories sorted by name + VisibleCategories() []VisibleFlagCategory +} + FlagCategories interface allows for category manipulation + +type FlagEnvHintFunc func(envVars []string, str string) string + FlagEnvHintFunc is used by the default FlagStringFunc to annotate flag help + with the environment variable details. + +var FlagEnvHinter FlagEnvHintFunc = withEnvHint + FlagEnvHinter annotates flag help message with the environment variable + details. This is used by the default FlagStringer. + +type FlagFileHintFunc func(filePath, str string) string + FlagFileHintFunc is used by the default FlagStringFunc to annotate flag help + with the file path details. + +var FlagFileHinter FlagFileHintFunc = withFileHint + FlagFileHinter annotates flag help message with the environment variable + details. This is used by the default FlagStringer. + +type FlagNamePrefixFunc func(fullName []string, placeholder string) string + FlagNamePrefixFunc is used by the default FlagStringFunc to create prefix + text for a flag's full name. + +var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames + FlagNamePrefixer converts a full flag name and its placeholder into the help + message flag prefix. This is used by the default FlagStringer. + +type FlagStringFunc func(Flag) string + FlagStringFunc is used by the help generation to display a flag, which is + expected to be a single line. + +var FlagStringer FlagStringFunc = stringifyFlag + FlagStringer converts a flag definition to a string. This is used by help to + display a flag. + +type FlagsByName []Flag + FlagsByName is a slice of Flag. + +func (f FlagsByName) Len() int + +func (f FlagsByName) Less(i, j int) bool + +func (f FlagsByName) Swap(i, j int) + +type Float64Flag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value float64 + Destination *float64 + + Aliases []string + EnvVars []string +} + Float64Flag is a flag with type float64 + +func (f *Float64Flag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *Float64Flag) Get(ctx *Context) float64 + Get returns the flag’s value in the given Context. + +func (f *Float64Flag) GetCategory() string + GetCategory returns the category for the flag + +func (f *Float64Flag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *Float64Flag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *Float64Flag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *Float64Flag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *Float64Flag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *Float64Flag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *Float64Flag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *Float64Flag) Names() []string + Names returns the names of the flag + +func (f *Float64Flag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *Float64Flag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + +type Float64Slice struct { + // Has unexported fields. +} + Float64Slice wraps []float64 to satisfy flag.Value + +func NewFloat64Slice(defaults ...float64) *Float64Slice + NewFloat64Slice makes a *Float64Slice with default values + +func (f *Float64Slice) Get() interface{} + Get returns the slice of float64s set by this flag + +func (f *Float64Slice) Serialize() string + Serialize allows Float64Slice to fulfill Serializer + +func (f *Float64Slice) Set(value string) error + Set parses the value into a float64 and appends it to the list of values + +func (f *Float64Slice) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *Float64Slice) Value() []float64 + Value returns the slice of float64s set by this flag + +type Float64SliceFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *Float64Slice + Destination *Float64Slice + + Aliases []string + EnvVars []string +} + Float64SliceFlag is a flag with type *Float64Slice + +func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *Float64SliceFlag) Get(ctx *Context) []float64 + Get returns the flag’s value in the given Context. + +func (f *Float64SliceFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *Float64SliceFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *Float64SliceFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *Float64SliceFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *Float64SliceFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *Float64SliceFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *Float64SliceFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *Float64SliceFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *Float64SliceFlag) Names() []string + Names returns the names of the flag + +func (f *Float64SliceFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *Float64SliceFlag) TakesValue() bool + TakesValue returns true if the flag takes a value, otherwise false + +type Generic interface { + Set(value string) error + String() string +} + Generic is a generic parseable type identified by a specific flag + +type GenericFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value Generic + Destination *Generic + + Aliases []string + EnvVars []string + + TakesFile bool +} + GenericFlag is a flag with type Generic + +func (f GenericFlag) Apply(set *flag.FlagSet) error + Apply takes the flagset and calls Set on the generic flag with the value + provided by the user for parsing by the flag + +func (f *GenericFlag) Get(ctx *Context) interface{} + Get returns the flag’s value in the given Context. + +func (f *GenericFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *GenericFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *GenericFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *GenericFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *GenericFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *GenericFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *GenericFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *GenericFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *GenericFlag) Names() []string + Names returns the names of the flag + +func (f *GenericFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *GenericFlag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + +type Int64Flag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value int64 + Destination *int64 + + Aliases []string + EnvVars []string +} + Int64Flag is a flag with type int64 + +func (f *Int64Flag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *Int64Flag) Get(ctx *Context) int64 + Get returns the flag’s value in the given Context. + +func (f *Int64Flag) GetCategory() string + GetCategory returns the category for the flag + +func (f *Int64Flag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *Int64Flag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *Int64Flag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *Int64Flag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *Int64Flag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *Int64Flag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *Int64Flag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *Int64Flag) Names() []string + Names returns the names of the flag + +func (f *Int64Flag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *Int64Flag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + +type Int64Slice struct { + // Has unexported fields. +} + Int64Slice wraps []int64 to satisfy flag.Value + +func NewInt64Slice(defaults ...int64) *Int64Slice + NewInt64Slice makes an *Int64Slice with default values + +func (i *Int64Slice) Get() interface{} + Get returns the slice of ints set by this flag + +func (i *Int64Slice) Serialize() string + Serialize allows Int64Slice to fulfill Serializer + +func (i *Int64Slice) Set(value string) error + Set parses the value into an integer and appends it to the list of values + +func (i *Int64Slice) String() string + String returns a readable representation of this value (for usage defaults) + +func (i *Int64Slice) Value() []int64 + Value returns the slice of ints set by this flag + +type Int64SliceFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *Int64Slice + Destination *Int64Slice + + Aliases []string + EnvVars []string +} + Int64SliceFlag is a flag with type *Int64Slice + +func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *Int64SliceFlag) Get(ctx *Context) []int64 + Get returns the flag’s value in the given Context. + +func (f *Int64SliceFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *Int64SliceFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *Int64SliceFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *Int64SliceFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *Int64SliceFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *Int64SliceFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *Int64SliceFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *Int64SliceFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *Int64SliceFlag) Names() []string + Names returns the names of the flag + +func (f *Int64SliceFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *Int64SliceFlag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + +type IntFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value int + Destination *int + + Aliases []string + EnvVars []string +} + IntFlag is a flag with type int + +func (f *IntFlag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *IntFlag) Get(ctx *Context) int + Get returns the flag’s value in the given Context. + +func (f *IntFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *IntFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *IntFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *IntFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *IntFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *IntFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *IntFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *IntFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *IntFlag) Names() []string + Names returns the names of the flag + +func (f *IntFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *IntFlag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + +type IntSlice struct { + // Has unexported fields. +} + IntSlice wraps []int to satisfy flag.Value + +func NewIntSlice(defaults ...int) *IntSlice + NewIntSlice makes an *IntSlice with default values + +func (i *IntSlice) Get() interface{} + Get returns the slice of ints set by this flag + +func (i *IntSlice) Serialize() string + Serialize allows IntSlice to fulfill Serializer + +func (i *IntSlice) Set(value string) error + Set parses the value into an integer and appends it to the list of values + +func (i *IntSlice) SetInt(value int) + TODO: Consistently have specific Set function for Int64 and Float64 ? SetInt + directly adds an integer to the list of values + +func (i *IntSlice) String() string + String returns a readable representation of this value (for usage defaults) + +func (i *IntSlice) Value() []int + Value returns the slice of ints set by this flag + +type IntSliceFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *IntSlice + Destination *IntSlice + + Aliases []string + EnvVars []string +} + IntSliceFlag is a flag with type *IntSlice + +func (f *IntSliceFlag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *IntSliceFlag) Get(ctx *Context) []int + Get returns the flag’s value in the given Context. + +func (f *IntSliceFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *IntSliceFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *IntSliceFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *IntSliceFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *IntSliceFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *IntSliceFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *IntSliceFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *IntSliceFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *IntSliceFlag) Names() []string + Names returns the names of the flag + +func (f *IntSliceFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *IntSliceFlag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + +type MultiError interface { + error + Errors() []error +} + MultiError is an error that wraps multiple errors. + +type OnUsageErrorFunc func(cCtx *Context, err error, isSubcommand bool) error + OnUsageErrorFunc is executed if a usage error occurs. This is useful for + displaying customized usage error messages. This function is able to replace + the original error messages. If this function is not set, the "Incorrect + usage" is displayed and the execution is interrupted. + +type Path = string + +type PathFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value Path + Destination *Path + + Aliases []string + EnvVars []string + + TakesFile bool +} + PathFlag is a flag with type Path + +func (f *PathFlag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *PathFlag) Get(ctx *Context) string + Get returns the flag’s value in the given Context. + +func (f *PathFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *PathFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *PathFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *PathFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *PathFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *PathFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *PathFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *PathFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *PathFlag) Names() []string + Names returns the names of the flag + +func (f *PathFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *PathFlag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + +type RequiredFlag interface { + Flag + + IsRequired() bool +} + RequiredFlag is an interface that allows us to mark flags as required it + allows flags required flags to be backwards compatible with the Flag + interface + +type Serializer interface { + Serialize() string +} + Serializer is used to circumvent the limitations of flag.FlagSet.Set + +type StringFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value string + Destination *string + + Aliases []string + EnvVars []string + + TakesFile bool +} + StringFlag is a flag with type string + +func (f *StringFlag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *StringFlag) Get(ctx *Context) string + Get returns the flag’s value in the given Context. + +func (f *StringFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *StringFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *StringFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *StringFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *StringFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *StringFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *StringFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *StringFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *StringFlag) Names() []string + Names returns the names of the flag + +func (f *StringFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *StringFlag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + +type StringSlice struct { + // Has unexported fields. +} + StringSlice wraps a []string to satisfy flag.Value + +func NewStringSlice(defaults ...string) *StringSlice + NewStringSlice creates a *StringSlice with default values + +func (s *StringSlice) Get() interface{} + Get returns the slice of strings set by this flag + +func (s *StringSlice) Serialize() string + Serialize allows StringSlice to fulfill Serializer + +func (s *StringSlice) Set(value string) error + Set appends the string value to the list of values + +func (s *StringSlice) String() string + String returns a readable representation of this value (for usage defaults) + +func (s *StringSlice) Value() []string + Value returns the slice of strings set by this flag + +type StringSliceFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *StringSlice + Destination *StringSlice + + Aliases []string + EnvVars []string + + TakesFile bool +} + StringSliceFlag is a flag with type *StringSlice + +func (f *StringSliceFlag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *StringSliceFlag) Get(ctx *Context) []string + Get returns the flag’s value in the given Context. + +func (f *StringSliceFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *StringSliceFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *StringSliceFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *StringSliceFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *StringSliceFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *StringSliceFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *StringSliceFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *StringSliceFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *StringSliceFlag) Names() []string + Names returns the names of the flag + +func (f *StringSliceFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *StringSliceFlag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + +type SuggestCommandFunc func(commands []*Command, provided string) string + +type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string + +type Timestamp struct { + // Has unexported fields. +} + Timestamp wrap to satisfy golang's flag interface. + +func NewTimestamp(timestamp time.Time) *Timestamp + Timestamp constructor + +func (t *Timestamp) Get() interface{} + Get returns the flag structure + +func (t *Timestamp) Set(value string) error + Parses the string value to timestamp + +func (t *Timestamp) SetLayout(layout string) + Set the timestamp string layout for future parsing + +func (t *Timestamp) SetTimestamp(value time.Time) + Set the timestamp value directly + +func (t *Timestamp) String() string + String returns a readable representation of this value (for usage defaults) + +func (t *Timestamp) Value() *time.Time + Value returns the timestamp value stored in the flag + +type TimestampFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *Timestamp + Destination *Timestamp + + Aliases []string + EnvVars []string + + Layout string +} + TimestampFlag is a flag with type *Timestamp + +func (f *TimestampFlag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *TimestampFlag) Get(ctx *Context) *time.Time + Get returns the flag’s value in the given Context. + +func (f *TimestampFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *TimestampFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *TimestampFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *TimestampFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *TimestampFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *TimestampFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *TimestampFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *TimestampFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *TimestampFlag) Names() []string + Names returns the names of the flag + +func (f *TimestampFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *TimestampFlag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + +type Uint64Flag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value uint64 + Destination *uint64 + + Aliases []string + EnvVars []string +} + Uint64Flag is a flag with type uint64 + +func (f *Uint64Flag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *Uint64Flag) Get(ctx *Context) uint64 + Get returns the flag’s value in the given Context. + +func (f *Uint64Flag) GetCategory() string + GetCategory returns the category for the flag + +func (f *Uint64Flag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *Uint64Flag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *Uint64Flag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *Uint64Flag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *Uint64Flag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *Uint64Flag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *Uint64Flag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *Uint64Flag) Names() []string + Names returns the names of the flag + +func (f *Uint64Flag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *Uint64Flag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + +type UintFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value uint + Destination *uint + + Aliases []string + EnvVars []string +} + UintFlag is a flag with type uint + +func (f *UintFlag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *UintFlag) Get(ctx *Context) uint + Get returns the flag’s value in the given Context. + +func (f *UintFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *UintFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *UintFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *UintFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *UintFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *UintFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *UintFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *UintFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *UintFlag) Names() []string + Names returns the names of the flag + +func (f *UintFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *UintFlag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + +type VisibleFlag interface { + Flag + + // IsVisible returns true if the flag is not hidden, otherwise false + IsVisible() bool +} + VisibleFlag is an interface that allows to check if a flag is visible + +type VisibleFlagCategory interface { + // Name returns the category name string + Name() string + // Flags returns a slice of VisibleFlag sorted by name + Flags() []VisibleFlag +} + VisibleFlagCategory is a category containing flags. + +package altsrc // import "github.com/urfave/cli/v2/altsrc" + + +FUNCTIONS + +func ApplyInputSourceValues(cCtx *cli.Context, inputSourceContext InputSourceContext, flags []cli.Flag) error + ApplyInputSourceValues iterates over all provided flags and executes + ApplyInputSourceValue on flags implementing the FlagInputSourceExtension + interface to initialize these flags to an alternate input source. + +func InitInputSource(flags []cli.Flag, createInputSource func() (InputSourceContext, error)) cli.BeforeFunc + InitInputSource is used to to setup an InputSourceContext on a cli.Command + Before method. It will create a new input source based on the func provided. + If there is no error it will then apply the new input source to any flags + that are supported by the input source + +func InitInputSourceWithContext(flags []cli.Flag, createInputSource func(cCtx *cli.Context) (InputSourceContext, error)) cli.BeforeFunc + InitInputSourceWithContext is used to to setup an InputSourceContext on a + cli.Command Before method. It will create a new input source based on the + func provided with potentially using existing cli.Context values to + initialize itself. If there is no error it will then apply the new input + source to any flags that are supported by the input source + +func NewJSONSourceFromFlagFunc(flag string) func(c *cli.Context) (InputSourceContext, error) + NewJSONSourceFromFlagFunc returns a func that takes a cli.Context and + returns an InputSourceContext suitable for retrieving config variables from + a file containing JSON data with the file name defined by the given flag. + +func NewTomlSourceFromFlagFunc(flagFileName string) func(cCtx *cli.Context) (InputSourceContext, error) + NewTomlSourceFromFlagFunc creates a new TOML InputSourceContext from a + provided flag name and source context. + +func NewYamlSourceFromFlagFunc(flagFileName string) func(cCtx *cli.Context) (InputSourceContext, error) + NewYamlSourceFromFlagFunc creates a new Yaml InputSourceContext from a + provided flag name and source context. + + +TYPES + +type BoolFlag struct { + *cli.BoolFlag + // Has unexported fields. +} + BoolFlag is the flag type that wraps cli.BoolFlag to allow for other values + to be specified + +func NewBoolFlag(fl *cli.BoolFlag) *BoolFlag + NewBoolFlag creates a new BoolFlag + +func (f *BoolFlag) Apply(set *flag.FlagSet) error + Apply saves the flagSet for later usage calls, then calls the wrapped + BoolFlag.Apply + +func (f *BoolFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error + ApplyInputSourceValue applies a Bool value to the flagSet if required + +type DurationFlag struct { + *cli.DurationFlag + // Has unexported fields. +} + DurationFlag is the flag type that wraps cli.DurationFlag to allow for other + values to be specified + +func NewDurationFlag(fl *cli.DurationFlag) *DurationFlag + NewDurationFlag creates a new DurationFlag + +func (f *DurationFlag) Apply(set *flag.FlagSet) error + Apply saves the flagSet for later usage calls, then calls the wrapped + DurationFlag.Apply + +func (f *DurationFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error + ApplyInputSourceValue applies a Duration value to the flagSet if required + +type FlagInputSourceExtension interface { + cli.Flag + ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error +} + FlagInputSourceExtension is an extension interface of cli.Flag that allows a + value to be set on the existing parsed flags. + +type Float64Flag struct { + *cli.Float64Flag + // Has unexported fields. +} + Float64Flag is the flag type that wraps cli.Float64Flag to allow for other + values to be specified + +func NewFloat64Flag(fl *cli.Float64Flag) *Float64Flag + NewFloat64Flag creates a new Float64Flag + +func (f *Float64Flag) Apply(set *flag.FlagSet) error + Apply saves the flagSet for later usage calls, then calls the wrapped + Float64Flag.Apply + +func (f *Float64Flag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error + ApplyInputSourceValue applies a Float64 value to the flagSet if required + +type Float64SliceFlag struct { + *cli.Float64SliceFlag + // Has unexported fields. +} + Float64SliceFlag is the flag type that wraps cli.Float64SliceFlag to allow + for other values to be specified + +func NewFloat64SliceFlag(fl *cli.Float64SliceFlag) *Float64SliceFlag + NewFloat64SliceFlag creates a new Float64SliceFlag + +func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error + Apply saves the flagSet for later usage calls, then calls the wrapped + Float64SliceFlag.Apply + +type GenericFlag struct { + *cli.GenericFlag + // Has unexported fields. +} + GenericFlag is the flag type that wraps cli.GenericFlag to allow for other + values to be specified + +func NewGenericFlag(fl *cli.GenericFlag) *GenericFlag + NewGenericFlag creates a new GenericFlag + +func (f *GenericFlag) Apply(set *flag.FlagSet) error + Apply saves the flagSet for later usage calls, then calls the wrapped + GenericFlag.Apply + +func (f *GenericFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error + ApplyInputSourceValue applies a generic value to the flagSet if required + +type InputSourceContext interface { + Source() string + + Int(name string) (int, error) + Duration(name string) (time.Duration, error) + Float64(name string) (float64, error) + String(name string) (string, error) + StringSlice(name string) ([]string, error) + IntSlice(name string) ([]int, error) + Generic(name string) (cli.Generic, error) + Bool(name string) (bool, error) + + // Has unexported methods. +} + InputSourceContext is an interface used to allow other input sources to be + implemented as needed. + + Source returns an identifier for the input source. In case of file source it + should return path to the file. + +func NewJSONSource(data []byte) (InputSourceContext, error) + NewJSONSource returns an InputSourceContext suitable for retrieving config + variables from raw JSON data. + +func NewJSONSourceFromFile(f string) (InputSourceContext, error) + NewJSONSourceFromFile returns an InputSourceContext suitable for retrieving + config variables from a file (or url) containing JSON data. + +func NewJSONSourceFromReader(r io.Reader) (InputSourceContext, error) + NewJSONSourceFromReader returns an InputSourceContext suitable for + retrieving config variables from an io.Reader that returns JSON data. + +func NewTomlSourceFromFile(file string) (InputSourceContext, error) + NewTomlSourceFromFile creates a new TOML InputSourceContext from a filepath. + +func NewYamlSourceFromFile(file string) (InputSourceContext, error) + NewYamlSourceFromFile creates a new Yaml InputSourceContext from a filepath. + +type Int64Flag struct { + *cli.Int64Flag + // Has unexported fields. +} + Int64Flag is the flag type that wraps cli.Int64Flag to allow for other + values to be specified + +func NewInt64Flag(fl *cli.Int64Flag) *Int64Flag + NewInt64Flag creates a new Int64Flag + +func (f *Int64Flag) Apply(set *flag.FlagSet) error + Apply saves the flagSet for later usage calls, then calls the wrapped + Int64Flag.Apply + +type Int64SliceFlag struct { + *cli.Int64SliceFlag + // Has unexported fields. +} + Int64SliceFlag is the flag type that wraps cli.Int64SliceFlag to allow for + other values to be specified + +func NewInt64SliceFlag(fl *cli.Int64SliceFlag) *Int64SliceFlag + NewInt64SliceFlag creates a new Int64SliceFlag + +func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error + Apply saves the flagSet for later usage calls, then calls the wrapped + Int64SliceFlag.Apply + +type IntFlag struct { + *cli.IntFlag + // Has unexported fields. +} + IntFlag is the flag type that wraps cli.IntFlag to allow for other values to + be specified + +func NewIntFlag(fl *cli.IntFlag) *IntFlag + NewIntFlag creates a new IntFlag + +func (f *IntFlag) Apply(set *flag.FlagSet) error + Apply saves the flagSet for later usage calls, then calls the wrapped + IntFlag.Apply + +func (f *IntFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error + ApplyInputSourceValue applies a int value to the flagSet if required + +type IntSliceFlag struct { + *cli.IntSliceFlag + // Has unexported fields. +} + IntSliceFlag is the flag type that wraps cli.IntSliceFlag to allow for other + values to be specified + +func NewIntSliceFlag(fl *cli.IntSliceFlag) *IntSliceFlag + NewIntSliceFlag creates a new IntSliceFlag + +func (f *IntSliceFlag) Apply(set *flag.FlagSet) error + Apply saves the flagSet for later usage calls, then calls the wrapped + IntSliceFlag.Apply + +func (f *IntSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error + ApplyInputSourceValue applies a IntSlice value if required + +type MapInputSource struct { + // Has unexported fields. +} + MapInputSource implements InputSourceContext to return data from the map + that is loaded. + +func NewMapInputSource(file string, valueMap map[interface{}]interface{}) *MapInputSource + NewMapInputSource creates a new MapInputSource for implementing custom input + sources. + +func (fsm *MapInputSource) Bool(name string) (bool, error) + Bool returns an bool from the map otherwise returns false + +func (fsm *MapInputSource) Duration(name string) (time.Duration, error) + Duration returns a duration from the map if it exists otherwise returns 0 + +func (fsm *MapInputSource) Float64(name string) (float64, error) + Float64 returns an float64 from the map if it exists otherwise returns 0 + +func (fsm *MapInputSource) Generic(name string) (cli.Generic, error) + Generic returns an cli.Generic from the map if it exists otherwise returns + nil + +func (fsm *MapInputSource) Int(name string) (int, error) + Int returns an int from the map if it exists otherwise returns 0 + +func (fsm *MapInputSource) IntSlice(name string) ([]int, error) + IntSlice returns an []int from the map if it exists otherwise returns nil + +func (fsm *MapInputSource) Source() string + Source returns the path of the source file + +func (fsm *MapInputSource) String(name string) (string, error) + String returns a string from the map if it exists otherwise returns an empty + string + +func (fsm *MapInputSource) StringSlice(name string) ([]string, error) + StringSlice returns an []string from the map if it exists otherwise returns + nil + +type PathFlag struct { + *cli.PathFlag + // Has unexported fields. +} + PathFlag is the flag type that wraps cli.PathFlag to allow for other values + to be specified + +func NewPathFlag(fl *cli.PathFlag) *PathFlag + NewPathFlag creates a new PathFlag + +func (f *PathFlag) Apply(set *flag.FlagSet) error + Apply saves the flagSet for later usage calls, then calls the wrapped + PathFlag.Apply + +func (f *PathFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error + ApplyInputSourceValue applies a Path value to the flagSet if required + +type StringFlag struct { + *cli.StringFlag + // Has unexported fields. +} + StringFlag is the flag type that wraps cli.StringFlag to allow for other + values to be specified + +func NewStringFlag(fl *cli.StringFlag) *StringFlag + NewStringFlag creates a new StringFlag + +func (f *StringFlag) Apply(set *flag.FlagSet) error + Apply saves the flagSet for later usage calls, then calls the wrapped + StringFlag.Apply + +func (f *StringFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error + ApplyInputSourceValue applies a String value to the flagSet if required + +type StringSliceFlag struct { + *cli.StringSliceFlag + // Has unexported fields. +} + StringSliceFlag is the flag type that wraps cli.StringSliceFlag to allow for + other values to be specified + +func NewStringSliceFlag(fl *cli.StringSliceFlag) *StringSliceFlag + NewStringSliceFlag creates a new StringSliceFlag + +func (f *StringSliceFlag) Apply(set *flag.FlagSet) error + Apply saves the flagSet for later usage calls, then calls the wrapped + StringSliceFlag.Apply + +func (f *StringSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error + ApplyInputSourceValue applies a StringSlice value to the flagSet if required + +type Uint64Flag struct { + *cli.Uint64Flag + // Has unexported fields. +} + Uint64Flag is the flag type that wraps cli.Uint64Flag to allow for other + values to be specified + +func NewUint64Flag(fl *cli.Uint64Flag) *Uint64Flag + NewUint64Flag creates a new Uint64Flag + +func (f *Uint64Flag) Apply(set *flag.FlagSet) error + Apply saves the flagSet for later usage calls, then calls the wrapped + Uint64Flag.Apply + +type UintFlag struct { + *cli.UintFlag + // Has unexported fields. +} + UintFlag is the flag type that wraps cli.UintFlag to allow for other values + to be specified + +func NewUintFlag(fl *cli.UintFlag) *UintFlag + NewUintFlag creates a new UintFlag + +func (f *UintFlag) Apply(set *flag.FlagSet) error + Apply saves the flagSet for later usage calls, then calls the wrapped + UintFlag.Apply + diff --git a/vendor/github.com/urfave/cli/v2/help.go b/vendor/github.com/urfave/cli/v2/help.go index 0a421ee99..ff59ddc8b 100644 --- a/vendor/github.com/urfave/cli/v2/help.go +++ b/vendor/github.com/urfave/cli/v2/help.go @@ -10,34 +10,39 @@ import ( "unicode/utf8" ) +const ( + helpName = "help" + helpAlias = "h" +) + var helpCommand = &Command{ - Name: "help", - Aliases: []string{"h"}, + Name: helpName, + Aliases: []string{helpAlias}, Usage: "Shows a list of commands or help for one command", ArgsUsage: "[command]", - Action: func(c *Context) error { - args := c.Args() + Action: func(cCtx *Context) error { + args := cCtx.Args() if args.Present() { - return ShowCommandHelp(c, args.First()) + return ShowCommandHelp(cCtx, args.First()) } - _ = ShowAppHelp(c) + _ = ShowAppHelp(cCtx) return nil }, } var helpSubcommand = &Command{ - Name: "help", - Aliases: []string{"h"}, + Name: helpName, + Aliases: []string{helpAlias}, Usage: "Shows a list of commands or help for one command", ArgsUsage: "[command]", - Action: func(c *Context) error { - args := c.Args() + Action: func(cCtx *Context) error { + args := cCtx.Args() if args.Present() { - return ShowCommandHelp(c, args.First()) + return ShowCommandHelp(cCtx, args.First()) } - return ShowSubcommandHelp(c) + return ShowSubcommandHelp(cCtx) }, } @@ -71,30 +76,30 @@ func ShowAppHelpAndExit(c *Context, exitCode int) { } // ShowAppHelp is an action that displays the help. -func ShowAppHelp(c *Context) error { - tpl := c.App.CustomAppHelpTemplate +func ShowAppHelp(cCtx *Context) error { + tpl := cCtx.App.CustomAppHelpTemplate if tpl == "" { tpl = AppHelpTemplate } - if c.App.ExtraInfo == nil { - HelpPrinter(c.App.Writer, tpl, c.App) + if cCtx.App.ExtraInfo == nil { + HelpPrinter(cCtx.App.Writer, tpl, cCtx.App) return nil } customAppData := func() map[string]interface{} { return map[string]interface{}{ - "ExtraInfo": c.App.ExtraInfo, + "ExtraInfo": cCtx.App.ExtraInfo, } } - HelpPrinterCustom(c.App.Writer, tpl, c.App, customAppData()) + HelpPrinterCustom(cCtx.App.Writer, tpl, cCtx.App, customAppData()) return nil } // DefaultAppComplete prints the list of subcommands as the default app completion method -func DefaultAppComplete(c *Context) { - DefaultCompleteWithFlags(nil)(c) +func DefaultAppComplete(cCtx *Context) { + DefaultCompleteWithFlags(nil)(cCtx) } func printCommandSuggestions(commands []*Command, writer io.Writer) { @@ -102,7 +107,7 @@ func printCommandSuggestions(commands []*Command, writer io.Writer) { if command.Hidden { continue } - if os.Getenv("_CLI_ZSH_AUTOCOMPLETE_HACK") == "1" { + if strings.HasSuffix(os.Getenv("SHELL"), "zsh") { for _, name := range command.Names() { _, _ = fmt.Fprintf(writer, "%s:%s\n", name, command.Usage) } @@ -159,23 +164,30 @@ func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) { } } -func DefaultCompleteWithFlags(cmd *Command) func(c *Context) { - return func(c *Context) { +func DefaultCompleteWithFlags(cmd *Command) func(cCtx *Context) { + return func(cCtx *Context) { if len(os.Args) > 2 { lastArg := os.Args[len(os.Args)-2] + if strings.HasPrefix(lastArg, "-") { - printFlagSuggestions(lastArg, c.App.Flags, c.App.Writer) if cmd != nil { - printFlagSuggestions(lastArg, cmd.Flags, c.App.Writer) + printFlagSuggestions(lastArg, cmd.Flags, cCtx.App.Writer) + + return } + + printFlagSuggestions(lastArg, cCtx.App.Flags, cCtx.App.Writer) + return } } + if cmd != nil { - printCommandSuggestions(cmd.Subcommands, c.App.Writer) - } else { - printCommandSuggestions(c.App.Commands, c.App.Writer) + printCommandSuggestions(cmd.Subcommands, cCtx.App.Writer) + return } + + printCommandSuggestions(cCtx.App.Commands, cCtx.App.Writer) } } @@ -207,7 +219,13 @@ func ShowCommandHelp(ctx *Context, command string) error { } if ctx.App.CommandNotFound == nil { - return Exit(fmt.Sprintf("No help topic for '%v'", command), 3) + errMsg := fmt.Sprintf("No help topic for '%v'", command) + if ctx.App.Suggest { + if suggestion := SuggestCommand(ctx.App.Commands, command); suggestion != "" { + errMsg += ". " + suggestion + } + } + return Exit(errMsg, 3) } ctx.App.CommandNotFound(ctx, command) @@ -221,32 +239,32 @@ func ShowSubcommandHelpAndExit(c *Context, exitCode int) { } // ShowSubcommandHelp prints help for the given subcommand -func ShowSubcommandHelp(c *Context) error { - if c == nil { +func ShowSubcommandHelp(cCtx *Context) error { + if cCtx == nil { return nil } - if c.Command != nil { - return ShowCommandHelp(c, c.Command.Name) + if cCtx.Command != nil { + return ShowCommandHelp(cCtx, cCtx.Command.Name) } - return ShowCommandHelp(c, "") + return ShowCommandHelp(cCtx, "") } // ShowVersion prints the version number of the App -func ShowVersion(c *Context) { - VersionPrinter(c) +func ShowVersion(cCtx *Context) { + VersionPrinter(cCtx) } -func printVersion(c *Context) { - _, _ = fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version) +func printVersion(cCtx *Context) { + _, _ = fmt.Fprintf(cCtx.App.Writer, "%v version %v\n", cCtx.App.Name, cCtx.App.Version) } // ShowCompletions prints the lists of commands within a given context -func ShowCompletions(c *Context) { - a := c.App +func ShowCompletions(cCtx *Context) { + a := cCtx.App if a != nil && a.BashComplete != nil { - a.BashComplete(c) + a.BashComplete(cCtx) } } @@ -297,20 +315,20 @@ func printHelp(out io.Writer, templ string, data interface{}) { HelpPrinterCustom(out, templ, data, nil) } -func checkVersion(c *Context) bool { +func checkVersion(cCtx *Context) bool { found := false for _, name := range VersionFlag.Names() { - if c.Bool(name) { + if cCtx.Bool(name) { found = true } } return found } -func checkHelp(c *Context) bool { +func checkHelp(cCtx *Context) bool { found := false for _, name := range HelpFlag.Names() { - if c.Bool(name) { + if cCtx.Bool(name) { found = true } } @@ -326,9 +344,9 @@ func checkCommandHelp(c *Context, name string) bool { return false } -func checkSubcommandHelp(c *Context) bool { - if c.Bool("h") || c.Bool("help") { - _ = ShowSubcommandHelp(c) +func checkSubcommandHelp(cCtx *Context) bool { + if cCtx.Bool("h") || cCtx.Bool("help") { + _ = ShowSubcommandHelp(cCtx) return true } @@ -350,20 +368,20 @@ func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) { return true, arguments[:pos] } -func checkCompletions(c *Context) bool { - if !c.shellComplete { +func checkCompletions(cCtx *Context) bool { + if !cCtx.shellComplete { return false } - if args := c.Args(); args.Present() { + if args := cCtx.Args(); args.Present() { name := args.First() - if cmd := c.App.Command(name); cmd != nil { + if cmd := cCtx.App.Command(name); cmd != nil { // let the command handle the completion return false } } - ShowCompletions(c) + ShowCompletions(cCtx) return true } diff --git a/vendor/github.com/urfave/cli/v2/mkdocs-requirements.txt b/vendor/github.com/urfave/cli/v2/mkdocs-requirements.txt new file mode 100644 index 000000000..482ad0622 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/mkdocs-requirements.txt @@ -0,0 +1,5 @@ +mkdocs-git-revision-date-localized-plugin~=1.0 +mkdocs-material-extensions~=1.0 +mkdocs-material~=8.2 +mkdocs~=1.3 +pygments~=2.12 diff --git a/vendor/github.com/urfave/cli/v2/mkdocs.yml b/vendor/github.com/urfave/cli/v2/mkdocs.yml new file mode 100644 index 000000000..73b88c509 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/mkdocs.yml @@ -0,0 +1,62 @@ +# NOTE: the mkdocs dependencies will need to be installed out of +# band until this whole thing gets more automated: +# +# pip install -r mkdocs-requirements.txt +# + +site_name: urfave/cli +site_url: https://cli.urfave.org/ +repo_url: https://github.com/urfave/cli +edit_uri: edit/main/docs/ +nav: + - Home: index.md + - v2 Manual: v2/index.md + - v1 Manual: v1/index.md +theme: + name: material + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-4 + name: dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-7 + name: light mode +plugins: + - git-revision-date-localized + - search +# NOTE: this is the recommended configuration from +# https://squidfunk.github.io/mkdocs-material/setup/extensions/#recommended-configuration +markdown_extensions: + - abbr + - admonition + - attr_list + - def_list + - footnotes + - meta + - md_in_html + - toc: + permalink: true + - pymdownx.arithmatex: + generic: true + - pymdownx.betterem: + smart_enable: all + - pymdownx.caret + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg + - pymdownx.highlight + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde diff --git a/vendor/github.com/urfave/cli/v2/parse.go b/vendor/github.com/urfave/cli/v2/parse.go index 7df17296a..a2db306e1 100644 --- a/vendor/github.com/urfave/cli/v2/parse.go +++ b/vendor/github.com/urfave/cli/v2/parse.go @@ -26,9 +26,8 @@ func parseIter(set *flag.FlagSet, ip iterativeParser, args []string, shellComple return err } - errStr := err.Error() - trimmed := strings.TrimPrefix(errStr, "flag provided but not defined: -") - if errStr == trimmed { + trimmed, trimErr := flagFromError(err) + if trimErr != nil { return err } @@ -67,6 +66,19 @@ func parseIter(set *flag.FlagSet, ip iterativeParser, args []string, shellComple } } +const providedButNotDefinedErrMsg = "flag provided but not defined: -" + +// flagFromError tries to parse a provided flag from an error message. If the +// parsing fials, it returns the input error and an empty string +func flagFromError(err error) (string, error) { + errStr := err.Error() + trimmed := strings.TrimPrefix(errStr, providedButNotDefinedErrMsg) + if errStr == trimmed { + return "", err + } + return trimmed, nil +} + func splitShortOptions(set *flag.FlagSet, arg string) []string { shortFlagsExist := func(s string) bool { for _, c := range s[1:] { diff --git a/vendor/github.com/urfave/cli/v2/suggestions.go b/vendor/github.com/urfave/cli/v2/suggestions.go new file mode 100644 index 000000000..87fa905dd --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/suggestions.go @@ -0,0 +1,60 @@ +package cli + +import ( + "fmt" + + "github.com/xrash/smetrics" +) + +func jaroWinkler(a, b string) float64 { + // magic values are from https://github.com/xrash/smetrics/blob/039620a656736e6ad994090895784a7af15e0b80/jaro-winkler.go#L8 + const ( + boostThreshold = 0.7 + prefixSize = 4 + ) + return smetrics.JaroWinkler(a, b, boostThreshold, prefixSize) +} + +func suggestFlag(flags []Flag, provided string, hideHelp bool) string { + distance := 0.0 + suggestion := "" + + for _, flag := range flags { + flagNames := flag.Names() + if !hideHelp { + flagNames = append(flagNames, HelpFlag.Names()...) + } + for _, name := range flagNames { + newDistance := jaroWinkler(name, provided) + if newDistance > distance { + distance = newDistance + suggestion = name + } + } + } + + if len(suggestion) == 1 { + suggestion = "-" + suggestion + } else if len(suggestion) > 1 { + suggestion = "--" + suggestion + } + + return suggestion +} + +// suggestCommand takes a list of commands and a provided string to suggest a +// command name +func suggestCommand(commands []*Command, provided string) (suggestion string) { + distance := 0.0 + for _, command := range commands { + for _, name := range append(command.Names(), helpName, helpAlias) { + newDistance := jaroWinkler(name, provided) + if newDistance > distance { + distance = newDistance + suggestion = name + } + } + } + + return fmt.Sprintf(SuggestDidYouMeanTemplate, suggestion) +} diff --git a/vendor/github.com/urfave/cli/v2/template.go b/vendor/github.com/urfave/cli/v2/template.go index 39fa4db08..264eb856b 100644 --- a/vendor/github.com/urfave/cli/v2/template.go +++ b/vendor/github.com/urfave/cli/v2/template.go @@ -22,11 +22,16 @@ AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{range .VisibleCommands}} {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlagCategories}} + +GLOBAL OPTIONS:{{range .VisibleFlagCategories}} + {{if .Name}}{{.Name}} + {{end}}{{range .Flags}}{{.}} + {{end}}{{end}}{{else}}{{if .VisibleFlags}} GLOBAL OPTIONS: {{range $index, $option := .VisibleFlags}}{{if $index}} - {{end}}{{$option}}{{end}}{{end}}{{if .Copyright}} + {{end}}{{$option}}{{end}}{{end}}{{end}}{{if .Copyright}} COPYRIGHT: {{.Copyright}}{{end}} @@ -45,11 +50,16 @@ CATEGORY: {{.Category}}{{end}}{{if .Description}} DESCRIPTION: - {{.Description | nindent 3 | trim}}{{end}}{{if .VisibleFlags}} + {{.Description | nindent 3 | trim}}{{end}}{{if .VisibleFlagCategories}} + +OPTIONS:{{range .VisibleFlagCategories}} + {{if .Name}}{{.Name}} + {{end}}{{range .Flags}}{{.}} + {{end}}{{end}}{{else}}{{if .VisibleFlags}} OPTIONS: {{range .VisibleFlags}}{{.}} - {{end}}{{end}} + {{end}}{{end}}{{end}} ` // SubcommandHelpTemplate is the text template for the subcommand help topic. diff --git a/vendor/github.com/urfave/cli/v2/zz_generated.flags.go b/vendor/github.com/urfave/cli/v2/zz_generated.flags.go new file mode 100644 index 000000000..3cae978c0 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/zz_generated.flags.go @@ -0,0 +1,672 @@ +// WARNING: this file is generated. DO NOT EDIT + +package cli + +import "time" + +// Float64SliceFlag is a flag with type *Float64Slice +type Float64SliceFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *Float64Slice + Destination *Float64Slice + + Aliases []string + EnvVars []string +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *Float64SliceFlag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *Float64SliceFlag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *Float64SliceFlag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *Float64SliceFlag) IsVisible() bool { + return !f.Hidden +} + +// GenericFlag is a flag with type Generic +type GenericFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value Generic + Destination *Generic + + Aliases []string + EnvVars []string + + TakesFile bool +} + +// String returns a readable representation of this value (for usage defaults) +func (f *GenericFlag) String() string { + return FlagStringer(f) +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *GenericFlag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *GenericFlag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *GenericFlag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *GenericFlag) IsVisible() bool { + return !f.Hidden +} + +// Int64SliceFlag is a flag with type *Int64Slice +type Int64SliceFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *Int64Slice + Destination *Int64Slice + + Aliases []string + EnvVars []string +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *Int64SliceFlag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *Int64SliceFlag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *Int64SliceFlag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *Int64SliceFlag) IsVisible() bool { + return !f.Hidden +} + +// IntSliceFlag is a flag with type *IntSlice +type IntSliceFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *IntSlice + Destination *IntSlice + + Aliases []string + EnvVars []string +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *IntSliceFlag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *IntSliceFlag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *IntSliceFlag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *IntSliceFlag) IsVisible() bool { + return !f.Hidden +} + +// PathFlag is a flag with type Path +type PathFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value Path + Destination *Path + + Aliases []string + EnvVars []string + + TakesFile bool +} + +// String returns a readable representation of this value (for usage defaults) +func (f *PathFlag) String() string { + return FlagStringer(f) +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *PathFlag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *PathFlag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *PathFlag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *PathFlag) IsVisible() bool { + return !f.Hidden +} + +// StringSliceFlag is a flag with type *StringSlice +type StringSliceFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *StringSlice + Destination *StringSlice + + Aliases []string + EnvVars []string + + TakesFile bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *StringSliceFlag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *StringSliceFlag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *StringSliceFlag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *StringSliceFlag) IsVisible() bool { + return !f.Hidden +} + +// TimestampFlag is a flag with type *Timestamp +type TimestampFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *Timestamp + Destination *Timestamp + + Aliases []string + EnvVars []string + + Layout string +} + +// String returns a readable representation of this value (for usage defaults) +func (f *TimestampFlag) String() string { + return FlagStringer(f) +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *TimestampFlag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *TimestampFlag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *TimestampFlag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *TimestampFlag) IsVisible() bool { + return !f.Hidden +} + +// BoolFlag is a flag with type bool +type BoolFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value bool + Destination *bool + + Aliases []string + EnvVars []string +} + +// String returns a readable representation of this value (for usage defaults) +func (f *BoolFlag) String() string { + return FlagStringer(f) +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *BoolFlag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *BoolFlag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *BoolFlag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *BoolFlag) IsVisible() bool { + return !f.Hidden +} + +// Float64Flag is a flag with type float64 +type Float64Flag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value float64 + Destination *float64 + + Aliases []string + EnvVars []string +} + +// String returns a readable representation of this value (for usage defaults) +func (f *Float64Flag) String() string { + return FlagStringer(f) +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *Float64Flag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *Float64Flag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *Float64Flag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *Float64Flag) IsVisible() bool { + return !f.Hidden +} + +// IntFlag is a flag with type int +type IntFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value int + Destination *int + + Aliases []string + EnvVars []string +} + +// String returns a readable representation of this value (for usage defaults) +func (f *IntFlag) String() string { + return FlagStringer(f) +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *IntFlag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *IntFlag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *IntFlag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *IntFlag) IsVisible() bool { + return !f.Hidden +} + +// Int64Flag is a flag with type int64 +type Int64Flag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value int64 + Destination *int64 + + Aliases []string + EnvVars []string +} + +// String returns a readable representation of this value (for usage defaults) +func (f *Int64Flag) String() string { + return FlagStringer(f) +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *Int64Flag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *Int64Flag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *Int64Flag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *Int64Flag) IsVisible() bool { + return !f.Hidden +} + +// StringFlag is a flag with type string +type StringFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value string + Destination *string + + Aliases []string + EnvVars []string + + TakesFile bool +} + +// String returns a readable representation of this value (for usage defaults) +func (f *StringFlag) String() string { + return FlagStringer(f) +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *StringFlag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *StringFlag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *StringFlag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *StringFlag) IsVisible() bool { + return !f.Hidden +} + +// DurationFlag is a flag with type time.Duration +type DurationFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value time.Duration + Destination *time.Duration + + Aliases []string + EnvVars []string +} + +// String returns a readable representation of this value (for usage defaults) +func (f *DurationFlag) String() string { + return FlagStringer(f) +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *DurationFlag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *DurationFlag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *DurationFlag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *DurationFlag) IsVisible() bool { + return !f.Hidden +} + +// UintFlag is a flag with type uint +type UintFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value uint + Destination *uint + + Aliases []string + EnvVars []string +} + +// String returns a readable representation of this value (for usage defaults) +func (f *UintFlag) String() string { + return FlagStringer(f) +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *UintFlag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *UintFlag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *UintFlag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *UintFlag) IsVisible() bool { + return !f.Hidden +} + +// Uint64Flag is a flag with type uint64 +type Uint64Flag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value uint64 + Destination *uint64 + + Aliases []string + EnvVars []string +} + +// String returns a readable representation of this value (for usage defaults) +func (f *Uint64Flag) String() string { + return FlagStringer(f) +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *Uint64Flag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *Uint64Flag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *Uint64Flag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *Uint64Flag) IsVisible() bool { + return !f.Hidden +} + +// vim:ro diff --git a/vendor/github.com/vektah/gqlparser/v2/ast/definition.go b/vendor/github.com/vektah/gqlparser/v2/ast/definition.go index 9087d402a..d20390816 100644 --- a/vendor/github.com/vektah/gqlparser/v2/ast/definition.go +++ b/vendor/github.com/vektah/gqlparser/v2/ast/definition.go @@ -11,7 +11,7 @@ const ( InputObject DefinitionKind = "INPUT_OBJECT" ) -// ObjectDefinition is the core type definition object, it includes all of the definable types +// Definition is the core type definition object, it includes all of the definable types // but does *not* cover schema or directives. // // @vektah: Javascript implementation has different types for all of these, but they are diff --git a/vendor/github.com/vektah/gqlparser/v2/ast/document.go b/vendor/github.com/vektah/gqlparser/v2/ast/document.go index e74622cb1..43bfb54ff 100644 --- a/vendor/github.com/vektah/gqlparser/v2/ast/document.go +++ b/vendor/github.com/vektah/gqlparser/v2/ast/document.go @@ -37,6 +37,16 @@ type Schema struct { Description string } +// AddTypes is the helper to add types definition to the schema +func (s *Schema) AddTypes(defs ...*Definition) { + if s.Types == nil { + s.Types = make(map[string]*Definition) + } + for _, def := range defs { + s.Types[def.Name] = def + } +} + func (s *Schema) AddPossibleType(name string, def *Definition) { s.PossibleTypes[name] = append(s.PossibleTypes[name], def) } diff --git a/vendor/github.com/vektah/gqlparser/v2/formatter/formatter.go b/vendor/github.com/vektah/gqlparser/v2/formatter/formatter.go index 3131b70f0..eb40ae369 100644 --- a/vendor/github.com/vektah/gqlparser/v2/formatter/formatter.go +++ b/vendor/github.com/vektah/gqlparser/v2/formatter/formatter.go @@ -15,14 +15,30 @@ type Formatter interface { FormatQueryDocument(doc *ast.QueryDocument) } -func NewFormatter(w io.Writer) Formatter { - return &formatter{writer: w} +type FormatterOption func(*formatter) + +func WithIndent(indent string) FormatterOption { + return func(f *formatter) { + f.indent = indent + } +} + +func NewFormatter(w io.Writer, options ...FormatterOption) Formatter { + f := &formatter{ + indent: "\t", + writer: w, + } + for _, opt := range options { + opt(f) + } + return f } type formatter struct { writer io.Writer - indent int + indent string + indentSize int emitBuiltin bool padNext bool @@ -35,7 +51,7 @@ func (f *formatter) writeString(s string) { func (f *formatter) writeIndent() *formatter { if f.lineHead { - f.writeString(strings.Repeat("\t", f.indent)) + f.writeString(strings.Repeat(f.indent, f.indentSize)) } f.lineHead = false f.padNext = false @@ -82,11 +98,14 @@ func (f *formatter) WriteDescription(s string) *formatter { return f } - f.WriteString(`"""`).WriteNewline() - - ss := strings.Split(s, "\n") - for _, s := range ss { - f.WriteString(s).WriteNewline() + f.WriteString(`"""`) + if ss := strings.Split(s, "\n"); len(ss) > 1 { + f.WriteNewline() + for _, s := range ss { + f.WriteString(s).WriteNewline() + } + } else { + f.WriteString(s) } f.WriteString(`"""`).WriteNewline() @@ -95,11 +114,11 @@ func (f *formatter) WriteDescription(s string) *formatter { } func (f *formatter) IncrementIndent() { - f.indent++ + f.indentSize++ } func (f *formatter) DecrementIndent() { - f.indent-- + f.indentSize-- } func (f *formatter) NoPadding() *formatter { @@ -301,6 +320,8 @@ func (f *formatter) FormatArgumentDefinition(def *ast.ArgumentDefinition) { f.FormatValue(def.DefaultValue) } + f.NeedPadding().FormatDirectiveList(def.Directives) + if def.Description != "" { f.DecrementIndent() f.WriteNewline() diff --git a/vendor/github.com/vektah/gqlparser/v2/validator/prelude.go b/vendor/github.com/vektah/gqlparser/v2/validator/prelude.go index 034624f1e..c354ec0df 100644 --- a/vendor/github.com/vektah/gqlparser/v2/validator/prelude.go +++ b/vendor/github.com/vektah/gqlparser/v2/validator/prelude.go @@ -1,9 +1,15 @@ package validator -import "github.com/vektah/gqlparser/v2/ast" +import ( + _ "embed" + "github.com/vektah/gqlparser/v2/ast" +) + +//go:embed prelude.graphql +var preludeGraphql string var Prelude = &ast.Source{ Name: "prelude.graphql", - Input: "# This file defines all the implicitly declared types that are required by the graphql spec. It is implicitly included by calls to LoadSchema\n\n\"The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.\"\nscalar Int\n\n\"The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).\"\nscalar Float\n\n\"The `String`scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.\"\nscalar String\n\n\"The `Boolean` scalar type represents `true` or `false`.\"\nscalar Boolean\n\n\"\"\"The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as \"4\") or integer (such as 4) input value will be accepted as an ID.\"\"\"\nscalar ID\n\n\"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.\"\ndirective @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n\n\"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.\"\ndirective @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n\n\"The @deprecated built-in directive is used within the type system definition language to indicate deprecated portions of a GraphQL service's schema, such as deprecated fields on a type, arguments on a field, input fields on an input type, or values of an enum type.\"\ndirective @deprecated(reason: String = \"No longer supported\") on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE\n\n\"The @specifiedBy built-in directive is used within the type system definition language to provide a scalar specification URL for specifying the behavior of custom scalar types.\"\ndirective @specifiedBy(url: String!) on SCALAR\n\ntype __Schema {\n description: String\n types: [__Type!]!\n queryType: __Type!\n mutationType: __Type\n subscriptionType: __Type\n directives: [__Directive!]!\n}\n\ntype __Type {\n kind: __TypeKind!\n name: String\n description: String\n # must be non-null for OBJECT and INTERFACE, otherwise null.\n fields(includeDeprecated: Boolean = false): [__Field!]\n # must be non-null for OBJECT and INTERFACE, otherwise null.\n interfaces: [__Type!]\n # must be non-null for INTERFACE and UNION, otherwise null.\n possibleTypes: [__Type!]\n # must be non-null for ENUM, otherwise null.\n enumValues(includeDeprecated: Boolean = false): [__EnumValue!]\n # must be non-null for INPUT_OBJECT, otherwise null.\n inputFields: [__InputValue!]\n # must be non-null for NON_NULL and LIST, otherwise null.\n ofType: __Type\n # may be non-null for custom SCALAR, otherwise null.\n specifiedByURL: String\n}\n\ntype __Field {\n name: String!\n description: String\n args: [__InputValue!]!\n type: __Type!\n isDeprecated: Boolean!\n deprecationReason: String\n}\n\ntype __InputValue {\n name: String!\n description: String\n type: __Type!\n defaultValue: String\n}\n\ntype __EnumValue {\n name: String!\n description: String\n isDeprecated: Boolean!\n deprecationReason: String\n}\n\nenum __TypeKind {\n SCALAR\n OBJECT\n INTERFACE\n UNION\n ENUM\n INPUT_OBJECT\n LIST\n NON_NULL\n}\n\ntype __Directive {\n name: String!\n description: String\n locations: [__DirectiveLocation!]!\n args: [__InputValue!]!\n isRepeatable: Boolean!\n}\n\nenum __DirectiveLocation {\n QUERY\n MUTATION\n SUBSCRIPTION\n FIELD\n FRAGMENT_DEFINITION\n FRAGMENT_SPREAD\n INLINE_FRAGMENT\n VARIABLE_DEFINITION\n SCHEMA\n SCALAR\n OBJECT\n FIELD_DEFINITION\n ARGUMENT_DEFINITION\n INTERFACE\n UNION\n ENUM\n ENUM_VALUE\n INPUT_OBJECT\n INPUT_FIELD_DEFINITION\n}\n", + Input: preludeGraphql, BuiltIn: true, } diff --git a/vendor/github.com/vektah/gqlparser/v2/validator/schema.go b/vendor/github.com/vektah/gqlparser/v2/validator/schema.go index 679219324..57dc549f9 100644 --- a/vendor/github.com/vektah/gqlparser/v2/validator/schema.go +++ b/vendor/github.com/vektah/gqlparser/v2/validator/schema.go @@ -1,5 +1,3 @@ -//go:generate go run ./inliner/inliner.go - package validator import ( @@ -251,27 +249,27 @@ func validateDefinition(schema *Schema, def *Definition) *gqlerror.Error { switch def.Kind { case Object, Interface: if len(def.Fields) == 0 { - return gqlerror.ErrorPosf(def.Position, "%s must define one or more fields.", def.Kind) + return gqlerror.ErrorPosf(def.Position, "%s %s: must define one or more fields.", def.Kind, def.Name) } for _, field := range def.Fields { if typ, ok := schema.Types[field.Type.Name()]; ok { if !isValidKind(typ.Kind, Scalar, Object, Interface, Union, Enum) { - return gqlerror.ErrorPosf(field.Position, "%s field must be one of %s.", def.Kind, kindList(Scalar, Object, Interface, Union, Enum)) + return gqlerror.ErrorPosf(field.Position, "%s %s: field must be one of %s.", def.Kind, def.Name, kindList(Scalar, Object, Interface, Union, Enum)) } } } case Enum: if len(def.EnumValues) == 0 { - return gqlerror.ErrorPosf(def.Position, "%s must define one or more unique enum values.", def.Kind) + return gqlerror.ErrorPosf(def.Position, "%s %s: must define one or more unique enum values.", def.Kind, def.Name) } case InputObject: if len(def.Fields) == 0 { - return gqlerror.ErrorPosf(def.Position, "%s must define one or more input fields.", def.Kind) + return gqlerror.ErrorPosf(def.Position, "%s %s: must define one or more input fields.", def.Kind, def.Name) } for _, field := range def.Fields { if typ, ok := schema.Types[field.Type.Name()]; ok { if !isValidKind(typ.Kind, Scalar, Enum, InputObject) { - return gqlerror.ErrorPosf(field.Position, "%s field must be one of %s.", def.Kind, kindList(Scalar, Enum, InputObject)) + return gqlerror.ErrorPosf(field.Position, "%s %s: field must be one of %s.", typ.Kind, field.Name, kindList(Scalar, Enum, InputObject)) } } } diff --git a/vendor/github.com/vektah/gqlparser/v2/validator/schema_test.yml b/vendor/github.com/vektah/gqlparser/v2/validator/schema_test.yml index 50642cf0c..a07707ba0 100644 --- a/vendor/github.com/vektah/gqlparser/v2/validator/schema_test.yml +++ b/vendor/github.com/vektah/gqlparser/v2/validator/schema_test.yml @@ -61,7 +61,7 @@ object types: b: Int } error: - message: 'OBJECT must define one or more fields.' + message: 'OBJECT InvalidObject2: must define one or more fields.' locations: [{line: 6, column: 6}] - name: check reserved names on type name input: | @@ -98,7 +98,7 @@ object types: input: Input! } error: - message: 'OBJECT field must be one of SCALAR, OBJECT, INTERFACE, UNION, ENUM.' + message: 'OBJECT Query: field must be one of SCALAR, OBJECT, INTERFACE, UNION, ENUM.' locations: [{line: 5, column: 3}] interfaces: @@ -149,7 +149,7 @@ interfaces: b: Int } error: - message: 'INTERFACE must define one or more fields.' + message: 'INTERFACE InvalidInterface2: must define one or more fields.' locations: [{line: 6, column: 11}] - name: check reserved names on type name @@ -173,7 +173,7 @@ interfaces: input: Input! } error: - message: 'INTERFACE field must be one of SCALAR, OBJECT, INTERFACE, UNION, ENUM.' + message: 'INTERFACE Foo: field must be one of SCALAR, OBJECT, INTERFACE, UNION, ENUM.' locations: [{line: 8, column: 3}] - name: must have all fields from interface @@ -341,7 +341,7 @@ inputs: b: Int } error: - message: 'INPUT_OBJECT must define one or more input fields.' + message: 'INPUT_OBJECT InvalidInput2: must define one or more input fields.' locations: [{line: 6, column: 7}] - name: check reserved names on type name input: | @@ -357,7 +357,7 @@ inputs: type Object { id: ID } input Foo { a: Object! } error: - message: INPUT_OBJECT field must be one of SCALAR, ENUM, INPUT_OBJECT. + message: 'OBJECT a: field must be one of SCALAR, ENUM, INPUT_OBJECT.' locations: [{line: 2, column: 13}] - name: fields cannot be Interfaces @@ -365,7 +365,7 @@ inputs: interface Interface { id: ID! } input Foo { a: Interface! } error: - message: INPUT_OBJECT field must be one of SCALAR, ENUM, INPUT_OBJECT. + message: 'INTERFACE a: field must be one of SCALAR, ENUM, INPUT_OBJECT.' locations: [{line: 2, column: 13}] - name: fields cannot be Unions @@ -374,7 +374,7 @@ inputs: union Union = Object input Foo { a: Union! } error: - message: INPUT_OBJECT field must be one of SCALAR, ENUM, INPUT_OBJECT. + message: 'UNION a: field must be one of SCALAR, ENUM, INPUT_OBJECT.' locations: [{line: 3, column: 13}] args: @@ -434,7 +434,7 @@ enums: BAR } error: - message: 'ENUM must define one or more unique enum values.' + message: 'ENUM InvalidEnum2: must define one or more unique enum values.' locations: [{line: 6, column: 6}] - name: check reserved names on type name input: | diff --git a/vendor/github.com/vektah/gqlparser/v2/validator/vars.go b/vendor/github.com/vektah/gqlparser/v2/validator/vars.go index af2d76c7e..c3fd559bb 100644 --- a/vendor/github.com/vektah/gqlparser/v2/validator/vars.go +++ b/vendor/github.com/vektah/gqlparser/v2/validator/vars.go @@ -1,8 +1,10 @@ package validator import ( + "encoding/json" "fmt" "reflect" + "strconv" "strings" "github.com/vektah/gqlparser/v2/ast" @@ -28,6 +30,7 @@ func VariableValues(schema *ast.Schema, op *ast.OperationDefinition, variables m } val, hasValue := variables[v.Variable] + if !hasValue { if v.DefaultValue != nil { var err error @@ -49,6 +52,24 @@ func VariableValues(schema *ast.Schema, op *ast.OperationDefinition, variables m coercedVars[v.Variable] = nil } else { rv := reflect.ValueOf(val) + + jsonNumber, isJsonNumber := val.(json.Number) + if isJsonNumber { + if v.Type.NamedType == "Int" { + n, err := jsonNumber.Int64() + if err != nil { + return nil, gqlerror.ErrorPathf(validator.path, "cannot use value %d as %s", n, v.Type.NamedType) + } + rv = reflect.ValueOf(n) + } else if v.Type.NamedType == "Float" { + f, err := jsonNumber.Float64() + if err != nil { + return nil, gqlerror.ErrorPathf(validator.path, "cannot use value %f as %s", f, v.Type.NamedType) + } + rv = reflect.ValueOf(f) + + } + } if rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface { rv = rv.Elem() } @@ -132,11 +153,11 @@ func (v *varValidator) validateVarType(typ *ast.Type, val reflect.Value) (reflec kind := val.Type().Kind() switch typ.NamedType { case "Int": - if kind == reflect.String || kind == reflect.Int || kind == reflect.Int32 || kind == reflect.Int64 { + if kind == reflect.Int || kind == reflect.Int32 || kind == reflect.Int64 || kind == reflect.Float32 || kind == reflect.Float64 || IsValidIntString(val, kind) { return val, nil } case "Float": - if kind == reflect.String || kind == reflect.Float32 || kind == reflect.Float64 || kind == reflect.Int || kind == reflect.Int32 || kind == reflect.Int64 { + if kind == reflect.Float32 || kind == reflect.Float64 || kind == reflect.Int || kind == reflect.Int32 || kind == reflect.Int64 || IsValidFloatString(val, kind) { return val, nil } case "String": @@ -218,3 +239,20 @@ func (v *varValidator) validateVarType(typ *ast.Type, val reflect.Value) (reflec } return val, nil } + +func IsValidIntString(val reflect.Value, kind reflect.Kind) bool { + if kind != reflect.String { + return false + } + _, e := strconv.ParseInt(fmt.Sprintf("%v", val.Interface()), 10, 64) + + return e == nil +} + +func IsValidFloatString(val reflect.Value, kind reflect.Kind) bool { + if kind != reflect.String { + return false + } + _, e := strconv.ParseFloat(fmt.Sprintf("%v", val.Interface()), 64) + return e == nil +} diff --git a/vendor/github.com/xWTF/chardet/2022.go b/vendor/github.com/xWTF/chardet/2022.go new file mode 100644 index 000000000..e1186f28e --- /dev/null +++ b/vendor/github.com/xWTF/chardet/2022.go @@ -0,0 +1,103 @@ +package chardet + +import ( + "bytes" +) + +type recognizer2022 struct { + charset string + escapes [][]byte +} + +func (r *recognizer2022) Match(input *recognizerInput, order int) (output recognizerOutput) { + return recognizerOutput{ + Charset: r.charset, + Confidence: r.matchConfidence(input.input), + order: order, + } +} + +func (r *recognizer2022) matchConfidence(input []byte) int { + var hits, misses, shifts int +input: + for i := 0; i < len(input); i++ { + c := input[i] + if c == 0x1B { + for _, esc := range r.escapes { + if bytes.HasPrefix(input[i+1:], esc) { + hits++ + i += len(esc) + continue input + } + } + misses++ + } else if c == 0x0E || c == 0x0F { + shifts++ + } + } + if hits == 0 { + return 0 + } + quality := (100*hits - 100*misses) / (hits + misses) + if hits+shifts < 5 { + quality -= (5 - (hits + shifts)) * 10 + } + if quality < 0 { + quality = 0 + } + return quality +} + +var escapeSequences_2022JP = [][]byte{ + {0x24, 0x28, 0x43}, // KS X 1001:1992 + {0x24, 0x28, 0x44}, // JIS X 212-1990 + {0x24, 0x40}, // JIS C 6226-1978 + {0x24, 0x41}, // GB 2312-80 + {0x24, 0x42}, // JIS X 208-1983 + {0x26, 0x40}, // JIS X 208 1990, 1997 + {0x28, 0x42}, // ASCII + {0x28, 0x48}, // JIS-Roman + {0x28, 0x49}, // Half-width katakana + {0x28, 0x4a}, // JIS-Roman + {0x2e, 0x41}, // ISO 8859-1 + {0x2e, 0x46}, // ISO 8859-7 +} + +var escapeSequences_2022KR = [][]byte{ + {0x24, 0x29, 0x43}, +} + +var escapeSequences_2022CN = [][]byte{ + {0x24, 0x29, 0x41}, // GB 2312-80 + {0x24, 0x29, 0x47}, // CNS 11643-1992 Plane 1 + {0x24, 0x2A, 0x48}, // CNS 11643-1992 Plane 2 + {0x24, 0x29, 0x45}, // ISO-IR-165 + {0x24, 0x2B, 0x49}, // CNS 11643-1992 Plane 3 + {0x24, 0x2B, 0x4A}, // CNS 11643-1992 Plane 4 + {0x24, 0x2B, 0x4B}, // CNS 11643-1992 Plane 5 + {0x24, 0x2B, 0x4C}, // CNS 11643-1992 Plane 6 + {0x24, 0x2B, 0x4D}, // CNS 11643-1992 Plane 7 + {0x4e}, // SS2 + {0x4f}, // SS3 +} + +func newRecognizer_2022JP() *recognizer2022 { + return &recognizer2022{ + "ISO-2022-JP", + escapeSequences_2022JP, + } +} + +func newRecognizer_2022KR() *recognizer2022 { + return &recognizer2022{ + "ISO-2022-KR", + escapeSequences_2022KR, + } +} + +func newRecognizer_2022CN() *recognizer2022 { + return &recognizer2022{ + "ISO-2022-CN", + escapeSequences_2022CN, + } +} diff --git a/vendor/github.com/xWTF/chardet/AUTHORS b/vendor/github.com/xWTF/chardet/AUTHORS new file mode 100644 index 000000000..842d0216d --- /dev/null +++ b/vendor/github.com/xWTF/chardet/AUTHORS @@ -0,0 +1 @@ +Sheng Yu (yusheng dot sjtu at gmail dot com) diff --git a/vendor/github.com/xWTF/chardet/LICENSE b/vendor/github.com/xWTF/chardet/LICENSE new file mode 100644 index 000000000..35ee796b9 --- /dev/null +++ b/vendor/github.com/xWTF/chardet/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2012 chardet Authors + +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. + +Partial of the Software is derived from ICU project. See icu-license.html for +license of the derivative portions. diff --git a/vendor/github.com/xWTF/chardet/README.md b/vendor/github.com/xWTF/chardet/README.md new file mode 100644 index 000000000..72a7c071e --- /dev/null +++ b/vendor/github.com/xWTF/chardet/README.md @@ -0,0 +1,12 @@ +# chardet + +chardet is library to automatically detect +[charset](http://en.wikipedia.org/wiki/Character_encoding) of texts for [Go +programming language](http://golang.org/). It's based on the algorithm and data +in [ICU](http://icu-project.org/)'s implementation. + +The project was created by [saintfish](http://github.com/saintfish/chardet). In January 2015 it was forked by the [gogits](http://github.com/gogs/chardet) project in order to incorporate bugfixes and new features. In January 2023 it was forked by xWTF in order to fix a bug. + +## Documentation and Usage + +See [pkgdoc](http://godoc.org/github.com/gogs/chardet) diff --git a/vendor/github.com/xWTF/chardet/detector.go b/vendor/github.com/xWTF/chardet/detector.go new file mode 100644 index 000000000..e0a9e191d --- /dev/null +++ b/vendor/github.com/xWTF/chardet/detector.go @@ -0,0 +1,152 @@ +// Package chardet ports character set detection from ICU. +package chardet + +import ( + "errors" + "sort" +) + +// Result contains all the information that charset detector gives. +type Result struct { + // IANA name of the detected charset. + Charset string + // IANA name of the detected language. It may be empty for some charsets. + Language string + // Confidence of the Result. Scale from 1 to 100. The bigger, the more confident. + Confidence int + + // used for sorting internally + order int +} + +// Detector implements charset detection. +type Detector struct { + recognizers []recognizer + stripTag bool +} + +// List of charset recognizers +var recognizers = []recognizer{ + newRecognizer_utf8(), + newRecognizer_utf16be(), + newRecognizer_utf16le(), + newRecognizer_utf32be(), + newRecognizer_utf32le(), + newRecognizer_8859_1_en(), + newRecognizer_8859_1_da(), + newRecognizer_8859_1_de(), + newRecognizer_8859_1_es(), + newRecognizer_8859_1_fr(), + newRecognizer_8859_1_it(), + newRecognizer_8859_1_nl(), + newRecognizer_8859_1_no(), + newRecognizer_8859_1_pt(), + newRecognizer_8859_1_sv(), + newRecognizer_8859_2_cs(), + newRecognizer_8859_2_hu(), + newRecognizer_8859_2_pl(), + newRecognizer_8859_2_ro(), + newRecognizer_8859_5_ru(), + newRecognizer_8859_6_ar(), + newRecognizer_8859_7_el(), + newRecognizer_8859_8_I_he(), + newRecognizer_8859_8_he(), + newRecognizer_windows_1251(), + newRecognizer_windows_1256(), + newRecognizer_KOI8_R(), + newRecognizer_8859_9_tr(), + + newRecognizer_sjis(), + newRecognizer_gb_18030(), + newRecognizer_euc_jp(), + newRecognizer_euc_kr(), + newRecognizer_big5(), + + newRecognizer_2022JP(), + newRecognizer_2022KR(), + newRecognizer_2022CN(), + + newRecognizer_IBM424_he_rtl(), + newRecognizer_IBM424_he_ltr(), + newRecognizer_IBM420_ar_rtl(), + newRecognizer_IBM420_ar_ltr(), +} + +// NewTextDetector creates a Detector for plain text. +func NewTextDetector() *Detector { + return &Detector{recognizers, false} +} + +// NewHtmlDetector creates a Detector for Html. +func NewHtmlDetector() *Detector { + return &Detector{recognizers, true} +} + +var ( + NotDetectedError = errors.New("Charset not detected.") +) + +// DetectBest returns the Result with highest Confidence. +func (d *Detector) DetectBest(b []byte) (r *Result, err error) { + input := newRecognizerInput(b, d.stripTag) + outputChan := make(chan recognizerOutput) + for i, r := range d.recognizers { + go matchHelper(r, input, outputChan, i) + } + var output Result + for i := 0; i < len(d.recognizers); i++ { + o := <-outputChan + if output.Confidence < o.Confidence || (output.Confidence == o.Confidence && o.order < output.order) { + output = Result(o) + } + } + if output.Confidence == 0 { + return nil, NotDetectedError + } + return &output, nil +} + +// DetectAll returns all Results which have non-zero Confidence. The Results are sorted by Confidence in descending order. +func (d *Detector) DetectAll(b []byte) ([]Result, error) { + input := newRecognizerInput(b, d.stripTag) + outputChan := make(chan recognizerOutput) + for i, r := range d.recognizers { + go matchHelper(r, input, outputChan, i) + } + outputs := make(recognizerOutputs, 0, len(d.recognizers)) + for i := 0; i < len(d.recognizers); i++ { + o := <-outputChan + if o.Confidence > 0 { + outputs = append(outputs, o) + } + } + if len(outputs) == 0 { + return nil, NotDetectedError + } + + sort.Sort(outputs) + dedupOutputs := make([]Result, 0, len(outputs)) + foundCharsets := make(map[string]struct{}, len(outputs)) + for _, o := range outputs { + if _, found := foundCharsets[o.Charset]; !found { + dedupOutputs = append(dedupOutputs, Result(o)) + foundCharsets[o.Charset] = struct{}{} + } + } + if len(dedupOutputs) == 0 { + return nil, NotDetectedError + } + return dedupOutputs, nil +} + +func matchHelper(r recognizer, input *recognizerInput, outputChan chan<- recognizerOutput, order int) { + outputChan <- r.Match(input, order) +} + +type recognizerOutputs []recognizerOutput + +func (r recognizerOutputs) Len() int { return len(r) } +func (r recognizerOutputs) Less(i, j int) bool { + return r[i].Confidence > r[j].Confidence || (r[i].Confidence == r[j].Confidence && r[i].order < r[j].order) +} +func (r recognizerOutputs) Swap(i, j int) { r[i], r[j] = r[j], r[i] } diff --git a/vendor/github.com/xWTF/chardet/icu-license.html b/vendor/github.com/xWTF/chardet/icu-license.html new file mode 100644 index 000000000..d078d0575 --- /dev/null +++ b/vendor/github.com/xWTF/chardet/icu-license.html @@ -0,0 +1,51 @@ + + + + +ICU License - ICU 1.8.1 and later + + + +

    ICU License - ICU 1.8.1 and later

    + +

    COPYRIGHT AND PERMISSION NOTICE

    + +

    +Copyright (c) 1995-2012 International Business Machines Corporation and others +

    +

    +All rights reserved. +

    +

    +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, and/or sell +copies of the Software, and to permit persons +to whom the Software is furnished to do so, provided that the above +copyright notice(s) and this permission notice appear in all copies +of the Software and that both the above copyright notice(s) and this +permission notice appear in supporting documentation. +

    +

    +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 OF THIRD PARTY RIGHTS. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, +OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE +USE OR PERFORMANCE OF THIS SOFTWARE. +

    +

    +Except as contained in this notice, the name of a copyright holder shall not be +used in advertising or otherwise to promote the sale, use or other dealings in +this Software without prior written authorization of the copyright holder. +

    + +
    +

    +All trademarks and registered trademarks mentioned herein are the property of their respective owners. +

    + + diff --git a/vendor/github.com/xWTF/chardet/multi_byte.go b/vendor/github.com/xWTF/chardet/multi_byte.go new file mode 100644 index 000000000..6b7e7e2cb --- /dev/null +++ b/vendor/github.com/xWTF/chardet/multi_byte.go @@ -0,0 +1,346 @@ +package chardet + +import ( + "errors" + "math" +) + +type recognizerMultiByte struct { + charset string + language string + decoder charDecoder + commonChars []uint16 +} + +type charDecoder interface { + DecodeOneChar([]byte) (c uint16, remain []byte, err error) +} + +func (r *recognizerMultiByte) Match(input *recognizerInput, order int) (output recognizerOutput) { + return recognizerOutput{ + Charset: r.charset, + Language: r.language, + Confidence: r.matchConfidence(input), + order: order, + } +} + +func (r *recognizerMultiByte) matchConfidence(input *recognizerInput) int { + raw := input.raw + var c uint16 + var err error + var totalCharCount, badCharCount, singleByteCharCount, doubleByteCharCount, commonCharCount int + for c, raw, err = r.decoder.DecodeOneChar(raw); len(raw) > 0; c, raw, err = r.decoder.DecodeOneChar(raw) { + totalCharCount++ + if err != nil { + badCharCount++ + } else if c <= 0xFF { + singleByteCharCount++ + } else { + doubleByteCharCount++ + if r.commonChars != nil && binarySearch(r.commonChars, c) { + commonCharCount++ + } + } + if badCharCount >= 2 && badCharCount*5 >= doubleByteCharCount { + return 0 + } + } + + if doubleByteCharCount <= 10 && badCharCount == 0 { + if doubleByteCharCount == 0 && totalCharCount < 10 { + return 0 + } else { + return 10 + } + } + + if doubleByteCharCount < 20*badCharCount { + return 0 + } + if r.commonChars == nil { + confidence := 30 + doubleByteCharCount - 20*badCharCount + if confidence > 100 { + confidence = 100 + } + return confidence + } + maxVal := math.Log(float64(doubleByteCharCount) / 4) + scaleFactor := 90 / maxVal + confidence := int(math.Log(float64(commonCharCount)+1)*scaleFactor + 10) + if confidence > 100 { + confidence = 100 + } + if confidence < 0 { + confidence = 0 + } + return confidence +} + +func binarySearch(l []uint16, c uint16) bool { + start := 0 + end := len(l) - 1 + for start <= end { + mid := (start + end) / 2 + if c == l[mid] { + return true + } else if c < l[mid] { + end = mid - 1 + } else { + start = mid + 1 + } + } + return false +} + +var eobError = errors.New("End of input buffer") +var badCharError = errors.New("Decode a bad char") + +type charDecoder_sjis struct { +} + +func (charDecoder_sjis) DecodeOneChar(input []byte) (c uint16, remain []byte, err error) { + if len(input) == 0 { + return 0, nil, eobError + } + first := input[0] + c = uint16(first) + remain = input[1:] + if first <= 0x7F || (first > 0xA0 && first <= 0xDF) { + return + } + if len(remain) == 0 { + return c, remain, badCharError + } + second := remain[0] + remain = remain[1:] + c = c<<8 | uint16(second) + if (second >= 0x40 && second <= 0x7F) || (second >= 0x80 && second <= 0xFE) { + } else { + err = badCharError + } + return +} + +var commonChars_sjis = []uint16{ + 0x8140, 0x8141, 0x8142, 0x8145, 0x815b, 0x8169, 0x816a, 0x8175, 0x8176, 0x82a0, + 0x82a2, 0x82a4, 0x82a9, 0x82aa, 0x82ab, 0x82ad, 0x82af, 0x82b1, 0x82b3, 0x82b5, + 0x82b7, 0x82bd, 0x82be, 0x82c1, 0x82c4, 0x82c5, 0x82c6, 0x82c8, 0x82c9, 0x82cc, + 0x82cd, 0x82dc, 0x82e0, 0x82e7, 0x82e8, 0x82e9, 0x82ea, 0x82f0, 0x82f1, 0x8341, + 0x8343, 0x834e, 0x834f, 0x8358, 0x835e, 0x8362, 0x8367, 0x8375, 0x8376, 0x8389, + 0x838a, 0x838b, 0x838d, 0x8393, 0x8e96, 0x93fa, 0x95aa, +} + +func newRecognizer_sjis() *recognizerMultiByte { + return &recognizerMultiByte{ + "Shift_JIS", + "ja", + charDecoder_sjis{}, + commonChars_sjis, + } +} + +type charDecoder_euc struct { +} + +func (charDecoder_euc) DecodeOneChar(input []byte) (c uint16, remain []byte, err error) { + if len(input) == 0 { + return 0, nil, eobError + } + first := input[0] + remain = input[1:] + c = uint16(first) + if first <= 0x8D { + return uint16(first), remain, nil + } + if len(remain) == 0 { + return 0, nil, eobError + } + second := remain[0] + remain = remain[1:] + c = c<<8 | uint16(second) + if first >= 0xA1 && first <= 0xFE { + if second < 0xA1 { + err = badCharError + } + return + } + if first == 0x8E { + if second < 0xA1 { + err = badCharError + } + return + } + if first == 0x8F { + if len(remain) == 0 { + return 0, nil, eobError + } + third := remain[0] + remain = remain[1:] + c = c<<0 | uint16(third) + if third < 0xa1 { + err = badCharError + } + } + return +} + +var commonChars_euc_jp = []uint16{ + 0xa1a1, 0xa1a2, 0xa1a3, 0xa1a6, 0xa1bc, 0xa1ca, 0xa1cb, 0xa1d6, 0xa1d7, 0xa4a2, + 0xa4a4, 0xa4a6, 0xa4a8, 0xa4aa, 0xa4ab, 0xa4ac, 0xa4ad, 0xa4af, 0xa4b1, 0xa4b3, + 0xa4b5, 0xa4b7, 0xa4b9, 0xa4bb, 0xa4bd, 0xa4bf, 0xa4c0, 0xa4c1, 0xa4c3, 0xa4c4, + 0xa4c6, 0xa4c7, 0xa4c8, 0xa4c9, 0xa4ca, 0xa4cb, 0xa4ce, 0xa4cf, 0xa4d0, 0xa4de, + 0xa4df, 0xa4e1, 0xa4e2, 0xa4e4, 0xa4e8, 0xa4e9, 0xa4ea, 0xa4eb, 0xa4ec, 0xa4ef, + 0xa4f2, 0xa4f3, 0xa5a2, 0xa5a3, 0xa5a4, 0xa5a6, 0xa5a7, 0xa5aa, 0xa5ad, 0xa5af, + 0xa5b0, 0xa5b3, 0xa5b5, 0xa5b7, 0xa5b8, 0xa5b9, 0xa5bf, 0xa5c3, 0xa5c6, 0xa5c7, + 0xa5c8, 0xa5c9, 0xa5cb, 0xa5d0, 0xa5d5, 0xa5d6, 0xa5d7, 0xa5de, 0xa5e0, 0xa5e1, + 0xa5e5, 0xa5e9, 0xa5ea, 0xa5eb, 0xa5ec, 0xa5ed, 0xa5f3, 0xb8a9, 0xb9d4, 0xbaee, + 0xbbc8, 0xbef0, 0xbfb7, 0xc4ea, 0xc6fc, 0xc7bd, 0xcab8, 0xcaf3, 0xcbdc, 0xcdd1, +} + +var commonChars_euc_kr = []uint16{ + 0xb0a1, 0xb0b3, 0xb0c5, 0xb0cd, 0xb0d4, 0xb0e6, 0xb0ed, 0xb0f8, 0xb0fa, 0xb0fc, + 0xb1b8, 0xb1b9, 0xb1c7, 0xb1d7, 0xb1e2, 0xb3aa, 0xb3bb, 0xb4c2, 0xb4cf, 0xb4d9, + 0xb4eb, 0xb5a5, 0xb5b5, 0xb5bf, 0xb5c7, 0xb5e9, 0xb6f3, 0xb7af, 0xb7c2, 0xb7ce, + 0xb8a6, 0xb8ae, 0xb8b6, 0xb8b8, 0xb8bb, 0xb8e9, 0xb9ab, 0xb9ae, 0xb9cc, 0xb9ce, + 0xb9fd, 0xbab8, 0xbace, 0xbad0, 0xbaf1, 0xbbe7, 0xbbf3, 0xbbfd, 0xbcad, 0xbcba, + 0xbcd2, 0xbcf6, 0xbdba, 0xbdc0, 0xbdc3, 0xbdc5, 0xbec6, 0xbec8, 0xbedf, 0xbeee, + 0xbef8, 0xbefa, 0xbfa1, 0xbfa9, 0xbfc0, 0xbfe4, 0xbfeb, 0xbfec, 0xbff8, 0xc0a7, + 0xc0af, 0xc0b8, 0xc0ba, 0xc0bb, 0xc0bd, 0xc0c7, 0xc0cc, 0xc0ce, 0xc0cf, 0xc0d6, + 0xc0da, 0xc0e5, 0xc0fb, 0xc0fc, 0xc1a4, 0xc1a6, 0xc1b6, 0xc1d6, 0xc1df, 0xc1f6, + 0xc1f8, 0xc4a1, 0xc5cd, 0xc6ae, 0xc7cf, 0xc7d1, 0xc7d2, 0xc7d8, 0xc7e5, 0xc8ad, +} + +func newRecognizer_euc_jp() *recognizerMultiByte { + return &recognizerMultiByte{ + "EUC-JP", + "ja", + charDecoder_euc{}, + commonChars_euc_jp, + } +} + +func newRecognizer_euc_kr() *recognizerMultiByte { + return &recognizerMultiByte{ + "EUC-KR", + "ko", + charDecoder_euc{}, + commonChars_euc_kr, + } +} + +type charDecoder_big5 struct { +} + +func (charDecoder_big5) DecodeOneChar(input []byte) (c uint16, remain []byte, err error) { + if len(input) == 0 { + return 0, nil, eobError + } + first := input[0] + remain = input[1:] + c = uint16(first) + if first <= 0x7F || first == 0xFF { + return + } + if len(remain) == 0 { + return c, nil, eobError + } + second := remain[0] + remain = remain[1:] + c = c<<8 | uint16(second) + if second < 0x40 || second == 0x7F || second == 0xFF { + err = badCharError + } + return +} + +var commonChars_big5 = []uint16{ + 0xa140, 0xa141, 0xa142, 0xa143, 0xa147, 0xa149, 0xa175, 0xa176, 0xa440, 0xa446, + 0xa447, 0xa448, 0xa451, 0xa454, 0xa457, 0xa464, 0xa46a, 0xa46c, 0xa477, 0xa4a3, + 0xa4a4, 0xa4a7, 0xa4c1, 0xa4ce, 0xa4d1, 0xa4df, 0xa4e8, 0xa4fd, 0xa540, 0xa548, + 0xa558, 0xa569, 0xa5cd, 0xa5e7, 0xa657, 0xa661, 0xa662, 0xa668, 0xa670, 0xa6a8, + 0xa6b3, 0xa6b9, 0xa6d3, 0xa6db, 0xa6e6, 0xa6f2, 0xa740, 0xa751, 0xa759, 0xa7da, + 0xa8a3, 0xa8a5, 0xa8ad, 0xa8d1, 0xa8d3, 0xa8e4, 0xa8fc, 0xa9c0, 0xa9d2, 0xa9f3, + 0xaa6b, 0xaaba, 0xaabe, 0xaacc, 0xaafc, 0xac47, 0xac4f, 0xacb0, 0xacd2, 0xad59, + 0xaec9, 0xafe0, 0xb0ea, 0xb16f, 0xb2b3, 0xb2c4, 0xb36f, 0xb44c, 0xb44e, 0xb54c, + 0xb5a5, 0xb5bd, 0xb5d0, 0xb5d8, 0xb671, 0xb7ed, 0xb867, 0xb944, 0xbad8, 0xbb44, + 0xbba1, 0xbdd1, 0xc2c4, 0xc3b9, 0xc440, 0xc45f, +} + +func newRecognizer_big5() *recognizerMultiByte { + return &recognizerMultiByte{ + "Big5", + "zh", + charDecoder_big5{}, + commonChars_big5, + } +} + +type charDecoder_gb_18030 struct { +} + +func (charDecoder_gb_18030) DecodeOneChar(input []byte) (c uint16, remain []byte, err error) { + if len(input) == 0 { + return 0, nil, eobError + } + first := input[0] + remain = input[1:] + c = uint16(first) + if first <= 0x80 { + return + } + if len(remain) == 0 { + return 0, nil, eobError + } + second := remain[0] + remain = remain[1:] + c = c<<8 | uint16(second) + if first >= 0x81 && first <= 0xFE { + if (second >= 0x40 && second <= 0x7E) || (second >= 0x80 && second <= 0xFE) { + return + } + + if second >= 0x30 && second <= 0x39 { + if len(remain) == 0 { + return 0, nil, eobError + } + third := remain[0] + remain = remain[1:] + if third >= 0x81 && third <= 0xFE { + if len(remain) == 0 { + return 0, nil, eobError + } + fourth := remain[0] + remain = remain[1:] + if fourth >= 0x30 && fourth <= 0x39 { + c = c<<16 | uint16(third)<<8 | uint16(fourth) + return + } + } + } + err = badCharError + } + return +} + +var commonChars_gb_18030 = []uint16{ + 0xa1a1, 0xa1a2, 0xa1a3, 0xa1a4, 0xa1b0, 0xa1b1, 0xa1f1, 0xa1f3, 0xa3a1, 0xa3ac, + 0xa3ba, 0xb1a8, 0xb1b8, 0xb1be, 0xb2bb, 0xb3c9, 0xb3f6, 0xb4f3, 0xb5bd, 0xb5c4, + 0xb5e3, 0xb6af, 0xb6d4, 0xb6e0, 0xb7a2, 0xb7a8, 0xb7bd, 0xb7d6, 0xb7dd, 0xb8b4, + 0xb8df, 0xb8f6, 0xb9ab, 0xb9c9, 0xb9d8, 0xb9fa, 0xb9fd, 0xbacd, 0xbba7, 0xbbd6, + 0xbbe1, 0xbbfa, 0xbcbc, 0xbcdb, 0xbcfe, 0xbdcc, 0xbecd, 0xbedd, 0xbfb4, 0xbfc6, + 0xbfc9, 0xc0b4, 0xc0ed, 0xc1cb, 0xc2db, 0xc3c7, 0xc4dc, 0xc4ea, 0xc5cc, 0xc6f7, + 0xc7f8, 0xc8ab, 0xc8cb, 0xc8d5, 0xc8e7, 0xc9cf, 0xc9fa, 0xcab1, 0xcab5, 0xcac7, + 0xcad0, 0xcad6, 0xcaf5, 0xcafd, 0xccec, 0xcdf8, 0xceaa, 0xcec4, 0xced2, 0xcee5, + 0xcfb5, 0xcfc2, 0xcfd6, 0xd0c2, 0xd0c5, 0xd0d0, 0xd0d4, 0xd1a7, 0xd2aa, 0xd2b2, + 0xd2b5, 0xd2bb, 0xd2d4, 0xd3c3, 0xd3d0, 0xd3fd, 0xd4c2, 0xd4da, 0xd5e2, 0xd6d0, +} + +func newRecognizer_gb_18030() *recognizerMultiByte { + return &recognizerMultiByte{ + "GB18030", + "zh", + charDecoder_gb_18030{}, + commonChars_gb_18030, + } +} diff --git a/vendor/github.com/xWTF/chardet/recognizer.go b/vendor/github.com/xWTF/chardet/recognizer.go new file mode 100644 index 000000000..70ebcf3e1 --- /dev/null +++ b/vendor/github.com/xWTF/chardet/recognizer.go @@ -0,0 +1,83 @@ +package chardet + +type recognizer interface { + Match(*recognizerInput, int) recognizerOutput +} + +type recognizerOutput Result + +type recognizerInput struct { + raw []byte + input []byte + tagStripped bool + byteStats []int + hasC1Bytes bool +} + +func newRecognizerInput(raw []byte, stripTag bool) *recognizerInput { + input, stripped := mayStripInput(raw, stripTag) + byteStats := computeByteStats(input) + return &recognizerInput{ + raw: raw, + input: input, + tagStripped: stripped, + byteStats: byteStats, + hasC1Bytes: computeHasC1Bytes(byteStats), + } +} + +func mayStripInput(raw []byte, stripTag bool) (out []byte, stripped bool) { + const inputBufferSize = 8192 + out = make([]byte, 0, inputBufferSize) + var badTags, openTags int32 + var inMarkup bool = false + stripped = false + if stripTag { + stripped = true + for _, c := range raw { + if c == '<' { + if inMarkup { + badTags += 1 + } + inMarkup = true + openTags += 1 + } + if !inMarkup { + out = append(out, c) + if len(out) >= inputBufferSize { + break + } + } + if c == '>' { + inMarkup = false + } + } + } + if openTags < 5 || openTags/5 < badTags || (len(out) < 100 && len(raw) > 600) { + limit := len(raw) + if limit > inputBufferSize { + limit = inputBufferSize + } + out = make([]byte, limit) + copy(out, raw[:limit]) + stripped = false + } + return +} + +func computeByteStats(input []byte) []int { + r := make([]int, 256) + for _, c := range input { + r[c] += 1 + } + return r +} + +func computeHasC1Bytes(byteStats []int) bool { + for _, count := range byteStats[0x80 : 0x9F+1] { + if count > 0 { + return true + } + } + return false +} diff --git a/vendor/github.com/xWTF/chardet/single_byte.go b/vendor/github.com/xWTF/chardet/single_byte.go new file mode 100644 index 000000000..3aa323ea5 --- /dev/null +++ b/vendor/github.com/xWTF/chardet/single_byte.go @@ -0,0 +1,883 @@ +package chardet + +// Recognizer for single byte charset family +type recognizerSingleByte struct { + charset string + hasC1ByteCharset string + language string + charMap *[256]byte + ngram *[64]uint32 +} + +func (r *recognizerSingleByte) Match(input *recognizerInput, order int) recognizerOutput { + var charset string = r.charset + if input.hasC1Bytes && len(r.hasC1ByteCharset) > 0 { + charset = r.hasC1ByteCharset + } + return recognizerOutput{ + Charset: charset, + Language: r.language, + Confidence: r.parseNgram(input.input), + order: order, + } +} + +type ngramState struct { + ngram uint32 + ignoreSpace bool + ngramCount, ngramHit uint32 + table *[64]uint32 +} + +func newNgramState(table *[64]uint32) *ngramState { + return &ngramState{ + ngram: 0, + ignoreSpace: false, + ngramCount: 0, + ngramHit: 0, + table: table, + } +} + +func (s *ngramState) AddByte(b byte) { + const ngramMask = 0xFFFFFF + if !(b == 0x20 && s.ignoreSpace) { + s.ngram = ((s.ngram << 8) | uint32(b)) & ngramMask + s.ignoreSpace = (s.ngram == 0x20) + s.ngramCount++ + if s.lookup() { + s.ngramHit++ + } + } + s.ignoreSpace = (b == 0x20) +} + +func (s *ngramState) HitRate() float32 { + if s.ngramCount == 0 { + return 0 + } + return float32(s.ngramHit) / float32(s.ngramCount) +} + +func (s *ngramState) lookup() bool { + var index int + if s.table[index+32] <= s.ngram { + index += 32 + } + if s.table[index+16] <= s.ngram { + index += 16 + } + if s.table[index+8] <= s.ngram { + index += 8 + } + if s.table[index+4] <= s.ngram { + index += 4 + } + if s.table[index+2] <= s.ngram { + index += 2 + } + if s.table[index+1] <= s.ngram { + index += 1 + } + if s.table[index] > s.ngram { + index -= 1 + } + if index < 0 || s.table[index] != s.ngram { + return false + } + return true +} + +func (r *recognizerSingleByte) parseNgram(input []byte) int { + state := newNgramState(r.ngram) + for _, inChar := range input { + c := r.charMap[inChar] + if c != 0 { + state.AddByte(c) + } + } + state.AddByte(0x20) + rate := state.HitRate() + if rate > 0.33 { + return 98 + } + return int(rate * 300) +} + +var charMap_8859_1 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0xAA, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0xB5, 0x20, 0x20, + 0x20, 0x20, 0xBA, 0x20, 0x20, 0x20, 0x20, 0x20, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0x20, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0x20, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, +} + +var ngrams_8859_1_en = [64]uint32{ + 0x206120, 0x20616E, 0x206265, 0x20636F, 0x20666F, 0x206861, 0x206865, 0x20696E, 0x206D61, 0x206F66, 0x207072, 0x207265, 0x207361, 0x207374, 0x207468, 0x20746F, + 0x207768, 0x616964, 0x616C20, 0x616E20, 0x616E64, 0x617320, 0x617420, 0x617465, 0x617469, 0x642061, 0x642074, 0x652061, 0x652073, 0x652074, 0x656420, 0x656E74, + 0x657220, 0x657320, 0x666F72, 0x686174, 0x686520, 0x686572, 0x696420, 0x696E20, 0x696E67, 0x696F6E, 0x697320, 0x6E2061, 0x6E2074, 0x6E6420, 0x6E6720, 0x6E7420, + 0x6F6620, 0x6F6E20, 0x6F7220, 0x726520, 0x727320, 0x732061, 0x732074, 0x736169, 0x737420, 0x742074, 0x746572, 0x746861, 0x746865, 0x74696F, 0x746F20, 0x747320, +} + +var ngrams_8859_1_da = [64]uint32{ + 0x206166, 0x206174, 0x206465, 0x20656E, 0x206572, 0x20666F, 0x206861, 0x206920, 0x206D65, 0x206F67, 0x2070E5, 0x207369, 0x207374, 0x207469, 0x207669, 0x616620, + 0x616E20, 0x616E64, 0x617220, 0x617420, 0x646520, 0x64656E, 0x646572, 0x646574, 0x652073, 0x656420, 0x656465, 0x656E20, 0x656E64, 0x657220, 0x657265, 0x657320, + 0x657420, 0x666F72, 0x676520, 0x67656E, 0x676572, 0x696765, 0x696C20, 0x696E67, 0x6B6520, 0x6B6B65, 0x6C6572, 0x6C6967, 0x6C6C65, 0x6D6564, 0x6E6465, 0x6E6520, + 0x6E6720, 0x6E6765, 0x6F6720, 0x6F6D20, 0x6F7220, 0x70E520, 0x722064, 0x722065, 0x722073, 0x726520, 0x737465, 0x742073, 0x746520, 0x746572, 0x74696C, 0x766572, +} + +var ngrams_8859_1_de = [64]uint32{ + 0x20616E, 0x206175, 0x206265, 0x206461, 0x206465, 0x206469, 0x206569, 0x206765, 0x206861, 0x20696E, 0x206D69, 0x207363, 0x207365, 0x20756E, 0x207665, 0x20766F, + 0x207765, 0x207A75, 0x626572, 0x636820, 0x636865, 0x636874, 0x646173, 0x64656E, 0x646572, 0x646965, 0x652064, 0x652073, 0x65696E, 0x656974, 0x656E20, 0x657220, + 0x657320, 0x67656E, 0x68656E, 0x687420, 0x696368, 0x696520, 0x696E20, 0x696E65, 0x697420, 0x6C6963, 0x6C6C65, 0x6E2061, 0x6E2064, 0x6E2073, 0x6E6420, 0x6E6465, + 0x6E6520, 0x6E6720, 0x6E6765, 0x6E7465, 0x722064, 0x726465, 0x726569, 0x736368, 0x737465, 0x742064, 0x746520, 0x74656E, 0x746572, 0x756E64, 0x756E67, 0x766572, +} + +var ngrams_8859_1_es = [64]uint32{ + 0x206120, 0x206361, 0x20636F, 0x206465, 0x20656C, 0x20656E, 0x206573, 0x20696E, 0x206C61, 0x206C6F, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207265, 0x207365, + 0x20756E, 0x207920, 0x612063, 0x612064, 0x612065, 0x61206C, 0x612070, 0x616369, 0x61646F, 0x616C20, 0x617220, 0x617320, 0x6369F3, 0x636F6E, 0x646520, 0x64656C, + 0x646F20, 0x652064, 0x652065, 0x65206C, 0x656C20, 0x656E20, 0x656E74, 0x657320, 0x657374, 0x69656E, 0x69F36E, 0x6C6120, 0x6C6F73, 0x6E2065, 0x6E7465, 0x6F2064, + 0x6F2065, 0x6F6E20, 0x6F7220, 0x6F7320, 0x706172, 0x717565, 0x726120, 0x726573, 0x732064, 0x732065, 0x732070, 0x736520, 0x746520, 0x746F20, 0x756520, 0xF36E20, +} + +var ngrams_8859_1_fr = [64]uint32{ + 0x206175, 0x20636F, 0x206461, 0x206465, 0x206475, 0x20656E, 0x206574, 0x206C61, 0x206C65, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207365, 0x20736F, 0x20756E, + 0x20E020, 0x616E74, 0x617469, 0x636520, 0x636F6E, 0x646520, 0x646573, 0x647520, 0x652061, 0x652063, 0x652064, 0x652065, 0x65206C, 0x652070, 0x652073, 0x656E20, + 0x656E74, 0x657220, 0x657320, 0x657420, 0x657572, 0x696F6E, 0x697320, 0x697420, 0x6C6120, 0x6C6520, 0x6C6573, 0x6D656E, 0x6E2064, 0x6E6520, 0x6E7320, 0x6E7420, + 0x6F6E20, 0x6F6E74, 0x6F7572, 0x717565, 0x72206C, 0x726520, 0x732061, 0x732064, 0x732065, 0x73206C, 0x732070, 0x742064, 0x746520, 0x74696F, 0x756520, 0x757220, +} + +var ngrams_8859_1_it = [64]uint32{ + 0x20616C, 0x206368, 0x20636F, 0x206465, 0x206469, 0x206520, 0x20696C, 0x20696E, 0x206C61, 0x207065, 0x207072, 0x20756E, 0x612063, 0x612064, 0x612070, 0x612073, + 0x61746F, 0x636865, 0x636F6E, 0x64656C, 0x646920, 0x652061, 0x652063, 0x652064, 0x652069, 0x65206C, 0x652070, 0x652073, 0x656C20, 0x656C6C, 0x656E74, 0x657220, + 0x686520, 0x692061, 0x692063, 0x692064, 0x692073, 0x696120, 0x696C20, 0x696E20, 0x696F6E, 0x6C6120, 0x6C6520, 0x6C6920, 0x6C6C61, 0x6E6520, 0x6E6920, 0x6E6F20, + 0x6E7465, 0x6F2061, 0x6F2064, 0x6F2069, 0x6F2073, 0x6F6E20, 0x6F6E65, 0x706572, 0x726120, 0x726520, 0x736920, 0x746120, 0x746520, 0x746920, 0x746F20, 0x7A696F, +} + +var ngrams_8859_1_nl = [64]uint32{ + 0x20616C, 0x206265, 0x206461, 0x206465, 0x206469, 0x206565, 0x20656E, 0x206765, 0x206865, 0x20696E, 0x206D61, 0x206D65, 0x206F70, 0x207465, 0x207661, 0x207665, + 0x20766F, 0x207765, 0x207A69, 0x61616E, 0x616172, 0x616E20, 0x616E64, 0x617220, 0x617420, 0x636874, 0x646520, 0x64656E, 0x646572, 0x652062, 0x652076, 0x65656E, + 0x656572, 0x656E20, 0x657220, 0x657273, 0x657420, 0x67656E, 0x686574, 0x696520, 0x696E20, 0x696E67, 0x697320, 0x6E2062, 0x6E2064, 0x6E2065, 0x6E2068, 0x6E206F, + 0x6E2076, 0x6E6465, 0x6E6720, 0x6F6E64, 0x6F6F72, 0x6F7020, 0x6F7220, 0x736368, 0x737465, 0x742064, 0x746520, 0x74656E, 0x746572, 0x76616E, 0x766572, 0x766F6F, +} + +var ngrams_8859_1_no = [64]uint32{ + 0x206174, 0x206176, 0x206465, 0x20656E, 0x206572, 0x20666F, 0x206861, 0x206920, 0x206D65, 0x206F67, 0x2070E5, 0x207365, 0x20736B, 0x20736F, 0x207374, 0x207469, + 0x207669, 0x20E520, 0x616E64, 0x617220, 0x617420, 0x646520, 0x64656E, 0x646574, 0x652073, 0x656420, 0x656E20, 0x656E65, 0x657220, 0x657265, 0x657420, 0x657474, + 0x666F72, 0x67656E, 0x696B6B, 0x696C20, 0x696E67, 0x6B6520, 0x6B6B65, 0x6C6520, 0x6C6C65, 0x6D6564, 0x6D656E, 0x6E2073, 0x6E6520, 0x6E6720, 0x6E6765, 0x6E6E65, + 0x6F6720, 0x6F6D20, 0x6F7220, 0x70E520, 0x722073, 0x726520, 0x736F6D, 0x737465, 0x742073, 0x746520, 0x74656E, 0x746572, 0x74696C, 0x747420, 0x747465, 0x766572, +} + +var ngrams_8859_1_pt = [64]uint32{ + 0x206120, 0x20636F, 0x206461, 0x206465, 0x20646F, 0x206520, 0x206573, 0x206D61, 0x206E6F, 0x206F20, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207265, 0x207365, + 0x20756D, 0x612061, 0x612063, 0x612064, 0x612070, 0x616465, 0x61646F, 0x616C20, 0x617220, 0x617261, 0x617320, 0x636F6D, 0x636F6E, 0x646120, 0x646520, 0x646F20, + 0x646F73, 0x652061, 0x652064, 0x656D20, 0x656E74, 0x657320, 0x657374, 0x696120, 0x696361, 0x6D656E, 0x6E7465, 0x6E746F, 0x6F2061, 0x6F2063, 0x6F2064, 0x6F2065, + 0x6F2070, 0x6F7320, 0x706172, 0x717565, 0x726120, 0x726573, 0x732061, 0x732064, 0x732065, 0x732070, 0x737461, 0x746520, 0x746F20, 0x756520, 0xE36F20, 0xE7E36F, +} + +var ngrams_8859_1_sv = [64]uint32{ + 0x206174, 0x206176, 0x206465, 0x20656E, 0x2066F6, 0x206861, 0x206920, 0x20696E, 0x206B6F, 0x206D65, 0x206F63, 0x2070E5, 0x20736B, 0x20736F, 0x207374, 0x207469, + 0x207661, 0x207669, 0x20E472, 0x616465, 0x616E20, 0x616E64, 0x617220, 0x617474, 0x636820, 0x646520, 0x64656E, 0x646572, 0x646574, 0x656420, 0x656E20, 0x657220, + 0x657420, 0x66F672, 0x67656E, 0x696C6C, 0x696E67, 0x6B6120, 0x6C6C20, 0x6D6564, 0x6E2073, 0x6E6120, 0x6E6465, 0x6E6720, 0x6E6765, 0x6E696E, 0x6F6368, 0x6F6D20, + 0x6F6E20, 0x70E520, 0x722061, 0x722073, 0x726120, 0x736B61, 0x736F6D, 0x742073, 0x746120, 0x746520, 0x746572, 0x74696C, 0x747420, 0x766172, 0xE47220, 0xF67220, +} + +func newRecognizer_8859_1(language string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "ISO-8859-1", + hasC1ByteCharset: "windows-1252", + language: language, + charMap: &charMap_8859_1, + ngram: ngram, + } +} + +func newRecognizer_8859_1_en() *recognizerSingleByte { + return newRecognizer_8859_1("en", &ngrams_8859_1_en) +} +func newRecognizer_8859_1_da() *recognizerSingleByte { + return newRecognizer_8859_1("da", &ngrams_8859_1_da) +} +func newRecognizer_8859_1_de() *recognizerSingleByte { + return newRecognizer_8859_1("de", &ngrams_8859_1_de) +} +func newRecognizer_8859_1_es() *recognizerSingleByte { + return newRecognizer_8859_1("es", &ngrams_8859_1_es) +} +func newRecognizer_8859_1_fr() *recognizerSingleByte { + return newRecognizer_8859_1("fr", &ngrams_8859_1_fr) +} +func newRecognizer_8859_1_it() *recognizerSingleByte { + return newRecognizer_8859_1("it", &ngrams_8859_1_it) +} +func newRecognizer_8859_1_nl() *recognizerSingleByte { + return newRecognizer_8859_1("nl", &ngrams_8859_1_nl) +} +func newRecognizer_8859_1_no() *recognizerSingleByte { + return newRecognizer_8859_1("no", &ngrams_8859_1_no) +} +func newRecognizer_8859_1_pt() *recognizerSingleByte { + return newRecognizer_8859_1("pt", &ngrams_8859_1_pt) +} +func newRecognizer_8859_1_sv() *recognizerSingleByte { + return newRecognizer_8859_1("sv", &ngrams_8859_1_sv) +} + +var charMap_8859_2 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0xB1, 0x20, 0xB3, 0x20, 0xB5, 0xB6, 0x20, + 0x20, 0xB9, 0xBA, 0xBB, 0xBC, 0x20, 0xBE, 0xBF, + 0x20, 0xB1, 0x20, 0xB3, 0x20, 0xB5, 0xB6, 0xB7, + 0x20, 0xB9, 0xBA, 0xBB, 0xBC, 0x20, 0xBE, 0xBF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0x20, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0x20, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0x20, +} + +var ngrams_8859_2_cs = [64]uint32{ + 0x206120, 0x206279, 0x20646F, 0x206A65, 0x206E61, 0x206E65, 0x206F20, 0x206F64, 0x20706F, 0x207072, 0x2070F8, 0x20726F, 0x207365, 0x20736F, 0x207374, 0x20746F, + 0x207620, 0x207679, 0x207A61, 0x612070, 0x636520, 0x636820, 0x652070, 0x652073, 0x652076, 0x656D20, 0x656EED, 0x686F20, 0x686F64, 0x697374, 0x6A6520, 0x6B7465, + 0x6C6520, 0x6C6920, 0x6E6120, 0x6EE920, 0x6EEC20, 0x6EED20, 0x6F2070, 0x6F646E, 0x6F6A69, 0x6F7374, 0x6F7520, 0x6F7661, 0x706F64, 0x706F6A, 0x70726F, 0x70F865, + 0x736520, 0x736F75, 0x737461, 0x737469, 0x73746E, 0x746572, 0x746EED, 0x746F20, 0x752070, 0xBE6520, 0xE16EED, 0xE9686F, 0xED2070, 0xED2073, 0xED6D20, 0xF86564, +} + +var ngrams_8859_2_hu = [64]uint32{ + 0x206120, 0x20617A, 0x206265, 0x206567, 0x20656C, 0x206665, 0x206861, 0x20686F, 0x206973, 0x206B65, 0x206B69, 0x206BF6, 0x206C65, 0x206D61, 0x206D65, 0x206D69, + 0x206E65, 0x20737A, 0x207465, 0x20E973, 0x612061, 0x61206B, 0x61206D, 0x612073, 0x616B20, 0x616E20, 0x617A20, 0x62616E, 0x62656E, 0x656779, 0x656B20, 0x656C20, + 0x656C65, 0x656D20, 0x656E20, 0x657265, 0x657420, 0x657465, 0x657474, 0x677920, 0x686F67, 0x696E74, 0x697320, 0x6B2061, 0x6BF67A, 0x6D6567, 0x6D696E, 0x6E2061, + 0x6E616B, 0x6E656B, 0x6E656D, 0x6E7420, 0x6F6779, 0x732061, 0x737A65, 0x737A74, 0x737AE1, 0x73E967, 0x742061, 0x747420, 0x74E173, 0x7A6572, 0xE16E20, 0xE97320, +} + +var ngrams_8859_2_pl = [64]uint32{ + 0x20637A, 0x20646F, 0x206920, 0x206A65, 0x206B6F, 0x206D61, 0x206D69, 0x206E61, 0x206E69, 0x206F64, 0x20706F, 0x207072, 0x207369, 0x207720, 0x207769, 0x207779, + 0x207A20, 0x207A61, 0x612070, 0x612077, 0x616E69, 0x636820, 0x637A65, 0x637A79, 0x646F20, 0x647A69, 0x652070, 0x652073, 0x652077, 0x65207A, 0x65676F, 0x656A20, + 0x656D20, 0x656E69, 0x676F20, 0x696120, 0x696520, 0x69656A, 0x6B6120, 0x6B6920, 0x6B6965, 0x6D6965, 0x6E6120, 0x6E6961, 0x6E6965, 0x6F2070, 0x6F7761, 0x6F7769, + 0x706F6C, 0x707261, 0x70726F, 0x70727A, 0x727A65, 0x727A79, 0x7369EA, 0x736B69, 0x737461, 0x776965, 0x796368, 0x796D20, 0x7A6520, 0x7A6965, 0x7A7920, 0xF37720, +} + +var ngrams_8859_2_ro = [64]uint32{ + 0x206120, 0x206163, 0x206361, 0x206365, 0x20636F, 0x206375, 0x206465, 0x206469, 0x206C61, 0x206D61, 0x207065, 0x207072, 0x207365, 0x2073E3, 0x20756E, 0x20BA69, + 0x20EE6E, 0x612063, 0x612064, 0x617265, 0x617420, 0x617465, 0x617520, 0x636172, 0x636F6E, 0x637520, 0x63E320, 0x646520, 0x652061, 0x652063, 0x652064, 0x652070, + 0x652073, 0x656120, 0x656920, 0x656C65, 0x656E74, 0x657374, 0x692061, 0x692063, 0x692064, 0x692070, 0x696520, 0x696920, 0x696E20, 0x6C6120, 0x6C6520, 0x6C6F72, + 0x6C7569, 0x6E6520, 0x6E7472, 0x6F7220, 0x70656E, 0x726520, 0x726561, 0x727520, 0x73E320, 0x746520, 0x747275, 0x74E320, 0x756920, 0x756C20, 0xBA6920, 0xEE6E20, +} + +func newRecognizer_8859_2(language string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "ISO-8859-2", + hasC1ByteCharset: "windows-1250", + language: language, + charMap: &charMap_8859_2, + ngram: ngram, + } +} + +func newRecognizer_8859_2_cs() *recognizerSingleByte { + return newRecognizer_8859_2("cs", &ngrams_8859_2_cs) +} +func newRecognizer_8859_2_hu() *recognizerSingleByte { + return newRecognizer_8859_2("hu", &ngrams_8859_2_hu) +} +func newRecognizer_8859_2_pl() *recognizerSingleByte { + return newRecognizer_8859_2("pl", &ngrams_8859_2_pl) +} +func newRecognizer_8859_2_ro() *recognizerSingleByte { + return newRecognizer_8859_2("ro", &ngrams_8859_2_ro) +} + +var charMap_8859_5 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0x20, 0xFE, 0xFF, + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, + 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, + 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0x20, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0x20, 0xFE, 0xFF, +} + +var ngrams_8859_5_ru = [64]uint32{ + 0x20D220, 0x20D2DE, 0x20D4DE, 0x20D7D0, 0x20D820, 0x20DAD0, 0x20DADE, 0x20DDD0, 0x20DDD5, 0x20DED1, 0x20DFDE, 0x20DFE0, 0x20E0D0, 0x20E1DE, 0x20E1E2, 0x20E2DE, + 0x20E7E2, 0x20EDE2, 0xD0DDD8, 0xD0E2EC, 0xD3DE20, 0xD5DBEC, 0xD5DDD8, 0xD5E1E2, 0xD5E220, 0xD820DF, 0xD8D520, 0xD8D820, 0xD8EF20, 0xDBD5DD, 0xDBD820, 0xDBECDD, + 0xDDD020, 0xDDD520, 0xDDD8D5, 0xDDD8EF, 0xDDDE20, 0xDDDED2, 0xDE20D2, 0xDE20DF, 0xDE20E1, 0xDED220, 0xDED2D0, 0xDED3DE, 0xDED920, 0xDEDBEC, 0xDEDC20, 0xDEE1E2, + 0xDFDEDB, 0xDFE0D5, 0xDFE0D8, 0xDFE0DE, 0xE0D0D2, 0xE0D5D4, 0xE1E2D0, 0xE1E2D2, 0xE1E2D8, 0xE1EF20, 0xE2D5DB, 0xE2DE20, 0xE2DEE0, 0xE2EC20, 0xE7E2DE, 0xEBE520, +} + +func newRecognizer_8859_5(language string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "ISO-8859-5", + language: language, + charMap: &charMap_8859_5, + ngram: ngram, + } +} + +func newRecognizer_8859_5_ru() *recognizerSingleByte { + return newRecognizer_8859_5("ru", &ngrams_8859_5_ru) +} + +var charMap_8859_6 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, + 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, + 0xD8, 0xD9, 0xDA, 0x20, 0x20, 0x20, 0x20, 0x20, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, +} + +var ngrams_8859_6_ar = [64]uint32{ + 0x20C7E4, 0x20C7E6, 0x20C8C7, 0x20D9E4, 0x20E1EA, 0x20E4E4, 0x20E5E6, 0x20E8C7, 0xC720C7, 0xC7C120, 0xC7CA20, 0xC7D120, 0xC7E420, 0xC7E4C3, 0xC7E4C7, 0xC7E4C8, + 0xC7E4CA, 0xC7E4CC, 0xC7E4CD, 0xC7E4CF, 0xC7E4D3, 0xC7E4D9, 0xC7E4E2, 0xC7E4E5, 0xC7E4E8, 0xC7E4EA, 0xC7E520, 0xC7E620, 0xC7E6CA, 0xC820C7, 0xC920C7, 0xC920E1, + 0xC920E4, 0xC920E5, 0xC920E8, 0xCA20C7, 0xCF20C7, 0xCFC920, 0xD120C7, 0xD1C920, 0xD320C7, 0xD920C7, 0xD9E4E9, 0xE1EA20, 0xE420C7, 0xE4C920, 0xE4E920, 0xE4EA20, + 0xE520C7, 0xE5C720, 0xE5C920, 0xE5E620, 0xE620C7, 0xE720C7, 0xE7C720, 0xE8C7E4, 0xE8E620, 0xE920C7, 0xEA20C7, 0xEA20E5, 0xEA20E8, 0xEAC920, 0xEAD120, 0xEAE620, +} + +func newRecognizer_8859_6(language string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "ISO-8859-6", + language: language, + charMap: &charMap_8859_6, + ngram: ngram, + } +} + +func newRecognizer_8859_6_ar() *recognizerSingleByte { + return newRecognizer_8859_6("ar", &ngrams_8859_6_ar) +} + +var charMap_8859_7 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0xA1, 0xA2, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xDC, 0x20, + 0xDD, 0xDE, 0xDF, 0x20, 0xFC, 0x20, 0xFD, 0xFE, + 0xC0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0x20, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0xFB, 0xDC, 0xDD, 0xDE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0x20, +} + +var ngrams_8859_7_el = [64]uint32{ + 0x20E1ED, 0x20E1F0, 0x20E3E9, 0x20E4E9, 0x20E5F0, 0x20E720, 0x20EAE1, 0x20ECE5, 0x20EDE1, 0x20EF20, 0x20F0E1, 0x20F0EF, 0x20F0F1, 0x20F3F4, 0x20F3F5, 0x20F4E7, + 0x20F4EF, 0xDFE120, 0xE120E1, 0xE120F4, 0xE1E920, 0xE1ED20, 0xE1F0FC, 0xE1F220, 0xE3E9E1, 0xE5E920, 0xE5F220, 0xE720F4, 0xE7ED20, 0xE7F220, 0xE920F4, 0xE9E120, + 0xE9EADE, 0xE9F220, 0xEAE1E9, 0xEAE1F4, 0xECE520, 0xED20E1, 0xED20E5, 0xED20F0, 0xEDE120, 0xEFF220, 0xEFF520, 0xF0EFF5, 0xF0F1EF, 0xF0FC20, 0xF220E1, 0xF220E5, + 0xF220EA, 0xF220F0, 0xF220F4, 0xF3E520, 0xF3E720, 0xF3F4EF, 0xF4E120, 0xF4E1E9, 0xF4E7ED, 0xF4E7F2, 0xF4E9EA, 0xF4EF20, 0xF4EFF5, 0xF4F9ED, 0xF9ED20, 0xFEED20, +} + +func newRecognizer_8859_7(language string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "ISO-8859-7", + hasC1ByteCharset: "windows-1253", + language: language, + charMap: &charMap_8859_7, + ngram: ngram, + } +} + +func newRecognizer_8859_7_el() *recognizerSingleByte { + return newRecognizer_8859_7("el", &ngrams_8859_7_el) +} + +var charMap_8859_8 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0xB5, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0x20, 0x20, 0x20, 0x20, 0x20, +} + +var ngrams_8859_8_I_he = [64]uint32{ + 0x20E0E5, 0x20E0E7, 0x20E0E9, 0x20E0FA, 0x20E1E9, 0x20E1EE, 0x20E4E0, 0x20E4E5, 0x20E4E9, 0x20E4EE, 0x20E4F2, 0x20E4F9, 0x20E4FA, 0x20ECE0, 0x20ECE4, 0x20EEE0, + 0x20F2EC, 0x20F9EC, 0xE0FA20, 0xE420E0, 0xE420E1, 0xE420E4, 0xE420EC, 0xE420EE, 0xE420F9, 0xE4E5E0, 0xE5E020, 0xE5ED20, 0xE5EF20, 0xE5F820, 0xE5FA20, 0xE920E4, + 0xE9E420, 0xE9E5FA, 0xE9E9ED, 0xE9ED20, 0xE9EF20, 0xE9F820, 0xE9FA20, 0xEC20E0, 0xEC20E4, 0xECE020, 0xECE420, 0xED20E0, 0xED20E1, 0xED20E4, 0xED20EC, 0xED20EE, + 0xED20F9, 0xEEE420, 0xEF20E4, 0xF0E420, 0xF0E920, 0xF0E9ED, 0xF2EC20, 0xF820E4, 0xF8E9ED, 0xF9EC20, 0xFA20E0, 0xFA20E1, 0xFA20E4, 0xFA20EC, 0xFA20EE, 0xFA20F9, +} + +var ngrams_8859_8_he = [64]uint32{ + 0x20E0E5, 0x20E0EC, 0x20E4E9, 0x20E4EC, 0x20E4EE, 0x20E4F0, 0x20E9F0, 0x20ECF2, 0x20ECF9, 0x20EDE5, 0x20EDE9, 0x20EFE5, 0x20EFE9, 0x20F8E5, 0x20F8E9, 0x20FAE0, + 0x20FAE5, 0x20FAE9, 0xE020E4, 0xE020EC, 0xE020ED, 0xE020FA, 0xE0E420, 0xE0E5E4, 0xE0EC20, 0xE0EE20, 0xE120E4, 0xE120ED, 0xE120FA, 0xE420E4, 0xE420E9, 0xE420EC, + 0xE420ED, 0xE420EF, 0xE420F8, 0xE420FA, 0xE4EC20, 0xE5E020, 0xE5E420, 0xE7E020, 0xE9E020, 0xE9E120, 0xE9E420, 0xEC20E4, 0xEC20ED, 0xEC20FA, 0xECF220, 0xECF920, + 0xEDE9E9, 0xEDE9F0, 0xEDE9F8, 0xEE20E4, 0xEE20ED, 0xEE20FA, 0xEEE120, 0xEEE420, 0xF2E420, 0xF920E4, 0xF920ED, 0xF920FA, 0xF9E420, 0xFAE020, 0xFAE420, 0xFAE5E9, +} + +func newRecognizer_8859_8(language string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "ISO-8859-8", + hasC1ByteCharset: "windows-1255", + language: language, + charMap: &charMap_8859_8, + ngram: ngram, + } +} + +func newRecognizer_8859_8_I_he() *recognizerSingleByte { + r := newRecognizer_8859_8("he", &ngrams_8859_8_I_he) + r.charset = "ISO-8859-8-I" + return r +} + +func newRecognizer_8859_8_he() *recognizerSingleByte { + return newRecognizer_8859_8("he", &ngrams_8859_8_he) +} + +var charMap_8859_9 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0xAA, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0xB5, 0x20, 0x20, + 0x20, 0x20, 0xBA, 0x20, 0x20, 0x20, 0x20, 0x20, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0x20, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0x69, 0xFE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0x20, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, +} + +var ngrams_8859_9_tr = [64]uint32{ + 0x206261, 0x206269, 0x206275, 0x206461, 0x206465, 0x206765, 0x206861, 0x20696C, 0x206B61, 0x206B6F, 0x206D61, 0x206F6C, 0x207361, 0x207461, 0x207665, 0x207961, + 0x612062, 0x616B20, 0x616C61, 0x616D61, 0x616E20, 0x616EFD, 0x617220, 0x617261, 0x6172FD, 0x6173FD, 0x617961, 0x626972, 0x646120, 0x646520, 0x646920, 0x652062, + 0x65206B, 0x656469, 0x656E20, 0x657220, 0x657269, 0x657369, 0x696C65, 0x696E20, 0x696E69, 0x697220, 0x6C616E, 0x6C6172, 0x6C6520, 0x6C6572, 0x6E2061, 0x6E2062, + 0x6E206B, 0x6E6461, 0x6E6465, 0x6E6520, 0x6E6920, 0x6E696E, 0x6EFD20, 0x72696E, 0x72FD6E, 0x766520, 0x796120, 0x796F72, 0xFD6E20, 0xFD6E64, 0xFD6EFD, 0xFDF0FD, +} + +func newRecognizer_8859_9(language string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "ISO-8859-9", + hasC1ByteCharset: "windows-1254", + language: language, + charMap: &charMap_8859_9, + ngram: ngram, + } +} + +func newRecognizer_8859_9_tr() *recognizerSingleByte { + return newRecognizer_8859_9("tr", &ngrams_8859_9_tr) +} + +var charMap_windows_1256 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x81, 0x20, 0x83, 0x20, 0x20, 0x20, 0x20, + 0x88, 0x20, 0x8A, 0x20, 0x9C, 0x8D, 0x8E, 0x8F, + 0x90, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x98, 0x20, 0x9A, 0x20, 0x9C, 0x20, 0x20, 0x9F, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0xAA, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0xB5, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, + 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0x20, + 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0x20, 0x20, 0x20, 0x20, 0xF4, 0x20, 0x20, 0x20, + 0x20, 0xF9, 0x20, 0xFB, 0xFC, 0x20, 0x20, 0xFF, +} + +var ngrams_windows_1256 = [64]uint32{ + 0x20C7E1, 0x20C7E4, 0x20C8C7, 0x20DAE1, 0x20DDED, 0x20E1E1, 0x20E3E4, 0x20E6C7, 0xC720C7, 0xC7C120, 0xC7CA20, 0xC7D120, 0xC7E120, 0xC7E1C3, 0xC7E1C7, 0xC7E1C8, + 0xC7E1CA, 0xC7E1CC, 0xC7E1CD, 0xC7E1CF, 0xC7E1D3, 0xC7E1DA, 0xC7E1DE, 0xC7E1E3, 0xC7E1E6, 0xC7E1ED, 0xC7E320, 0xC7E420, 0xC7E4CA, 0xC820C7, 0xC920C7, 0xC920DD, + 0xC920E1, 0xC920E3, 0xC920E6, 0xCA20C7, 0xCF20C7, 0xCFC920, 0xD120C7, 0xD1C920, 0xD320C7, 0xDA20C7, 0xDAE1EC, 0xDDED20, 0xE120C7, 0xE1C920, 0xE1EC20, 0xE1ED20, + 0xE320C7, 0xE3C720, 0xE3C920, 0xE3E420, 0xE420C7, 0xE520C7, 0xE5C720, 0xE6C7E1, 0xE6E420, 0xEC20C7, 0xED20C7, 0xED20E3, 0xED20E6, 0xEDC920, 0xEDD120, 0xEDE420, +} + +func newRecognizer_windows_1256() *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "windows-1256", + language: "ar", + charMap: &charMap_windows_1256, + ngram: &ngrams_windows_1256, + } +} + +var charMap_windows_1251 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x90, 0x83, 0x20, 0x83, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x9A, 0x20, 0x9C, 0x9D, 0x9E, 0x9F, + 0x90, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x9A, 0x20, 0x9C, 0x9D, 0x9E, 0x9F, + 0x20, 0xA2, 0xA2, 0xBC, 0x20, 0xB4, 0x20, 0x20, + 0xB8, 0x20, 0xBA, 0x20, 0x20, 0x20, 0x20, 0xBF, + 0x20, 0x20, 0xB3, 0xB3, 0xB4, 0xB5, 0x20, 0x20, + 0xB8, 0x20, 0xBA, 0x20, 0xBC, 0xBE, 0xBE, 0xBF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, +} + +var ngrams_windows_1251 = [64]uint32{ + 0x20E220, 0x20E2EE, 0x20E4EE, 0x20E7E0, 0x20E820, 0x20EAE0, 0x20EAEE, 0x20EDE0, 0x20EDE5, 0x20EEE1, 0x20EFEE, 0x20EFF0, 0x20F0E0, 0x20F1EE, 0x20F1F2, 0x20F2EE, + 0x20F7F2, 0x20FDF2, 0xE0EDE8, 0xE0F2FC, 0xE3EE20, 0xE5EBFC, 0xE5EDE8, 0xE5F1F2, 0xE5F220, 0xE820EF, 0xE8E520, 0xE8E820, 0xE8FF20, 0xEBE5ED, 0xEBE820, 0xEBFCED, + 0xEDE020, 0xEDE520, 0xEDE8E5, 0xEDE8FF, 0xEDEE20, 0xEDEEE2, 0xEE20E2, 0xEE20EF, 0xEE20F1, 0xEEE220, 0xEEE2E0, 0xEEE3EE, 0xEEE920, 0xEEEBFC, 0xEEEC20, 0xEEF1F2, + 0xEFEEEB, 0xEFF0E5, 0xEFF0E8, 0xEFF0EE, 0xF0E0E2, 0xF0E5E4, 0xF1F2E0, 0xF1F2E2, 0xF1F2E8, 0xF1FF20, 0xF2E5EB, 0xF2EE20, 0xF2EEF0, 0xF2FC20, 0xF7F2EE, 0xFBF520, +} + +func newRecognizer_windows_1251() *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "windows-1251", + language: "ar", + charMap: &charMap_windows_1251, + ngram: &ngrams_windows_1251, + } +} + +var charMap_KOI8_R = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0xA3, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0xA3, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, + 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, + 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, + 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, + 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, + 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, +} + +var ngrams_KOI8_R = [64]uint32{ + 0x20C4CF, 0x20C920, 0x20CBC1, 0x20CBCF, 0x20CEC1, 0x20CEC5, 0x20CFC2, 0x20D0CF, 0x20D0D2, 0x20D2C1, 0x20D3CF, 0x20D3D4, 0x20D4CF, 0x20D720, 0x20D7CF, 0x20DAC1, + 0x20DCD4, 0x20DED4, 0xC1CEC9, 0xC1D4D8, 0xC5CCD8, 0xC5CEC9, 0xC5D3D4, 0xC5D420, 0xC7CF20, 0xC920D0, 0xC9C520, 0xC9C920, 0xC9D120, 0xCCC5CE, 0xCCC920, 0xCCD8CE, + 0xCEC120, 0xCEC520, 0xCEC9C5, 0xCEC9D1, 0xCECF20, 0xCECFD7, 0xCF20D0, 0xCF20D3, 0xCF20D7, 0xCFC7CF, 0xCFCA20, 0xCFCCD8, 0xCFCD20, 0xCFD3D4, 0xCFD720, 0xCFD7C1, + 0xD0CFCC, 0xD0D2C5, 0xD0D2C9, 0xD0D2CF, 0xD2C1D7, 0xD2C5C4, 0xD3D120, 0xD3D4C1, 0xD3D4C9, 0xD3D4D7, 0xD4C5CC, 0xD4CF20, 0xD4CFD2, 0xD4D820, 0xD9C820, 0xDED4CF, +} + +func newRecognizer_KOI8_R() *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "KOI8-R", + language: "ru", + charMap: &charMap_KOI8_R, + ngram: &ngrams_KOI8_R, + } +} + +var charMap_IBM424_he = [256]byte{ + /* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ + /* 0- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 1- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 2- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 3- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 4- */ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 5- */ 0x40, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 6- */ 0x40, 0x40, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 7- */ 0x40, 0x71, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x00, 0x40, 0x40, + /* 8- */ 0x40, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 9- */ 0x40, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* A- */ 0xA0, 0x40, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* B- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* C- */ 0x40, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* D- */ 0x40, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* E- */ 0x40, 0x40, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* F- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, +} + +var ngrams_IBM424_he_rtl = [64]uint32{ + 0x404146, 0x404148, 0x404151, 0x404171, 0x404251, 0x404256, 0x404541, 0x404546, 0x404551, 0x404556, 0x404562, 0x404569, 0x404571, 0x405441, 0x405445, 0x405641, + 0x406254, 0x406954, 0x417140, 0x454041, 0x454042, 0x454045, 0x454054, 0x454056, 0x454069, 0x454641, 0x464140, 0x465540, 0x465740, 0x466840, 0x467140, 0x514045, + 0x514540, 0x514671, 0x515155, 0x515540, 0x515740, 0x516840, 0x517140, 0x544041, 0x544045, 0x544140, 0x544540, 0x554041, 0x554042, 0x554045, 0x554054, 0x554056, + 0x554069, 0x564540, 0x574045, 0x584540, 0x585140, 0x585155, 0x625440, 0x684045, 0x685155, 0x695440, 0x714041, 0x714042, 0x714045, 0x714054, 0x714056, 0x714069, +} + +var ngrams_IBM424_he_ltr = [64]uint32{ + 0x404146, 0x404154, 0x404551, 0x404554, 0x404556, 0x404558, 0x405158, 0x405462, 0x405469, 0x405546, 0x405551, 0x405746, 0x405751, 0x406846, 0x406851, 0x407141, + 0x407146, 0x407151, 0x414045, 0x414054, 0x414055, 0x414071, 0x414540, 0x414645, 0x415440, 0x415640, 0x424045, 0x424055, 0x424071, 0x454045, 0x454051, 0x454054, + 0x454055, 0x454057, 0x454068, 0x454071, 0x455440, 0x464140, 0x464540, 0x484140, 0x514140, 0x514240, 0x514540, 0x544045, 0x544055, 0x544071, 0x546240, 0x546940, + 0x555151, 0x555158, 0x555168, 0x564045, 0x564055, 0x564071, 0x564240, 0x564540, 0x624540, 0x694045, 0x694055, 0x694071, 0x694540, 0x714140, 0x714540, 0x714651, +} + +func newRecognizer_IBM424_he(charset string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: charset, + language: "he", + charMap: &charMap_IBM424_he, + ngram: ngram, + } +} + +func newRecognizer_IBM424_he_rtl() *recognizerSingleByte { + return newRecognizer_IBM424_he("IBM424_rtl", &ngrams_IBM424_he_rtl) +} + +func newRecognizer_IBM424_he_ltr() *recognizerSingleByte { + return newRecognizer_IBM424_he("IBM424_ltr", &ngrams_IBM424_he_ltr) +} + +var charMap_IBM420_ar = [256]byte{ + /* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ + /* 0- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 1- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 2- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 3- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 4- */ 0x40, 0x40, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 5- */ 0x40, 0x51, 0x52, 0x40, 0x40, 0x55, 0x56, 0x57, 0x58, 0x59, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 6- */ 0x40, 0x40, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 7- */ 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 8- */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, + /* 9- */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, + /* A- */ 0xA0, 0x40, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, + /* B- */ 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0x40, 0x40, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, + /* C- */ 0x40, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x40, 0xCB, 0x40, 0xCD, 0x40, 0xCF, + /* D- */ 0x40, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, + /* E- */ 0x40, 0x40, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xEA, 0xEB, 0x40, 0xED, 0xEE, 0xEF, + /* F- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0xFB, 0xFC, 0xFD, 0xFE, 0x40, +} + +var ngrams_IBM420_ar_rtl = [64]uint32{ + 0x4056B1, 0x4056BD, 0x405856, 0x409AB1, 0x40ABDC, 0x40B1B1, 0x40BBBD, 0x40CF56, 0x564056, 0x564640, 0x566340, 0x567540, 0x56B140, 0x56B149, 0x56B156, 0x56B158, + 0x56B163, 0x56B167, 0x56B169, 0x56B173, 0x56B178, 0x56B19A, 0x56B1AD, 0x56B1BB, 0x56B1CF, 0x56B1DC, 0x56BB40, 0x56BD40, 0x56BD63, 0x584056, 0x624056, 0x6240AB, + 0x6240B1, 0x6240BB, 0x6240CF, 0x634056, 0x734056, 0x736240, 0x754056, 0x756240, 0x784056, 0x9A4056, 0x9AB1DA, 0xABDC40, 0xB14056, 0xB16240, 0xB1DA40, 0xB1DC40, + 0xBB4056, 0xBB5640, 0xBB6240, 0xBBBD40, 0xBD4056, 0xBF4056, 0xBF5640, 0xCF56B1, 0xCFBD40, 0xDA4056, 0xDC4056, 0xDC40BB, 0xDC40CF, 0xDC6240, 0xDC7540, 0xDCBD40, +} + +var ngrams_IBM420_ar_ltr = [64]uint32{ + 0x404656, 0x4056BB, 0x4056BF, 0x406273, 0x406275, 0x4062B1, 0x4062BB, 0x4062DC, 0x406356, 0x407556, 0x4075DC, 0x40B156, 0x40BB56, 0x40BD56, 0x40BDBB, 0x40BDCF, + 0x40BDDC, 0x40DAB1, 0x40DCAB, 0x40DCB1, 0x49B156, 0x564056, 0x564058, 0x564062, 0x564063, 0x564073, 0x564075, 0x564078, 0x56409A, 0x5640B1, 0x5640BB, 0x5640BD, + 0x5640BF, 0x5640DA, 0x5640DC, 0x565840, 0x56B156, 0x56CF40, 0x58B156, 0x63B156, 0x63BD56, 0x67B156, 0x69B156, 0x73B156, 0x78B156, 0x9AB156, 0xAB4062, 0xADB156, + 0xB14062, 0xB15640, 0xB156CF, 0xB19A40, 0xB1B140, 0xBB4062, 0xBB40DC, 0xBBB156, 0xBD5640, 0xBDBB40, 0xCF4062, 0xCF40DC, 0xCFB156, 0xDAB19A, 0xDCAB40, 0xDCB156, +} + +func newRecognizer_IBM420_ar(charset string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: charset, + language: "ar", + charMap: &charMap_IBM420_ar, + ngram: ngram, + } +} + +func newRecognizer_IBM420_ar_rtl() *recognizerSingleByte { + return newRecognizer_IBM420_ar("IBM420_rtl", &ngrams_IBM420_ar_rtl) +} + +func newRecognizer_IBM420_ar_ltr() *recognizerSingleByte { + return newRecognizer_IBM420_ar("IBM420_ltr", &ngrams_IBM420_ar_ltr) +} diff --git a/vendor/github.com/xWTF/chardet/unicode.go b/vendor/github.com/xWTF/chardet/unicode.go new file mode 100644 index 000000000..478d7cf38 --- /dev/null +++ b/vendor/github.com/xWTF/chardet/unicode.go @@ -0,0 +1,106 @@ +package chardet + +import ( + "bytes" +) + +var ( + utf16beBom = []byte{0xFE, 0xFF} + utf16leBom = []byte{0xFF, 0xFE} + utf32beBom = []byte{0x00, 0x00, 0xFE, 0xFF} + utf32leBom = []byte{0xFF, 0xFE, 0x00, 0x00} +) + +type recognizerUtf16be struct { +} + +func newRecognizer_utf16be() *recognizerUtf16be { + return &recognizerUtf16be{} +} + +func (*recognizerUtf16be) Match(input *recognizerInput, order int) (output recognizerOutput) { + output = recognizerOutput{ + Charset: "UTF-16BE", + order: order, + } + if bytes.HasPrefix(input.raw, utf16beBom) { + output.Confidence = 100 + } + return +} + +type recognizerUtf16le struct { +} + +func newRecognizer_utf16le() *recognizerUtf16le { + return &recognizerUtf16le{} +} + +func (*recognizerUtf16le) Match(input *recognizerInput, order int) (output recognizerOutput) { + output = recognizerOutput{ + Charset: "UTF-16LE", + order: order, + } + if bytes.HasPrefix(input.raw, utf16leBom) && !bytes.HasPrefix(input.raw, utf32leBom) { + output.Confidence = 100 + } + return +} + +type recognizerUtf32 struct { + name string + bom []byte + decodeChar func(input []byte) uint32 +} + +func decodeUtf32be(input []byte) uint32 { + return uint32(input[0])<<24 | uint32(input[1])<<16 | uint32(input[2])<<8 | uint32(input[3]) +} + +func decodeUtf32le(input []byte) uint32 { + return uint32(input[3])<<24 | uint32(input[2])<<16 | uint32(input[1])<<8 | uint32(input[0]) +} + +func newRecognizer_utf32be() *recognizerUtf32 { + return &recognizerUtf32{ + "UTF-32BE", + utf32beBom, + decodeUtf32be, + } +} + +func newRecognizer_utf32le() *recognizerUtf32 { + return &recognizerUtf32{ + "UTF-32LE", + utf32leBom, + decodeUtf32le, + } +} + +func (r *recognizerUtf32) Match(input *recognizerInput, order int) (output recognizerOutput) { + output = recognizerOutput{ + Charset: r.name, + order: order, + } + hasBom := bytes.HasPrefix(input.raw, r.bom) + var numValid, numInvalid uint32 + for b := input.raw; len(b) >= 4; b = b[4:] { + if c := r.decodeChar(b); c >= 0x10FFFF || (c >= 0xD800 && c <= 0xDFFF) { + numInvalid++ + } else { + numValid++ + } + } + if hasBom && numInvalid == 0 { + output.Confidence = 100 + } else if hasBom && numValid > numInvalid*10 { + output.Confidence = 80 + } else if numValid > 3 && numInvalid == 0 { + output.Confidence = 100 + } else if numValid > 0 && numInvalid == 0 { + output.Confidence = 80 + } else if numValid > numInvalid*10 { + output.Confidence = 25 + } + return +} diff --git a/vendor/github.com/xWTF/chardet/utf8.go b/vendor/github.com/xWTF/chardet/utf8.go new file mode 100644 index 000000000..db83f4f5c --- /dev/null +++ b/vendor/github.com/xWTF/chardet/utf8.go @@ -0,0 +1,72 @@ +package chardet + +import ( + "bytes" +) + +var utf8Bom = []byte{0xEF, 0xBB, 0xBF} + +type recognizerUtf8 struct { +} + +func newRecognizer_utf8() *recognizerUtf8 { + return &recognizerUtf8{} +} + +func (*recognizerUtf8) Match(input *recognizerInput, order int) (output recognizerOutput) { + output = recognizerOutput{ + Charset: "UTF-8", + order: order, + } + hasBom := bytes.HasPrefix(input.raw, utf8Bom) + inputLen := len(input.raw) + var numValid, numInvalid uint32 + var trailBytes uint8 + for i := 0; i < inputLen; i++ { + c := input.raw[i] + if c&0x80 == 0 { + continue + } + if c&0xE0 == 0xC0 { + trailBytes = 1 + } else if c&0xF0 == 0xE0 { + trailBytes = 2 + } else if c&0xF8 == 0xF0 { + trailBytes = 3 + } else { + numInvalid++ + if numInvalid > 5 { + break + } + trailBytes = 0 + } + + for i++; i < inputLen; i++ { + c = input.raw[i] + if c&0xC0 != 0x80 { + numInvalid++ + break + } + if trailBytes--; trailBytes == 0 { + numValid++ + break + } + } + } + + if hasBom && numInvalid == 0 { + output.Confidence = 100 + } else if hasBom && numValid > numInvalid*10 { + output.Confidence = 80 + } else if numValid > 3 && numInvalid == 0 { + output.Confidence = 100 + } else if numValid > 0 && numInvalid == 0 { + output.Confidence = 80 + } else if numValid == 0 && numInvalid == 0 { + // Plain ASCII + output.Confidence = 10 + } else if numValid > numInvalid*10 { + output.Confidence = 25 + } + return +} diff --git a/vendor/github.com/xrash/smetrics/.travis.yml b/vendor/github.com/xrash/smetrics/.travis.yml new file mode 100644 index 000000000..d1cd67ff9 --- /dev/null +++ b/vendor/github.com/xrash/smetrics/.travis.yml @@ -0,0 +1,9 @@ +language: go +go: + - 1.11 + - 1.12 + - 1.13 + - 1.14.x + - master +script: + - cd tests && make diff --git a/vendor/github.com/xrash/smetrics/LICENSE b/vendor/github.com/xrash/smetrics/LICENSE new file mode 100644 index 000000000..80445682f --- /dev/null +++ b/vendor/github.com/xrash/smetrics/LICENSE @@ -0,0 +1,21 @@ +Copyright (C) 2016 Felipe da Cunha Gonçalves +All Rights Reserved. + +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. diff --git a/vendor/github.com/xrash/smetrics/README.md b/vendor/github.com/xrash/smetrics/README.md new file mode 100644 index 000000000..5e0c1a463 --- /dev/null +++ b/vendor/github.com/xrash/smetrics/README.md @@ -0,0 +1,49 @@ +[![Build Status](https://travis-ci.org/xrash/smetrics.svg?branch=master)](http://travis-ci.org/xrash/smetrics) + +# smetrics + +`smetrics` is "string metrics". + +Package smetrics provides a bunch of algorithms for calculating the distance between strings. + +There are implementations for calculating the popular Levenshtein distance (aka Edit Distance or Wagner-Fischer), as well as the Jaro distance, the Jaro-Winkler distance, and more. + +# How to import + +```go +import "github.com/xrash/smetrics" +``` + +# Documentation + +Go to [https://pkg.go.dev/github.com/xrash/smetrics](https://pkg.go.dev/github.com/xrash/smetrics) for complete documentation. + +# Example + +```go +package main + +import ( + "github.com/xrash/smetrics" +) + +func main() { + smetrics.WagnerFischer("POTATO", "POTATTO", 1, 1, 2) + smetrics.WagnerFischer("MOUSE", "HOUSE", 2, 2, 4) + + smetrics.Ukkonen("POTATO", "POTATTO", 1, 1, 2) + smetrics.Ukkonen("MOUSE", "HOUSE", 2, 2, 4) + + smetrics.Jaro("AL", "AL") + smetrics.Jaro("MARTHA", "MARHTA") + + smetrics.JaroWinkler("AL", "AL", 0.7, 4) + smetrics.JaroWinkler("MARTHA", "MARHTA", 0.7, 4) + + smetrics.Soundex("Euler") + smetrics.Soundex("Ellery") + + smetrics.Hamming("aaa", "aaa") + smetrics.Hamming("aaa", "aab") +} +``` diff --git a/vendor/github.com/xrash/smetrics/doc.go b/vendor/github.com/xrash/smetrics/doc.go new file mode 100644 index 000000000..21bc986c9 --- /dev/null +++ b/vendor/github.com/xrash/smetrics/doc.go @@ -0,0 +1,19 @@ +/* +Package smetrics provides a bunch of algorithms for calculating +the distance between strings. + +There are implementations for calculating the popular Levenshtein +distance (aka Edit Distance or Wagner-Fischer), as well as the Jaro +distance, the Jaro-Winkler distance, and more. + +For the Levenshtein distance, you can use the functions WagnerFischer() +and Ukkonen(). Read the documentation on these functions. + +For the Jaro and Jaro-Winkler algorithms, check the functions +Jaro() and JaroWinkler(). Read the documentation on these functions. + +For the Soundex algorithm, check the function Soundex(). + +For the Hamming distance algorithm, check the function Hamming(). +*/ +package smetrics diff --git a/vendor/github.com/xrash/smetrics/hamming.go b/vendor/github.com/xrash/smetrics/hamming.go new file mode 100644 index 000000000..505d3e5da --- /dev/null +++ b/vendor/github.com/xrash/smetrics/hamming.go @@ -0,0 +1,25 @@ +package smetrics + +import ( + "fmt" +) + +// The Hamming distance is the minimum number of substitutions required to change string A into string B. Both strings must have the same size. If the strings have different sizes, the function returns an error. +func Hamming(a, b string) (int, error) { + al := len(a) + bl := len(b) + + if al != bl { + return -1, fmt.Errorf("strings are not equal (len(a)=%d, len(b)=%d)", al, bl) + } + + var difference = 0 + + for i := range a { + if a[i] != b[i] { + difference = difference + 1 + } + } + + return difference, nil +} diff --git a/vendor/github.com/xrash/smetrics/jaro-winkler.go b/vendor/github.com/xrash/smetrics/jaro-winkler.go new file mode 100644 index 000000000..abdb28883 --- /dev/null +++ b/vendor/github.com/xrash/smetrics/jaro-winkler.go @@ -0,0 +1,28 @@ +package smetrics + +import ( + "math" +) + +// The Jaro-Winkler distance. The result is 1 for equal strings, and 0 for completely different strings. It is commonly used on Record Linkage stuff, thus it tries to be accurate for common typos when writing real names such as person names and street names. +// Jaro-Winkler is a modification of the Jaro algorithm. It works by first running Jaro, then boosting the score of exact matches at the beginning of the strings. Because of that, it introduces two more parameters: the boostThreshold and the prefixSize. These are commonly set to 0.7 and 4, respectively. +func JaroWinkler(a, b string, boostThreshold float64, prefixSize int) float64 { + j := Jaro(a, b) + + if j <= boostThreshold { + return j + } + + prefixSize = int(math.Min(float64(len(a)), math.Min(float64(prefixSize), float64(len(b))))) + + var prefixMatch float64 + for i := 0; i < prefixSize; i++ { + if a[i] == b[i] { + prefixMatch++ + } else { + break + } + } + + return j + 0.1*prefixMatch*(1.0-j) +} diff --git a/vendor/github.com/xrash/smetrics/jaro.go b/vendor/github.com/xrash/smetrics/jaro.go new file mode 100644 index 000000000..75f924e11 --- /dev/null +++ b/vendor/github.com/xrash/smetrics/jaro.go @@ -0,0 +1,86 @@ +package smetrics + +import ( + "math" +) + +// The Jaro distance. The result is 1 for equal strings, and 0 for completely different strings. +func Jaro(a, b string) float64 { + // If both strings are zero-length, they are completely equal, + // therefore return 1. + if len(a) == 0 && len(b) == 0 { + return 1 + } + + // If one string is zero-length, strings are completely different, + // therefore return 0. + if len(a) == 0 || len(b) == 0 { + return 0 + } + + // Define the necessary variables for the algorithm. + la := float64(len(a)) + lb := float64(len(b)) + matchRange := int(math.Max(0, math.Floor(math.Max(la, lb)/2.0)-1)) + matchesA := make([]bool, len(a)) + matchesB := make([]bool, len(b)) + var matches float64 = 0 + + // Step 1: Matches + // Loop through each character of the first string, + // looking for a matching character in the second string. + for i := 0; i < len(a); i++ { + start := int(math.Max(0, float64(i-matchRange))) + end := int(math.Min(lb-1, float64(i+matchRange))) + + for j := start; j <= end; j++ { + if matchesB[j] { + continue + } + + if a[i] == b[j] { + matchesA[i] = true + matchesB[j] = true + matches++ + break + } + } + } + + // If there are no matches, strings are completely different, + // therefore return 0. + if matches == 0 { + return 0 + } + + // Step 2: Transpositions + // Loop through the matches' arrays, looking for + // unaligned matches. Count the number of unaligned matches. + unaligned := 0 + j := 0 + for i := 0; i < len(a); i++ { + if !matchesA[i] { + continue + } + + for !matchesB[j] { + j++ + } + + if a[i] != b[j] { + unaligned++ + } + + j++ + } + + // The number of unaligned matches divided by two, is the number of _transpositions_. + transpositions := math.Floor(float64(unaligned / 2)) + + // Jaro distance is the average between these three numbers: + // 1. matches / length of string A + // 2. matches / length of string B + // 3. (matches - transpositions/matches) + // So, all that divided by three is the final result. + return ((matches / la) + (matches / lb) + ((matches - transpositions) / matches)) / 3.0 +} diff --git a/vendor/github.com/xrash/smetrics/soundex.go b/vendor/github.com/xrash/smetrics/soundex.go new file mode 100644 index 000000000..a2ad034d5 --- /dev/null +++ b/vendor/github.com/xrash/smetrics/soundex.go @@ -0,0 +1,41 @@ +package smetrics + +import ( + "strings" +) + +// The Soundex encoding. It is a phonetic algorithm that considers how the words sound in English. Soundex maps a string to a 4-byte code consisting of the first letter of the original string and three numbers. Strings that sound similar should map to the same code. +func Soundex(s string) string { + m := map[byte]string{ + 'B': "1", 'P': "1", 'F': "1", 'V': "1", + 'C': "2", 'S': "2", 'K': "2", 'G': "2", 'J': "2", 'Q': "2", 'X': "2", 'Z': "2", + 'D': "3", 'T': "3", + 'L': "4", + 'M': "5", 'N': "5", + 'R': "6", + } + + s = strings.ToUpper(s) + + r := string(s[0]) + p := s[0] + for i := 1; i < len(s) && len(r) < 4; i++ { + c := s[i] + + if (c < 'A' || c > 'Z') || (c == p) { + continue + } + + p = c + + if n, ok := m[c]; ok { + r += n + } + } + + for i := len(r); i < 4; i++ { + r += "0" + } + + return r +} diff --git a/vendor/github.com/xrash/smetrics/ukkonen.go b/vendor/github.com/xrash/smetrics/ukkonen.go new file mode 100644 index 000000000..3c5579cd9 --- /dev/null +++ b/vendor/github.com/xrash/smetrics/ukkonen.go @@ -0,0 +1,94 @@ +package smetrics + +import ( + "math" +) + +// The Ukkonen algorithm for calculating the Levenshtein distance. The algorithm is described in http://www.cs.helsinki.fi/u/ukkonen/InfCont85.PDF, or in docs/InfCont85.PDF. It runs on O(t . min(m, n)) where t is the actual distance between strings a and b. It needs O(min(t, m, n)) space. This function might be preferred over WagnerFischer() for *very* similar strings. But test it out yourself. +// The first two parameters are the two strings to be compared. The last three parameters are the insertion cost, the deletion cost and the substitution cost. These are normally defined as 1, 1 and 2 respectively. +func Ukkonen(a, b string, icost, dcost, scost int) int { + var lowerCost int + + if icost < dcost && icost < scost { + lowerCost = icost + } else if dcost < scost { + lowerCost = dcost + } else { + lowerCost = scost + } + + infinite := math.MaxInt32 / 2 + + var r []int + var k, kprime, p, t int + var ins, del, sub int + + if len(a) > len(b) { + t = (len(a) - len(b) + 1) * lowerCost + } else { + t = (len(b) - len(a) + 1) * lowerCost + } + + for { + if (t / lowerCost) < (len(b) - len(a)) { + continue + } + + // This is the right damn thing since the original Ukkonen + // paper minimizes the expression result only, but the uncommented version + // doesn't need to deal with floats so it's faster. + // p = int(math.Floor(0.5*((float64(t)/float64(lowerCost)) - float64(len(b) - len(a))))) + p = ((t / lowerCost) - (len(b) - len(a))) / 2 + + k = -p + kprime = k + + rowlength := (len(b) - len(a)) + (2 * p) + + r = make([]int, rowlength+2) + + for i := 0; i < rowlength+2; i++ { + r[i] = infinite + } + + for i := 0; i <= len(a); i++ { + for j := 0; j <= rowlength; j++ { + if i == j+k && i == 0 { + r[j] = 0 + } else { + if j-1 < 0 { + ins = infinite + } else { + ins = r[j-1] + icost + } + + del = r[j+1] + dcost + sub = r[j] + scost + + if i-1 < 0 || i-1 >= len(a) || j+k-1 >= len(b) || j+k-1 < 0 { + sub = infinite + } else if a[i-1] == b[j+k-1] { + sub = r[j] + } + + if ins < del && ins < sub { + r[j] = ins + } else if del < sub { + r[j] = del + } else { + r[j] = sub + } + } + } + k++ + } + + if r[(len(b)-len(a))+(2*p)+kprime] <= t { + break + } else { + t *= 2 + } + } + + return r[(len(b)-len(a))+(2*p)+kprime] +} diff --git a/vendor/github.com/xrash/smetrics/wagner-fischer.go b/vendor/github.com/xrash/smetrics/wagner-fischer.go new file mode 100644 index 000000000..9883aea04 --- /dev/null +++ b/vendor/github.com/xrash/smetrics/wagner-fischer.go @@ -0,0 +1,48 @@ +package smetrics + +// The Wagner-Fischer algorithm for calculating the Levenshtein distance. +// The first two parameters are the two strings to be compared. The last three parameters are the insertion cost, the deletion cost and the substitution cost. These are normally defined as 1, 1 and 2 respectively. +func WagnerFischer(a, b string, icost, dcost, scost int) int { + + // Allocate both rows. + row1 := make([]int, len(b)+1) + row2 := make([]int, len(b)+1) + var tmp []int + + // Initialize the first row. + for i := 1; i <= len(b); i++ { + row1[i] = i * icost + } + + // For each row... + for i := 1; i <= len(a); i++ { + row2[0] = i * dcost + + // For each column... + for j := 1; j <= len(b); j++ { + if a[i-1] == b[j-1] { + row2[j] = row1[j-1] + } else { + ins := row2[j-1] + icost + del := row1[j] + dcost + sub := row1[j-1] + scost + + if ins < del && ins < sub { + row2[j] = ins + } else if del < sub { + row2[j] = del + } else { + row2[j] = sub + } + } + } + + // Swap the rows at the end of each row. + tmp = row1 + row1 = row2 + row2 = tmp + } + + // Because we swapped the rows, the final result is in row1 instead of row2. + return row1[len(row1)-1] +} diff --git a/vendor/github.com/zencoder/go-dash/v3/LICENSE b/vendor/github.com/zencoder/go-dash/v3/LICENSE new file mode 100644 index 000000000..d9569559f --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/LICENSE @@ -0,0 +1,13 @@ + Copyright Brightcove, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/zencoder/go-dash/v3/helpers/ptrs/ptrs.go b/vendor/github.com/zencoder/go-dash/v3/helpers/ptrs/ptrs.go new file mode 100644 index 000000000..9072660cf --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/helpers/ptrs/ptrs.go @@ -0,0 +1,49 @@ +package ptrs + +func Strptr(v string) *string { + p := new(string) + *p = v + return p +} + +func Intptr(v int) *int { + p := new(int) + *p = v + return p +} + +func Int64ptr(v int64) *int64 { + p := new(int64) + *p = v + return p +} + +func Uintptr(v uint) *uint { + p := new(uint) + *p = v + return p +} + +func Uint32ptr(v uint32) *uint32 { + p := new(uint32) + *p = v + return p +} + +func Uint64ptr(v uint64) *uint64 { + p := new(uint64) + *p = v + return p +} + +func Boolptr(v bool) *bool { + p := new(bool) + *p = v + return p +} + +func Float64ptr(v float64) *float64 { + p := new(float64) + *p = v + return p +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/duration.go b/vendor/github.com/zencoder/go-dash/v3/mpd/duration.go new file mode 100644 index 000000000..8ebc87021 --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/duration.go @@ -0,0 +1,206 @@ +// based on code from golang src/time/time.go + +package mpd + +import ( + "encoding/xml" + "errors" + "fmt" + "regexp" + "strconv" + "strings" + "time" +) + +type Duration time.Duration + +var ( + rStart = "^P" // Must start with a 'P' + rDays = "(\\d+D)?" // We only allow Days for durations, not Months or Years + rTime = "(?:T" // If there's any 'time' units then they must be preceded by a 'T' + rHours = "(\\d+H)?" // Hours + rMinutes = "(\\d+M)?" // Minutes + rSeconds = "([\\d.]+S)?" // Seconds (Potentially decimal) + rEnd = ")?$" // end of regex must close "T" capture group +) + +var xmlDurationRegex = regexp.MustCompile(rStart + rDays + rTime + rHours + rMinutes + rSeconds + rEnd) + +func (d Duration) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { + return xml.Attr{Name: name, Value: d.String()}, nil +} + +func (d *Duration) UnmarshalXMLAttr(attr xml.Attr) error { + dur, err := ParseDuration(attr.Value) + if err != nil { + return err + } + *d = Duration(dur) + return nil +} + +// String renders a Duration in XML Duration Data Type format +func (d *Duration) String() string { + // Largest time is 2540400h10m10.000000000s + var buf [32]byte + w := len(buf) + + u := uint64(*d) + neg := *d < 0 + if neg { + u = -u + } + + if u < uint64(time.Second) { + // Special case: if duration is smaller than a second, + // use smaller units, like 1.2ms + var prec int + w-- + buf[w] = 'S' + w-- + if u == 0 { + return "PT0S" + } + /* + switch { + case u < uint64(Millisecond): + // print microseconds + prec = 3 + // U+00B5 'µ' micro sign == 0xC2 0xB5 + w-- // Need room for two bytes. + copy(buf[w:], "µ") + default: + // print milliseconds + prec = 6 + buf[w] = 'm' + } + */ + w, u = fmtFrac(buf[:w], u, prec) + w = fmtInt(buf[:w], u) + } else { + w-- + buf[w] = 'S' + + w, u = fmtFrac(buf[:w], u, 9) + + // u is now integer seconds + w = fmtInt(buf[:w], u%60) + u /= 60 + + // u is now integer minutes + if u > 0 { + w-- + buf[w] = 'M' + w = fmtInt(buf[:w], u%60) + u /= 60 + + // u is now integer hours + // Stop at hours because days can be different lengths. + if u > 0 { + w-- + buf[w] = 'H' + w = fmtInt(buf[:w], u) + } + } + } + + if neg { + w-- + buf[w] = '-' + } + + return "PT" + string(buf[w:]) +} + +// fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the +// tail of buf, omitting trailing zeros. it omits the decimal +// point too when the fraction is 0. It returns the index where the +// output bytes begin and the value v/10**prec. +func fmtFrac(buf []byte, v uint64, prec int) (nw int, nv uint64) { + // Omit trailing zeros up to and including decimal point. + w := len(buf) + print := false + for i := 0; i < prec; i++ { + digit := v % 10 + print = print || digit != 0 + if print { + w-- + buf[w] = byte(digit) + '0' + } + v /= 10 + } + if print { + w-- + buf[w] = '.' + } + return w, v +} + +// fmtInt formats v into the tail of buf. +// It returns the index where the output begins. +func fmtInt(buf []byte, v uint64) int { + w := len(buf) + if v == 0 { + w-- + buf[w] = '0' + } else { + for v > 0 { + w-- + buf[w] = byte(v%10) + '0' + v /= 10 + } + } + return w +} + +func ParseDuration(str string) (time.Duration, error) { + if len(str) < 3 { + return 0, errors.New("At least one number and designator are required") + } + + if strings.Contains(str, "-") { + return 0, errors.New("Duration cannot be negative") + } + + // Check that only the parts we expect exist and that everything's in the correct order + if !xmlDurationRegex.Match([]byte(str)) { + return 0, errors.New("Duration must be in the format: P[nD][T[nH][nM][nS]]") + } + + var parts = xmlDurationRegex.FindStringSubmatch(str) + var total time.Duration + + if parts[1] != "" { + days, err := strconv.Atoi(strings.TrimRight(parts[1], "D")) + if err != nil { + return 0, fmt.Errorf("Error parsing Days: %s", err) + } + total += time.Duration(days) * time.Hour * 24 + } + + if parts[2] != "" { + hours, err := strconv.Atoi(strings.TrimRight(parts[2], "H")) + if err != nil { + return 0, fmt.Errorf("Error parsing Hours: %s", err) + } + total += time.Duration(hours) * time.Hour + } + + if parts[3] != "" { + mins, err := strconv.Atoi(strings.TrimRight(parts[3], "M")) + if err != nil { + return 0, fmt.Errorf("Error parsing Minutes: %s", err) + } + total += time.Duration(mins) * time.Minute + } + + if parts[4] != "" { + secs, err := strconv.ParseFloat(strings.TrimRight(parts[4], "S"), 64) + if err != nil { + return 0, fmt.Errorf("Error parsing Seconds: %s", err) + } + total += time.Duration(secs * float64(time.Second)) + } + + return total, nil +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/events.go b/vendor/github.com/zencoder/go-dash/v3/mpd/events.go new file mode 100644 index 000000000..096291876 --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/events.go @@ -0,0 +1,18 @@ +package mpd + +import "encoding/xml" + +type EventStream struct { + XMLName xml.Name `xml:"EventStream"` + SchemeIDURI *string `xml:"schemeIdUri,attr"` + Value *string `xml:"value,attr,omitempty"` + Timescale *uint `xml:"timescale,attr"` + Events []Event `xml:"Event,omitempty"` +} + +type Event struct { + XMLName xml.Name `xml:"Event"` + ID *string `xml:"id,attr,omitempty"` + PresentationTime *uint64 `xml:"presentationTime,attr,omitempty"` + Duration *uint64 `xml:"duration,attr,omitempty"` +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/mpd.go b/vendor/github.com/zencoder/go-dash/v3/mpd/mpd.go new file mode 100644 index 000000000..57e718b4a --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/mpd.go @@ -0,0 +1,1168 @@ +package mpd + +import ( + "encoding/base64" + "encoding/hex" + "encoding/xml" + "errors" + "strings" + "time" + + . "github.com/zencoder/go-dash/v3/helpers/ptrs" +) + +// Type definition for DASH profiles +type DashProfile string + +// Constants for supported DASH profiles +const ( + // Live Profile + DASH_PROFILE_LIVE DashProfile = "urn:mpeg:dash:profile:isoff-live:2011" + // On Demand Profile + DASH_PROFILE_ONDEMAND DashProfile = "urn:mpeg:dash:profile:isoff-on-demand:2011" + // HbbTV Profile + DASH_PROFILE_HBBTV_1_5_LIVE DashProfile = "urn:hbbtv:dash:profile:isoff-live:2012,urn:mpeg:dash:profile:isoff-live:2011" +) + +type AudioChannelConfigurationScheme string + +const ( + // Scheme for non-Dolby Audio + AUDIO_CHANNEL_CONFIGURATION_MPEG_DASH AudioChannelConfigurationScheme = "urn:mpeg:dash:23003:3:audio_channel_configuration:2011" + // Scheme for Dolby Audio + AUDIO_CHANNEL_CONFIGURATION_MPEG_DOLBY AudioChannelConfigurationScheme = "tag:dolby.com,2014:dash:audio_channel_configuration:2011" +) + +// AccessibilityElementScheme is the scheme definition for an Accessibility element +type AccessibilityElementScheme string + +// Accessibility descriptor values for Audio Description +const ACCESSIBILITY_ELEMENT_SCHEME_DESCRIPTIVE_AUDIO AccessibilityElementScheme = "urn:tva:metadata:cs:AudioPurposeCS:2007" + +// Constants for some known MIME types, this is a limited list and others can be used. +const ( + DASH_MIME_TYPE_VIDEO_MP4 string = "video/mp4" + DASH_MIME_TYPE_AUDIO_MP4 string = "audio/mp4" + DASH_MIME_TYPE_SUBTITLE_VTT string = "text/vtt" + DASH_MIME_TYPE_SUBTITLE_TTML string = "application/ttaf+xml" + DASH_MIME_TYPE_SUBTITLE_SRT string = "application/x-subrip" + DASH_MIME_TYPE_SUBTITLE_DFXP string = "application/ttaf+xml" + DASH_MIME_TYPE_IMAGE_JPEG string = "image/jpeg" + DASH_CONTENT_TYPE_IMAGE string = "image" +) + +// Known error variables +var ( + ErrNoDASHProfileSet error = errors.New("No DASH profile set") + ErrAdaptationSetNil = errors.New("Adaptation Set nil") + ErrSegmentTemplateLiveProfileOnly = errors.New("Segment template can only be used with Live Profile") + ErrSegmentTemplateNil = errors.New("Segment Template nil ") + ErrRepresentationNil = errors.New("Representation nil") + ErrAccessibilityNil = errors.New("Accessibility nil") + ErrBaseURLEmpty = errors.New("Base URL empty") + ErrSegmentBaseOnDemandProfileOnly = errors.New("Segment Base can only be used with On-Demand Profile") + ErrSegmentBaseNil = errors.New("Segment Base nil") + ErrAudioChannelConfigurationNil = errors.New("Audio Channel Configuration nil") + ErrInvalidDefaultKID = errors.New("Invalid Default KID string, should be 32 characters") + ErrPROEmpty = errors.New("PlayReady PRO empty") + ErrContentProtectionNil = errors.New("Content Protection nil") +) + +type MPD struct { + XMLNs *string `xml:"xmlns,attr"` + Profiles *string `xml:"profiles,attr"` + Type *string `xml:"type,attr"` + MediaPresentationDuration *string `xml:"mediaPresentationDuration,attr"` + MinBufferTime *string `xml:"minBufferTime,attr"` + AvailabilityStartTime *string `xml:"availabilityStartTime,attr,omitempty"` + MinimumUpdatePeriod *string `xml:"minimumUpdatePeriod,attr"` + PublishTime *string `xml:"publishTime,attr"` + TimeShiftBufferDepth *string `xml:"timeShiftBufferDepth,attr"` + SuggestedPresentationDelay *Duration `xml:"suggestedPresentationDelay,attr,omitempty"` + BaseURL string `xml:"BaseURL,omitempty"` + Location string `xml:"Location,omitempty"` + period *Period + Periods []*Period `xml:"Period,omitempty"` + UTCTiming *DescriptorType `xml:"UTCTiming,omitempty"` +} + +type Period struct { + ID string `xml:"id,attr,omitempty"` + Duration Duration `xml:"duration,attr,omitempty"` + Start *Duration `xml:"start,attr,omitempty"` + BaseURL string `xml:"BaseURL,omitempty"` + SegmentBase *SegmentBase `xml:"SegmentBase,omitempty"` + SegmentList *SegmentList `xml:"SegmentList,omitempty"` + SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate,omitempty"` + AdaptationSets []*AdaptationSet `xml:"AdaptationSet,omitempty"` + EventStreams []EventStream `xml:"EventStream,omitempty"` +} + +type DescriptorType struct { + SchemeIDURI *string `xml:"schemeIdUri,attr"` + Value *string `xml:"value,attr"` + ID *string `xml:"id,attr"` +} + +// ISO 23009-1-2014 5.3.7 +type CommonAttributesAndElements struct { + Profiles *string `xml:"profiles,attr"` + Width *string `xml:"width,attr"` + Height *string `xml:"height,attr"` + Sar *string `xml:"sar,attr"` + FrameRate *string `xml:"frameRate,attr"` + AudioSamplingRate *string `xml:"audioSamplingRate,attr"` + MimeType *string `xml:"mimeType,attr"` + SegmentProfiles *string `xml:"segmentProfiles,attr"` + Codecs *string `xml:"codecs,attr"` + MaximumSAPPeriod *string `xml:"maximumSAPPeriod,attr"` + StartWithSAP *int64 `xml:"startWithSAP,attr"` + MaxPlayoutRate *string `xml:"maxPlayoutRate,attr"` + ScanType *string `xml:"scanType,attr"` + FramePacking []DescriptorType `xml:"FramePacking,omitempty"` + AudioChannelConfiguration []DescriptorType `xml:"AudioChannelConfiguration,omitempty"` + ContentProtection []ContentProtectioner `xml:"ContentProtection,omitempty"` + EssentialProperty []DescriptorType `xml:"EssentialProperty,omitempty"` + SupplementalProperty []DescriptorType `xml:"SupplementalProperty,omitempty"` + InbandEventStream *DescriptorType `xml:"inbandEventStream,attr"` +} + +type contentProtections []ContentProtectioner + +func (as *contentProtections) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var scheme string + for _, a := range start.Attr { + if a.Name.Local == "schemeIdUri" { + scheme = a.Value + break + } + } + var target ContentProtectioner + switch scheme { + case CONTENT_PROTECTION_ROOT_SCHEME_ID_URI: + target = &CENCContentProtection{} + case CONTENT_PROTECTION_PLAYREADY_SCHEME_ID: + target = &PlayreadyContentProtection{} + case CONTENT_PROTECTION_WIDEVINE_SCHEME_ID: + target = &WidevineContentProtection{} + default: + target = &ContentProtection{} + } + if err := d.DecodeElement(target, &start); err != nil { + return err + } + *as = append(*as, target) + return nil +} + +// wrappedAdaptationSet provides the default xml unmarshal +// to take care of the majority of our unmarshalling +type wrappedAdaptationSet AdaptationSet + +// dtoAdaptationSet parses the items out of AdaptationSet +// that give us trouble: +// * Content Protection interface +type dtoAdaptationSet struct { + wrappedAdaptationSet + ContentProtection contentProtections `xml:"ContentProtection,omitempty"` +} + +type AdaptationSet struct { + CommonAttributesAndElements + XMLName xml.Name `xml:"AdaptationSet"` + ID *string `xml:"id,attr"` + SegmentAlignment *bool `xml:"segmentAlignment,attr"` + Lang *string `xml:"lang,attr"` + Group *string `xml:"group,attr"` + PAR *string `xml:"par,attr"` + MinBandwidth *string `xml:"minBandwidth,attr"` + MaxBandwidth *string `xml:"maxBandwidth,attr"` + MinWidth *string `xml:"minWidth,attr"` + MaxWidth *string `xml:"maxWidth,attr"` + MinHeight *string `xml:"minHeight,attr"` + MaxHeight *string `xml:"maxHeight,attr"` + ContentType *string `xml:"contentType,attr"` + Roles []*Role `xml:"Role,omitempty"` + SegmentBase *SegmentBase `xml:"SegmentBase,omitempty"` + SegmentList *SegmentList `xml:"SegmentList,omitempty"` + SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate,omitempty"` // Live Profile Only + Representations []*Representation `xml:"Representation,omitempty"` + AccessibilityElems []*Accessibility `xml:"Accessibility,omitempty"` +} + +func (as *AdaptationSet) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var n dtoAdaptationSet + if err := d.DecodeElement(&n, &start); err != nil { + return err + } + *as = AdaptationSet(n.wrappedAdaptationSet) + as.ContentProtection = make([]ContentProtectioner, len(n.ContentProtection)) + for i := range n.ContentProtection { + as.ContentProtection[i] = n.ContentProtection[i] + } + return nil +} + +// Constants for DRM / ContentProtection +const ( + CONTENT_PROTECTION_ROOT_SCHEME_ID_URI = "urn:mpeg:dash:mp4protection:2011" + CONTENT_PROTECTION_ROOT_VALUE = "cenc" + CENC_XMLNS = "urn:mpeg:cenc:2013" + CONTENT_PROTECTION_WIDEVINE_SCHEME_ID = "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed" + CONTENT_PROTECTION_WIDEVINE_SCHEME_HEX = "edef8ba979d64acea3c827dcd51d21ed" + CONTENT_PROTECTION_PLAYREADY_SCHEME_ID = "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95" + CONTENT_PROTECTION_PLAYREADY_SCHEME_HEX = "9a04f07998404286ab92e65be0885f95" + CONTENT_PROTECTION_PLAYREADY_SCHEME_V10_ID = "urn:uuid:79f0049a-4098-8642-ab92-e65be0885f95" + CONTENT_PROTECTION_PLAYREADY_SCHEME_V10_HEX = "79f0049a40988642ab92e65be0885f95" + CONTENT_PROTECTION_PLAYREADY_XMLNS = "urn:microsoft:playready" +) + +type ContentProtectioner interface { + ContentProtected() +} + +type ContentProtection struct { + AdaptationSet *AdaptationSet `xml:"-"` + XMLName xml.Name `xml:"ContentProtection"` + SchemeIDURI *string `xml:"schemeIdUri,attr"` // Default: urn:mpeg:dash:mp4protection:2011 + XMLNS *string `xml:"cenc,attr"` // Default: urn:mpeg:cenc:2013 + Attrs []*xml.Attr `xml:",any,attr"` +} + +type CENCContentProtection struct { + ContentProtection + DefaultKID *string `xml:"default_KID,attr"` + Value *string `xml:"value,attr"` // Default: cenc +} + +type PlayreadyContentProtection struct { + ContentProtection + PlayreadyXMLNS *string `xml:"mspr,attr,omitempty"` + PRO *string `xml:"pro,omitempty"` + PSSH *string `xml:"pssh,omitempty"` +} + +type WidevineContentProtection struct { + ContentProtection + PSSH *string `xml:"pssh,omitempty"` +} + +type ContentProtectionMarshal struct { + AdaptationSet *AdaptationSet `xml:"-"` + XMLName xml.Name `xml:"ContentProtection"` + SchemeIDURI *string `xml:"schemeIdUri,attr"` // Default: urn:mpeg:dash:mp4protection:2011 + XMLNS *string `xml:"xmlns:cenc,attr"` // Default: urn:mpeg:cenc:2013 + Attrs []*xml.Attr `xml:",any,attr"` +} + +type CENCContentProtectionMarshal struct { + ContentProtectionMarshal + DefaultKID *string `xml:"cenc:default_KID,attr"` + Value *string `xml:"value,attr"` // Default: cenc +} + +type PlayreadyContentProtectionMarshal struct { + ContentProtectionMarshal + PlayreadyXMLNS *string `xml:"xmlns:mspr,attr,omitempty"` + PRO *string `xml:"mspr:pro,omitempty"` + PSSH *string `xml:"cenc:pssh,omitempty"` +} + +type WidevineContentProtectionMarshal struct { + ContentProtectionMarshal + PSSH *string `xml:"cenc:pssh,omitempty"` +} + +func (s ContentProtection) ContentProtected() {} + +func (s ContentProtection) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + err := e.Encode(&ContentProtectionMarshal{ + s.AdaptationSet, + s.XMLName, + s.SchemeIDURI, + s.XMLNS, + s.Attrs, + }) + if err != nil { + return err + } + return nil +} + +func (s CENCContentProtection) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + err := e.Encode(&CENCContentProtectionMarshal{ + ContentProtectionMarshal{ + s.AdaptationSet, + s.XMLName, + s.SchemeIDURI, + s.XMLNS, + s.Attrs, + }, + s.DefaultKID, + s.Value, + }) + if err != nil { + return err + } + return nil +} + +func (s PlayreadyContentProtection) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + err := e.Encode(&PlayreadyContentProtectionMarshal{ + ContentProtectionMarshal{ + s.AdaptationSet, + s.XMLName, + s.SchemeIDURI, + s.XMLNS, + s.Attrs, + }, + s.PlayreadyXMLNS, + s.PRO, + s.PSSH, + }) + if err != nil { + return err + } + return nil +} + +func (s WidevineContentProtection) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + err := e.Encode(&WidevineContentProtectionMarshal{ + ContentProtectionMarshal{ + s.AdaptationSet, + s.XMLName, + s.SchemeIDURI, + s.XMLNS, + s.Attrs, + }, + s.PSSH, + }) + if err != nil { + return err + } + return nil +} + +type Role struct { + AdaptationSet *AdaptationSet `xml:"-"` + SchemeIDURI *string `xml:"schemeIdUri,attr"` + Value *string `xml:"value,attr"` +} + +// Segment Template is for Live Profile Only +type SegmentTemplate struct { + AdaptationSet *AdaptationSet `xml:"-"` + SegmentTimeline *SegmentTimeline `xml:"SegmentTimeline,omitempty"` + PresentationTimeOffset *uint64 `xml:"presentationTimeOffset,attr,omitempty"` + Duration *int64 `xml:"duration,attr"` + Initialization *string `xml:"initialization,attr"` + Media *string `xml:"media,attr"` + StartNumber *int64 `xml:"startNumber,attr"` + Timescale *int64 `xml:"timescale,attr"` +} + +type Representation struct { + CommonAttributesAndElements + AdaptationSet *AdaptationSet `xml:"-"` + AudioChannelConfiguration *AudioChannelConfiguration `xml:"AudioChannelConfiguration,omitempty"` + AudioSamplingRate *int64 `xml:"audioSamplingRate,attr"` // Audio + Bandwidth *int64 `xml:"bandwidth,attr"` // Audio + Video + Codecs *string `xml:"codecs,attr"` // Audio + Video + FrameRate *string `xml:"frameRate,attr,omitempty"` // Video + Height *int64 `xml:"height,attr"` // Video + ID *string `xml:"id,attr"` // Audio + Video + Width *int64 `xml:"width,attr"` // Video + BaseURL *string `xml:"BaseURL,omitempty"` // On-Demand Profile + SegmentBase *SegmentBase `xml:"SegmentBase,omitempty"` // On-Demand Profile + SegmentList *SegmentList `xml:"SegmentList,omitempty"` + SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate,omitempty"` +} + +type Accessibility struct { + AdaptationSet *AdaptationSet `xml:"-"` + SchemeIdUri *string `xml:"schemeIdUri,attr,omitempty"` + Value *string `xml:"value,attr,omitempty"` +} + +type AudioChannelConfiguration struct { + SchemeIDURI *string `xml:"schemeIdUri,attr"` + // Value will be an int for non-Dolby Schemes, and a hexstring for Dolby Schemes, hence we make it a string + Value *string `xml:"value,attr"` +} + +// Creates a new static MPD object. +// profile - DASH Profile (Live or OnDemand). +// mediaPresentationDuration - Media Presentation Duration (i.e. PT6M16S). +// minBufferTime - Min Buffer Time (i.e. PT1.97S). +// attributes - Other attributes (optional). +func NewMPD(profile DashProfile, mediaPresentationDuration, minBufferTime string, attributes ...AttrMPD) *MPD { + period := &Period{} + mpd := &MPD{ + XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"), + Profiles: Strptr((string)(profile)), + Type: Strptr("static"), + MediaPresentationDuration: Strptr(mediaPresentationDuration), + MinBufferTime: Strptr(minBufferTime), + period: period, + Periods: []*Period{period}, + } + + for i := range attributes { + switch attr := attributes[i].(type) { + case *attrAvailabilityStartTime: + mpd.AvailabilityStartTime = attr.GetStrptr() + } + } + + return mpd +} + +// Creates a new dynamic MPD object. +// profile - DASH Profile (Live or OnDemand). +// availabilityStartTime - anchor for the computation of the earliest availability time (in UTC). +// minBufferTime - Min Buffer Time (i.e. PT1.97S). +// attributes - Other attributes (optional). +func NewDynamicMPD(profile DashProfile, availabilityStartTime, minBufferTime string, attributes ...AttrMPD) *MPD { + period := &Period{} + mpd := &MPD{ + XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"), + Profiles: Strptr((string)(profile)), + Type: Strptr("dynamic"), + AvailabilityStartTime: Strptr(availabilityStartTime), + MinBufferTime: Strptr(minBufferTime), + period: period, + Periods: []*Period{period}, + UTCTiming: &DescriptorType{}, + } + + for i := range attributes { + switch attr := attributes[i].(type) { + case *attrMinimumUpdatePeriod: + mpd.MinimumUpdatePeriod = attr.GetStrptr() + case *attrMediaPresentationDuration: + mpd.MediaPresentationDuration = attr.GetStrptr() + case *attrPublishTime: + mpd.PublishTime = attr.GetStrptr() + } + } + + return mpd +} + +// AddNewPeriod creates a new Period and make it the currently active one. +func (m *MPD) AddNewPeriod() *Period { + if m.period != nil && m.period.ID == "" && m.period.AdaptationSets == nil { + return m.GetCurrentPeriod() + } + period := &Period{} + m.Periods = append(m.Periods, period) + m.period = period + return period +} + +// GetCurrentPeriod returns the current Period. +func (m *MPD) GetCurrentPeriod() *Period { + return m.period +} + +func (period *Period) SetDuration(d time.Duration) { + period.Duration = Duration(d) +} + +// Create a new Adaptation Set for thumbnails. +// mimeType - e.g. (image/jpeg) +func (m *MPD) AddNewAdaptationSetThumbnails(mimeType string) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetThumbnails(mimeType) +} + +func (period *Period) AddNewAdaptationSetThumbnails(mimeType string) (*AdaptationSet, error) { + as := &AdaptationSet{ + ContentType: Strptr(DASH_CONTENT_TYPE_IMAGE), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +func (m *MPD) AddNewAdaptationSetThumbnailsWithID(id, mimeType string) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetThumbnailsWithID(id, mimeType) +} + +func (period *Period) AddNewAdaptationSetThumbnailsWithID(id, mimeType string) (*AdaptationSet, error) { + as := &AdaptationSet{ + ID: Strptr(id), + ContentType: Strptr(DASH_CONTENT_TYPE_IMAGE), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +// Create a new Adaptation Set for Audio Assets. +// mimeType - MIME Type (i.e. audio/mp4). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +// lang - Language (i.e. en). +func (m *MPD) AddNewAdaptationSetAudio(mimeType string, segmentAlignment bool, startWithSAP int64, lang string) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetAudio(mimeType, segmentAlignment, startWithSAP, lang) +} + +// Create a new Adaptation Set for Audio Assets. +// mimeType - MIME Type (i.e. audio/mp4). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +// lang - Language (i.e. en). +func (m *MPD) AddNewAdaptationSetAudioWithID(id string, mimeType string, segmentAlignment bool, startWithSAP int64, lang string) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetAudioWithID(id, mimeType, segmentAlignment, startWithSAP, lang) +} + +// Create a new Adaptation Set for Audio Assets. +// mimeType - MIME Type (i.e. audio/mp4). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +// lang - Language (i.e. en). +func (period *Period) AddNewAdaptationSetAudio(mimeType string, segmentAlignment bool, startWithSAP int64, lang string) (*AdaptationSet, error) { + as := &AdaptationSet{ + SegmentAlignment: Boolptr(segmentAlignment), + Lang: Strptr(lang), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + StartWithSAP: Int64ptr(startWithSAP), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +// Create a new Adaptation Set for Audio Assets. +// mimeType - MIME Type (i.e. audio/mp4). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +// lang - Language (i.e. en). +func (period *Period) AddNewAdaptationSetAudioWithID(id string, mimeType string, segmentAlignment bool, startWithSAP int64, lang string) (*AdaptationSet, error) { + as := &AdaptationSet{ + ID: Strptr(id), + SegmentAlignment: Boolptr(segmentAlignment), + Lang: Strptr(lang), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + StartWithSAP: Int64ptr(startWithSAP), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +// Create a new Adaptation Set for Video Assets. +// mimeType - MIME Type (i.e. video/mp4). +// scanType - Scan Type (i.e.progressive). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +func (m *MPD) AddNewAdaptationSetVideo(mimeType string, scanType string, segmentAlignment bool, startWithSAP int64) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetVideo(mimeType, scanType, segmentAlignment, startWithSAP) +} + +// Create a new Adaptation Set for Video Assets. +// mimeType - MIME Type (i.e. video/mp4). +// scanType - Scan Type (i.e.progressive). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +func (m *MPD) AddNewAdaptationSetVideoWithID(id string, mimeType string, scanType string, segmentAlignment bool, startWithSAP int64) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetVideoWithID(id, mimeType, scanType, segmentAlignment, startWithSAP) +} + +// Create a new Adaptation Set for Video Assets. +// mimeType - MIME Type (i.e. video/mp4). +// scanType - Scan Type (i.e.progressive). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +func (period *Period) AddNewAdaptationSetVideo(mimeType string, scanType string, segmentAlignment bool, startWithSAP int64) (*AdaptationSet, error) { + as := &AdaptationSet{ + SegmentAlignment: Boolptr(segmentAlignment), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + StartWithSAP: Int64ptr(startWithSAP), + ScanType: Strptr(scanType), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +// Create a new Adaptation Set for Video Assets. +// mimeType - MIME Type (i.e. video/mp4). +// scanType - Scan Type (i.e.progressive). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +func (period *Period) AddNewAdaptationSetVideoWithID(id string, mimeType string, scanType string, segmentAlignment bool, startWithSAP int64) (*AdaptationSet, error) { + as := &AdaptationSet{ + SegmentAlignment: Boolptr(segmentAlignment), + ID: Strptr(id), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + StartWithSAP: Int64ptr(startWithSAP), + ScanType: Strptr(scanType), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +// Create a new Adaptation Set for Subtitle Assets. +// mimeType - MIME Type (i.e. text/vtt). +// lang - Language (i.e. en). +func (m *MPD) AddNewAdaptationSetSubtitle(mimeType string, lang string) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetSubtitle(mimeType, lang) +} + +// Create a new Adaptation Set for Subtitle Assets. +// mimeType - MIME Type (i.e. text/vtt). +// lang - Language (i.e. en). +func (m *MPD) AddNewAdaptationSetSubtitleWithID(id string, mimeType string, lang string) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetSubtitleWithID(id, mimeType, lang) +} + +// Create a new Adaptation Set for Subtitle Assets. +// mimeType - MIME Type (i.e. text/vtt). +// lang - Language (i.e. en). +func (period *Period) AddNewAdaptationSetSubtitle(mimeType string, lang string) (*AdaptationSet, error) { + as := &AdaptationSet{ + Lang: Strptr(lang), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +// Create a new Adaptation Set for Subtitle Assets. +// mimeType - MIME Type (i.e. text/vtt). +// lang - Language (i.e. en). +func (period *Period) AddNewAdaptationSetSubtitleWithID(id string, mimeType string, lang string) (*AdaptationSet, error) { + as := &AdaptationSet{ + ID: Strptr(id), + Lang: Strptr(lang), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +// Internal helper method for adding a AdapatationSet. +func (period *Period) addAdaptationSet(as *AdaptationSet) error { + if as == nil { + return ErrAdaptationSetNil + } + period.AdaptationSets = append(period.AdaptationSets, as) + return nil +} + +// Adds a ContentProtection tag at the root level of an AdaptationSet. +// This ContentProtection tag does not include signaling for any particular DRM scheme. +// defaultKIDHex - Default Key ID as a Hex String. +// +// NOTE: this is only here for Legacy purposes. This will create an invalid UUID. +func (as *AdaptationSet) AddNewContentProtectionRootLegacyUUID(defaultKIDHex string) (*CENCContentProtection, error) { + if len(defaultKIDHex) != 32 || defaultKIDHex == "" { + return nil, ErrInvalidDefaultKID + } + + // Convert the KID into the correct format + defaultKID := strings.ToLower(defaultKIDHex[0:8] + "-" + defaultKIDHex[8:12] + "-" + defaultKIDHex[12:16] + "-" + defaultKIDHex[16:32]) + + cp := &CENCContentProtection{ + DefaultKID: Strptr(defaultKID), + Value: Strptr(CONTENT_PROTECTION_ROOT_VALUE), + } + cp.SchemeIDURI = Strptr(CONTENT_PROTECTION_ROOT_SCHEME_ID_URI) + cp.XMLNS = Strptr(CENC_XMLNS) + + err := as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +// Adds a ContentProtection tag at the root level of an AdaptationSet. +// This ContentProtection tag does not include signaling for any particular DRM scheme. +// defaultKIDHex - Default Key ID as a Hex String. +func (as *AdaptationSet) AddNewContentProtectionRoot(defaultKIDHex string) (*CENCContentProtection, error) { + if len(defaultKIDHex) != 32 || defaultKIDHex == "" { + return nil, ErrInvalidDefaultKID + } + + // Convert the KID into the correct format + defaultKID := strings.ToLower(defaultKIDHex[0:8] + "-" + defaultKIDHex[8:12] + "-" + defaultKIDHex[12:16] + "-" + defaultKIDHex[16:20] + "-" + defaultKIDHex[20:32]) + + cp := &CENCContentProtection{ + DefaultKID: Strptr(defaultKID), + Value: Strptr(CONTENT_PROTECTION_ROOT_VALUE), + } + cp.SchemeIDURI = Strptr(CONTENT_PROTECTION_ROOT_SCHEME_ID_URI) + cp.XMLNS = Strptr(CENC_XMLNS) + + err := as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +// AddNewContentProtectionSchemeWidevine adds a new content protection scheme for Widevine DRM to the adaptation set. With +// a element that contains a Base64 encoded PSSH box +// wvHeader - binary representation of Widevine Header +// !!! Note: this function will accept any byte slice as a wvHeader value !!! +func (as *AdaptationSet) AddNewContentProtectionSchemeWidevineWithPSSH(wvHeader []byte) (*WidevineContentProtection, error) { + cp, err := NewWidevineContentProtection(wvHeader) + if err != nil { + return nil, err + } + + err = as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +// AddNewContentProtectionSchemeWidevine adds a new content protection scheme for Widevine DRM to the adaptation set. +func (as *AdaptationSet) AddNewContentProtectionSchemeWidevine() (*WidevineContentProtection, error) { + cp, err := NewWidevineContentProtection(nil) + if err != nil { + return nil, err + } + + err = as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +func NewWidevineContentProtection(wvHeader []byte) (*WidevineContentProtection, error) { + cp := &WidevineContentProtection{} + cp.SchemeIDURI = Strptr(CONTENT_PROTECTION_WIDEVINE_SCHEME_ID) + + if len(wvHeader) > 0 { + cp.XMLNS = Strptr(CENC_XMLNS) + wvSystemID, err := hex.DecodeString(CONTENT_PROTECTION_WIDEVINE_SCHEME_HEX) + if err != nil { + panic(err.Error()) + } + psshBox, err := MakePSSHBox(wvSystemID, wvHeader) + if err != nil { + return nil, err + } + + psshB64 := base64.StdEncoding.EncodeToString(psshBox) + cp.PSSH = &psshB64 + } + return cp, nil +} + +// AddNewContentProtectionSchemePlayready adds a new content protection scheme for PlayReady DRM. +// pro - PlayReady Object Header, as a Base64 encoded string. +func (as *AdaptationSet) AddNewContentProtectionSchemePlayready(pro string) (*PlayreadyContentProtection, error) { + cp, err := newPlayreadyContentProtection(pro, CONTENT_PROTECTION_PLAYREADY_SCHEME_ID) + if err != nil { + return nil, err + } + + err = as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +// AddNewContentProtectionSchemePlayreadyV10 adds a new content protection scheme for PlayReady v1.0 DRM. +// pro - PlayReady Object Header, as a Base64 encoded string. +func (as *AdaptationSet) AddNewContentProtectionSchemePlayreadyV10(pro string) (*PlayreadyContentProtection, error) { + cp, err := newPlayreadyContentProtection(pro, CONTENT_PROTECTION_PLAYREADY_SCHEME_V10_ID) + if err != nil { + return nil, err + } + + err = as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +func newPlayreadyContentProtection(pro string, schemeIDURI string) (*PlayreadyContentProtection, error) { + if pro == "" { + return nil, ErrPROEmpty + } + + cp := &PlayreadyContentProtection{ + PlayreadyXMLNS: Strptr(CONTENT_PROTECTION_PLAYREADY_XMLNS), + PRO: Strptr(pro), + } + cp.SchemeIDURI = Strptr(schemeIDURI) + + return cp, nil +} + +// AddNewContentProtectionSchemePlayreadyWithPSSH adds a new content protection scheme for PlayReady DRM. The scheme +// will include both ms:pro and cenc:pssh subelements +// pro - PlayReady Object Header, as a Base64 encoded string. +func (as *AdaptationSet) AddNewContentProtectionSchemePlayreadyWithPSSH(pro string) (*PlayreadyContentProtection, error) { + cp, err := newPlayreadyContentProtection(pro, CONTENT_PROTECTION_PLAYREADY_SCHEME_ID) + if err != nil { + return nil, err + } + cp.XMLNS = Strptr(CENC_XMLNS) + prSystemID, err := hex.DecodeString(CONTENT_PROTECTION_PLAYREADY_SCHEME_HEX) + if err != nil { + panic(err.Error()) + } + + proBin, err := base64.StdEncoding.DecodeString(pro) + if err != nil { + return nil, err + } + + psshBox, err := MakePSSHBox(prSystemID, proBin) + if err != nil { + return nil, err + } + cp.PSSH = Strptr(base64.StdEncoding.EncodeToString(psshBox)) + + err = as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +// AddNewContentProtectionSchemePlayreadyV10WithPSSH adds a new content protection scheme for PlayReady v1.0 DRM. The scheme +// will include both ms:pro and cenc:pssh subelements +// pro - PlayReady Object Header, as a Base64 encoded string. +func (as *AdaptationSet) AddNewContentProtectionSchemePlayreadyV10WithPSSH(pro string) (*PlayreadyContentProtection, error) { + cp, err := newPlayreadyContentProtection(pro, CONTENT_PROTECTION_PLAYREADY_SCHEME_V10_ID) + if err != nil { + return nil, err + } + cp.XMLNS = Strptr(CENC_XMLNS) + prSystemID, err := hex.DecodeString(CONTENT_PROTECTION_PLAYREADY_SCHEME_V10_HEX) + if err != nil { + panic(err.Error()) + } + + proBin, err := base64.StdEncoding.DecodeString(pro) + if err != nil { + return nil, err + } + + psshBox, err := MakePSSHBox(prSystemID, proBin) + if err != nil { + return nil, err + } + cp.PSSH = Strptr(base64.StdEncoding.EncodeToString(psshBox)) + + err = as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +// Internal helper method for adding a ContentProtection to an AdaptationSet. +func (as *AdaptationSet) AddContentProtection(cp ContentProtectioner) error { + if cp == nil { + return ErrContentProtectionNil + } + + as.ContentProtection = append(as.ContentProtection, cp) + return nil +} + +// Sets up a new SegmentTemplate for an AdaptationSet. +// duration - relative to timescale (i.e. 2000). +// init - template string for init segment (i.e. $RepresentationID$/audio/en/init.mp4). +// media - template string for media segments. +// startNumber - the number to start segments from ($Number$) (i.e. 0). +// timescale - sets the timescale for duration (i.e. 1000, represents milliseconds). +func (as *AdaptationSet) SetNewSegmentTemplate(duration int64, init string, media string, startNumber int64, timescale int64) (*SegmentTemplate, error) { + st := &SegmentTemplate{ + Duration: Int64ptr(duration), + Initialization: Strptr(init), + Media: Strptr(media), + StartNumber: Int64ptr(startNumber), + Timescale: Int64ptr(timescale), + } + + err := as.setSegmentTemplate(st) + if err != nil { + return nil, err + } + return st, nil +} + +// Internal helper method for setting the Segment Template on an AdaptationSet. +func (as *AdaptationSet) setSegmentTemplate(st *SegmentTemplate) error { + if st == nil { + return ErrSegmentTemplateNil + } + st.AdaptationSet = as + as.SegmentTemplate = st + return nil +} + +// Adds a new SegmentTemplate to a thumbnail AdaptationSet +// duration - relative to timescale (i.e. 2000). +// media - template string for media segments. +// startNumber - the number to start segments from ($Number$) (i.e. 0). +// timescale - sets the timescale for duration (i.e. 1000, represents milliseconds). +func (as *AdaptationSet) SetNewSegmentTemplateThumbnails(duration int64, media string, startNumber int64, timescale int64) (*SegmentTemplate, error) { + st := &SegmentTemplate{ + Duration: Int64ptr(duration), + Media: Strptr(media), + StartNumber: Int64ptr(startNumber), + Timescale: Int64ptr(timescale), + } + + err := as.setSegmentTemplate(st) + if err != nil { + return nil, err + } + return st, nil +} + +// Adds a new Thumbnail representation to an AdaptationSet. +// bandwidth - in Bits/s (i.e. 1518664). +// id - ID for this representation, will get used as $RepresentationID$ in template strings. +// width - width of the video (i.e. 1280). +// height - height of the video (i.e 720). +// uri - +func (as *AdaptationSet) AddNewRepresentationThumbnails(id, val, uri string, bandwidth, width, height int64) (*Representation, error) { + r := &Representation{ + Bandwidth: Int64ptr(bandwidth), + ID: Strptr(id), + Width: Int64ptr(width), + Height: Int64ptr(height), + CommonAttributesAndElements: CommonAttributesAndElements{ + EssentialProperty: []DescriptorType{ + { + SchemeIDURI: Strptr(uri), + Value: Strptr(val), + }, + }, + }, + } + + err := as.addRepresentation(r) + if err != nil { + return nil, err + } + return r, nil +} + +// Adds a new Audio representation to an AdaptationSet. +// samplingRate - in Hz (i.e. 44100). +// bandwidth - in Bits/s (i.e. 67095). +// codecs - codec string for Audio Only (in RFC6381, https://tools.ietf.org/html/rfc6381) (i.e. mp4a.40.2). +// id - ID for this representation, will get used as $RepresentationID$ in template strings. +func (as *AdaptationSet) AddNewRepresentationAudio(samplingRate int64, bandwidth int64, codecs string, id string) (*Representation, error) { + r := &Representation{ + AudioSamplingRate: Int64ptr(samplingRate), + Bandwidth: Int64ptr(bandwidth), + Codecs: Strptr(codecs), + ID: Strptr(id), + } + + err := as.addRepresentation(r) + if err != nil { + return nil, err + } + return r, nil +} + +// Adds a new Video representation to an AdaptationSet. +// bandwidth - in Bits/s (i.e. 1518664). +// codecs - codec string for Audio Only (in RFC6381, https://tools.ietf.org/html/rfc6381) (i.e. avc1.4d401f). +// id - ID for this representation, will get used as $RepresentationID$ in template strings. +// frameRate - video frame rate (as a fraction) (i.e. 30000/1001). +// width - width of the video (i.e. 1280). +// height - height of the video (i.e 720). +func (as *AdaptationSet) AddNewRepresentationVideo(bandwidth int64, codecs string, id string, frameRate string, width int64, height int64) (*Representation, error) { + r := &Representation{ + Bandwidth: Int64ptr(bandwidth), + Codecs: Strptr(codecs), + ID: Strptr(id), + FrameRate: Strptr(frameRate), + Width: Int64ptr(width), + Height: Int64ptr(height), + } + + err := as.addRepresentation(r) + if err != nil { + return nil, err + } + return r, nil +} + +// Adds a new Subtitle representation to an AdaptationSet. +// bandwidth - in Bits/s (i.e. 256). +// id - ID for this representation, will get used as $RepresentationID$ in template strings. +func (as *AdaptationSet) AddNewRepresentationSubtitle(bandwidth int64, id string) (*Representation, error) { + r := &Representation{ + Bandwidth: Int64ptr(bandwidth), + ID: Strptr(id), + } + + err := as.addRepresentation(r) + if err != nil { + return nil, err + } + return r, nil +} + +// Internal helper method for adding a Representation to an AdaptationSet. +func (as *AdaptationSet) addRepresentation(r *Representation) error { + if r == nil { + return ErrRepresentationNil + } + r.AdaptationSet = as + as.Representations = append(as.Representations, r) + return nil +} + +// Internal helper method for adding an Accessibility element to an AdaptationSet. +func (as *AdaptationSet) addAccessibility(a *Accessibility) error { + if a == nil { + return ErrAccessibilityNil + } + a.AdaptationSet = as + as.AccessibilityElems = append(as.AccessibilityElems, a) + return nil +} + +// Adds a new Role to an AdaptationSet +// schemeIdUri - Scheme ID URI string (i.e. urn:mpeg:dash:role:2011) +// value - Value for this role, (i.e. caption, subtitle, main, alternate, supplementary, commentary, dub) +func (as *AdaptationSet) AddNewRole(schemeIDURI string, value string) (*Role, error) { + r := &Role{ + SchemeIDURI: Strptr(schemeIDURI), + Value: Strptr(value), + } + r.AdaptationSet = as + as.Roles = append(as.Roles, r) + return r, nil +} + +// AddNewAccessibilityElement adds a new accessibility element to an adaptation set +// schemeIdUri - Scheme ID URI for the Accessibility element (i.e. urn:tva:metadata:cs:AudioPurposeCS:2007) +// value - specified value based on scheme +func (as *AdaptationSet) AddNewAccessibilityElement(scheme AccessibilityElementScheme, val string) (*Accessibility, error) { + accessibility := &Accessibility{ + SchemeIdUri: Strptr((string)(scheme)), + Value: Strptr(val), + } + + err := as.addAccessibility(accessibility) + if err != nil { + return nil, err + } + + return accessibility, nil +} + +// Sets the BaseURL for a Representation. +// baseURL - Base URL as a string (i.e. 800k/output-audio-und.mp4) +func (r *Representation) SetNewBaseURL(baseURL string) error { + if baseURL == "" { + return ErrBaseURLEmpty + } + r.BaseURL = Strptr(baseURL) + return nil +} + +// Sets a new SegmentBase on a Representation. +// This is for On Demand profile. +// indexRange - Byte range to the index (sidx)atom. +// init - Byte range to the init atoms (ftyp+moov). +func (r *Representation) AddNewSegmentBase(indexRange string, initRange string) (*SegmentBase, error) { + sb := &SegmentBase{ + IndexRange: Strptr(indexRange), + Initialization: &URL{Range: Strptr(initRange)}, + } + + err := r.setSegmentBase(sb) + if err != nil { + return nil, err + } + return sb, nil +} + +// Internal helper method for setting the SegmentBase on a Representation. +func (r *Representation) setSegmentBase(sb *SegmentBase) error { + if r.AdaptationSet == nil { + return ErrNoDASHProfileSet + } + if sb == nil { + return ErrSegmentBaseNil + } + r.SegmentBase = sb + return nil +} + +// Sets a new AudioChannelConfiguration on a Representation. +// This is required for the HbbTV profile. +// scheme - One of the two AudioConfigurationSchemes. +// channelConfiguration - string that represents the channel configuration. +func (r *Representation) AddNewAudioChannelConfiguration(scheme AudioChannelConfigurationScheme, channelConfiguration string) (*AudioChannelConfiguration, error) { + acc := &AudioChannelConfiguration{ + SchemeIDURI: Strptr((string)(scheme)), + Value: Strptr(channelConfiguration), + } + + err := r.setAudioChannelConfiguration(acc) + if err != nil { + return nil, err + } + + return acc, nil +} + +// Internal helper method for setting the SegmentBase on a Representation. +func (r *Representation) setAudioChannelConfiguration(acc *AudioChannelConfiguration) error { + if acc == nil { + return ErrAudioChannelConfigurationNil + } + r.AudioChannelConfiguration = acc + return nil +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/mpd_attr.go b/vendor/github.com/zencoder/go-dash/v3/mpd/mpd_attr.go new file mode 100644 index 000000000..c64514a71 --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/mpd_attr.go @@ -0,0 +1,57 @@ +package mpd + +type AttrMPD interface { + GetStrptr() *string +} + +type attrAvailabilityStartTime struct { + strptr *string +} + +func (attr *attrAvailabilityStartTime) GetStrptr() *string { + return attr.strptr +} + +// AttrAvailabilityStartTime returns AttrMPD object for NewMPD +func AttrAvailabilityStartTime(value string) AttrMPD { + return &attrAvailabilityStartTime{strptr: &value} +} + +type attrMinimumUpdatePeriod struct { + strptr *string +} + +func (attr *attrMinimumUpdatePeriod) GetStrptr() *string { + return attr.strptr +} + +// AttrMinimumUpdatePeriod returns AttrMPD object for NewMPD +func AttrMinimumUpdatePeriod(value string) AttrMPD { + return &attrMinimumUpdatePeriod{strptr: &value} +} + +type attrMediaPresentationDuration struct { + strptr *string +} + +func (attr *attrMediaPresentationDuration) GetStrptr() *string { + return attr.strptr +} + +// AttrMediaPresentationDuration returns AttrMPD object for NewMPD +func AttrMediaPresentationDuration(value string) AttrMPD { + return &attrMediaPresentationDuration{strptr: &value} +} + +type attrPublishTime struct { + strptr *string +} + +func (attr *attrPublishTime) GetStrptr() *string { + return attr.strptr +} + +// AttrPublishTime returns AttrMPD object for NewMPD +func AttrPublishTime(value string) AttrMPD { + return &attrPublishTime{strptr: &value} +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/mpd_read_write.go b/vendor/github.com/zencoder/go-dash/v3/mpd/mpd_read_write.go new file mode 100644 index 000000000..35d3f9f33 --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/mpd_read_write.go @@ -0,0 +1,87 @@ +package mpd + +import ( + "bufio" + "bytes" + "encoding/xml" + "io" + "os" +) + +// Reads an MPD XML file from disk into a MPD object. +// path - File path to an MPD on disk +func ReadFromFile(path string) (*MPD, error) { + f, err := os.OpenFile(path, os.O_RDONLY, 0666) + if err != nil { + return nil, err + } + defer f.Close() + + return Read(f) +} + +// Reads a string into a MPD object. +// xmlStr - MPD manifest data as a string. +func ReadFromString(xmlStr string) (*MPD, error) { + b := bytes.NewBufferString(xmlStr) + return Read(b) +} + +// Reads from an io.Reader interface into an MPD object. +// r - Must implement the io.Reader interface. +func Read(r io.Reader) (*MPD, error) { + var mpd MPD + d := xml.NewDecoder(r) + err := d.Decode(&mpd) + if err != nil { + return nil, err + } + return &mpd, nil +} + +// Writes an MPD object to a file on disk. +// path - Output path to write the manifest to. +func (m *MPD) WriteToFile(path string) error { + // Open the file to write the XML to + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer f.Close() + if err = m.Write(f); err != nil { + return err + } + if err = f.Sync(); err != nil { + return err + } + return err +} + +// Writes an MPD object to a string. +func (m *MPD) WriteToString() (string, error) { + var b bytes.Buffer + w := bufio.NewWriter(&b) + err := m.Write(w) + if err != nil { + return "", err + } + err = w.Flush() + if err != nil { + return "", err + } + return b.String(), err +} + +// Writes an MPD object to an io.Writer interface +// w - Must implement the io.Writer interface. +func (m *MPD) Write(w io.Writer) error { + b, err := xml.MarshalIndent(m, "", " ") + if err != nil { + return err + } + + _, _ = w.Write([]byte(xml.Header)) + _, _ = w.Write(b) + _, _ = w.Write([]byte("\n")) + return nil +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/pssh.go b/vendor/github.com/zencoder/go-dash/v3/mpd/pssh.go new file mode 100644 index 000000000..98a7112c2 --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/pssh.go @@ -0,0 +1,41 @@ +package mpd + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +func MakePSSHBox(systemID, payload []byte) ([]byte, error) { + if len(systemID) != 16 { + return nil, fmt.Errorf("SystemID must be 16 bytes, was: %d", len(systemID)) + } + + psshBuf := &bytes.Buffer{} + size := uint32(12 + 16 + 4 + len(payload)) // 3 uint32s, systemID, "pssh" string and payload + if err := binary.Write(psshBuf, binary.BigEndian, size); err != nil { + return nil, err + } + + if err := binary.Write(psshBuf, binary.BigEndian, []byte("pssh")); err != nil { + return nil, err + } + + if err := binary.Write(psshBuf, binary.BigEndian, uint32(0)); err != nil { + return nil, err + } + + if _, err := psshBuf.Write(systemID); err != nil { + return nil, err + } + + if err := binary.Write(psshBuf, binary.BigEndian, uint32(len(payload))); err != nil { + return nil, err + } + + if _, err := psshBuf.Write(payload); err != nil { + return nil, err + } + + return psshBuf.Bytes(), nil +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/segment.go b/vendor/github.com/zencoder/go-dash/v3/mpd/segment.go new file mode 100644 index 000000000..bb112bfc7 --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/segment.go @@ -0,0 +1,47 @@ +package mpd + +type SegmentBase struct { + Initialization *URL `xml:"Initialization,omitempty"` + RepresentationIndex *URL `xml:"RepresentationIndex,omitempty"` + Timescale *uint32 `xml:"timescale,attr,omitempty"` + PresentationTimeOffset *uint64 `xml:"presentationTimeOffset,attr,omitempty"` + IndexRange *string `xml:"indexRange,attr,omitempty"` + IndexRangeExact *bool `xml:"indexRangeExact,attr,omitempty"` + AvailabilityTimeOffset *float32 `xml:"availabilityTimeOffset,attr,omitempty"` + AvailabilityTimeComplete *bool `xml:"availabilityTimeComplete,attr,omitempty"` +} + +type MultipleSegmentBase struct { + SegmentBase + SegmentTimeline *SegmentTimeline `xml:"SegmentTimeline,omitempty"` + BitstreamSwitching *URL `xml:"BitstreamSwitching,omitempty"` + Duration *uint32 `xml:"duration,attr,omitempty"` + StartNumber *uint32 `xml:"startNumber,attr,omitempty"` +} + +type SegmentList struct { + MultipleSegmentBase + SegmentURLs []*SegmentURL `xml:"SegmentURL,omitempty"` +} + +type SegmentURL struct { + Media *string `xml:"media,attr,omitempty"` + MediaRange *string `xml:"mediaRange,attr,omitempty"` + Index *string `xml:"index,attr,omitempty"` + IndexRange *string `xml:"indexRange,attr,omitempty"` +} + +type SegmentTimeline struct { + Segments []*SegmentTimelineSegment `xml:"S,omitempty"` +} + +type SegmentTimelineSegment struct { + StartTime *uint64 `xml:"t,attr,omitempty"` + Duration uint64 `xml:"d,attr"` + RepeatCount *int `xml:"r,attr,omitempty"` +} + +type URL struct { + SourceURL *string `xml:"sourceURL,attr,omitempty"` + Range *string `xml:"range,attr,omitempty"` +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/validate.go b/vendor/github.com/zencoder/go-dash/v3/mpd/validate.go new file mode 100644 index 000000000..6c8521acb --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/validate.go @@ -0,0 +1,9 @@ +package mpd + +// Validate checks for incomplete MPD object +func (m *MPD) Validate() error { + if m.Profiles == nil { + return ErrNoDASHProfileSet + } + return nil +} diff --git a/vendor/golang.org/x/image/AUTHORS b/vendor/golang.org/x/image/AUTHORS deleted file mode 100644 index 15167cd74..000000000 --- a/vendor/golang.org/x/image/AUTHORS +++ /dev/null @@ -1,3 +0,0 @@ -# This source code refers to The Go Authors for copyright purposes. -# The master list of authors is in the main Go distribution, -# visible at http://tip.golang.org/AUTHORS. diff --git a/vendor/golang.org/x/image/CONTRIBUTORS b/vendor/golang.org/x/image/CONTRIBUTORS deleted file mode 100644 index 1c4577e96..000000000 --- a/vendor/golang.org/x/image/CONTRIBUTORS +++ /dev/null @@ -1,3 +0,0 @@ -# This source code was written by the Go contributors. -# The master list of contributors is in the main Go distribution, -# visible at http://tip.golang.org/CONTRIBUTORS. diff --git a/vendor/golang.org/x/image/bmp/reader.go b/vendor/golang.org/x/image/bmp/reader.go index 52e25205c..e165c2e39 100644 --- a/vendor/golang.org/x/image/bmp/reader.go +++ b/vendor/golang.org/x/image/bmp/reader.go @@ -85,7 +85,7 @@ func decodeRGB(r io.Reader, c image.Config, topDown bool) (image.Image, error) { // decodeNRGBA reads a 32 bit-per-pixel BMP image from r. // If topDown is false, the image rows will be read bottom-up. -func decodeNRGBA(r io.Reader, c image.Config, topDown bool) (image.Image, error) { +func decodeNRGBA(r io.Reader, c image.Config, topDown, allowAlpha bool) (image.Image, error) { rgba := image.NewNRGBA(image.Rect(0, 0, c.Width, c.Height)) if c.Width == 0 || c.Height == 0 { return rgba, nil @@ -102,6 +102,9 @@ func decodeNRGBA(r io.Reader, c image.Config, topDown bool) (image.Image, error) for i := 0; i < len(p); i += 4 { // BMP images are stored in BGRA order rather than RGBA order. p[i+0], p[i+2] = p[i+2], p[i+0] + if !allowAlpha { + p[i+3] = 0xFF + } } } return rgba, nil @@ -110,7 +113,7 @@ func decodeNRGBA(r io.Reader, c image.Config, topDown bool) (image.Image, error) // Decode reads a BMP image from r and returns it as an image.Image. // Limitation: The file must be 8, 24 or 32 bits per pixel. func Decode(r io.Reader) (image.Image, error) { - c, bpp, topDown, err := decodeConfig(r) + c, bpp, topDown, allowAlpha, err := decodeConfig(r) if err != nil { return nil, err } @@ -120,7 +123,7 @@ func Decode(r io.Reader) (image.Image, error) { case 24: return decodeRGB(r, c, topDown) case 32: - return decodeNRGBA(r, c, topDown) + return decodeNRGBA(r, c, topDown, allowAlpha) } panic("unreachable") } @@ -129,13 +132,15 @@ func Decode(r io.Reader) (image.Image, error) { // decoding the entire image. // Limitation: The file must be 8, 24 or 32 bits per pixel. func DecodeConfig(r io.Reader) (image.Config, error) { - config, _, _, err := decodeConfig(r) + config, _, _, _, err := decodeConfig(r) return config, err } -func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown bool, err error) { - // We only support those BMP images that are a BITMAPFILEHEADER - // immediately followed by a BITMAPINFOHEADER. +func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown bool, allowAlpha bool, err error) { + // We only support those BMP images with one of the following DIB headers: + // - BITMAPINFOHEADER (40 bytes) + // - BITMAPV4HEADER (108 bytes) + // - BITMAPV5HEADER (124 bytes) const ( fileHeaderLen = 14 infoHeaderLen = 40 @@ -147,21 +152,21 @@ func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown b if err == io.EOF { err = io.ErrUnexpectedEOF } - return image.Config{}, 0, false, err + return image.Config{}, 0, false, false, err } if string(b[:2]) != "BM" { - return image.Config{}, 0, false, errors.New("bmp: invalid format") + return image.Config{}, 0, false, false, errors.New("bmp: invalid format") } offset := readUint32(b[10:14]) infoLen := readUint32(b[14:18]) if infoLen != infoHeaderLen && infoLen != v4InfoHeaderLen && infoLen != v5InfoHeaderLen { - return image.Config{}, 0, false, ErrUnsupported + return image.Config{}, 0, false, false, ErrUnsupported } if _, err := io.ReadFull(r, b[fileHeaderLen+4:fileHeaderLen+infoLen]); err != nil { if err == io.EOF { err = io.ErrUnexpectedEOF } - return image.Config{}, 0, false, err + return image.Config{}, 0, false, false, err } width := int(int32(readUint32(b[18:22]))) height := int(int32(readUint32(b[22:26]))) @@ -169,12 +174,12 @@ func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown b height, topDown = -height, true } if width < 0 || height < 0 { - return image.Config{}, 0, false, ErrUnsupported + return image.Config{}, 0, false, false, ErrUnsupported } // We only support 1 plane and 8, 24 or 32 bits per pixel and no // compression. planes, bpp, compression := readUint16(b[26:28]), readUint16(b[28:30]), readUint32(b[30:34]) - // if compression is set to BITFIELDS, but the bitmask is set to the default bitmask + // if compression is set to BI_BITFIELDS, but the bitmask is set to the default bitmask // that would be used if compression was set to 0, we can continue as if compression was 0 if compression == 3 && infoLen > infoHeaderLen && readUint32(b[54:58]) == 0xff0000 && readUint32(b[58:62]) == 0xff00 && @@ -182,16 +187,16 @@ func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown b compression = 0 } if planes != 1 || compression != 0 { - return image.Config{}, 0, false, ErrUnsupported + return image.Config{}, 0, false, false, ErrUnsupported } switch bpp { case 8: if offset != fileHeaderLen+infoLen+256*4 { - return image.Config{}, 0, false, ErrUnsupported + return image.Config{}, 0, false, false, ErrUnsupported } _, err = io.ReadFull(r, b[:256*4]) if err != nil { - return image.Config{}, 0, false, err + return image.Config{}, 0, false, false, err } pcm := make(color.Palette, 256) for i := range pcm { @@ -199,19 +204,40 @@ func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown b // Every 4th byte is padding. pcm[i] = color.RGBA{b[4*i+2], b[4*i+1], b[4*i+0], 0xFF} } - return image.Config{ColorModel: pcm, Width: width, Height: height}, 8, topDown, nil + return image.Config{ColorModel: pcm, Width: width, Height: height}, 8, topDown, false, nil case 24: if offset != fileHeaderLen+infoLen { - return image.Config{}, 0, false, ErrUnsupported + return image.Config{}, 0, false, false, ErrUnsupported } - return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 24, topDown, nil + return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 24, topDown, false, nil case 32: if offset != fileHeaderLen+infoLen { - return image.Config{}, 0, false, ErrUnsupported + return image.Config{}, 0, false, false, ErrUnsupported } - return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 32, topDown, nil + // 32 bits per pixel is possibly RGBX (X is padding) or RGBA (A is + // alpha transparency). However, for BMP images, "Alpha is a + // poorly-documented and inconsistently-used feature" says + // https://source.chromium.org/chromium/chromium/src/+/bc0a792d7ebc587190d1a62ccddba10abeea274b:third_party/blink/renderer/platform/image-decoders/bmp/bmp_image_reader.cc;l=621 + // + // That goes on to say "BITMAPV3HEADER+ have an alpha bitmask in the + // info header... so we respect it at all times... [For earlier + // (smaller) headers we] ignore alpha in Windows V3 BMPs except inside + // ICO files". + // + // "Ignore" means to always set alpha to 0xFF (fully opaque): + // https://source.chromium.org/chromium/chromium/src/+/bc0a792d7ebc587190d1a62ccddba10abeea274b:third_party/blink/renderer/platform/image-decoders/bmp/bmp_image_reader.h;l=272 + // + // Confusingly, "Windows V3" does not correspond to BITMAPV3HEADER, but + // instead corresponds to the earlier (smaller) BITMAPINFOHEADER: + // https://source.chromium.org/chromium/chromium/src/+/bc0a792d7ebc587190d1a62ccddba10abeea274b:third_party/blink/renderer/platform/image-decoders/bmp/bmp_image_reader.cc;l=258 + // + // This Go package does not support ICO files and the (infoLen > + // infoHeaderLen) condition distinguishes BITMAPINFOHEADER (40 bytes) + // vs later (larger) headers. + allowAlpha = infoLen > infoHeaderLen + return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 32, topDown, allowAlpha, nil } - return image.Config{}, 0, false, ErrUnsupported + return image.Config{}, 0, false, false, ErrUnsupported } func init() { diff --git a/vendor/golang.org/x/image/ccitt/table.go b/vendor/golang.org/x/image/ccitt/table.go index ef7ea9d40..8b3794bc2 100644 --- a/vendor/golang.org/x/image/ccitt/table.go +++ b/vendor/golang.org/x/image/ccitt/table.go @@ -31,27 +31,27 @@ package ccitt // modeDecodeTable represents Table 1 and the End-of-Line code. // -// +=XXXXX -// b009 +-+ -// | +=v0009 -// b007 +-+ -// | | +=v0008 -// b010 | +-+ -// | +=v0005 -// b006 +-+ -// | | +=v0007 -// b008 | +-+ -// | +=v0004 -// b005 +-+ -// | +=v0000 -// b003 +-+ -// | +=v0001 -// b002 +-+ -// | | +=v0006 -// b004 | +-+ -// | +=v0003 -// b001 +-+ -// +=v0002 +// +=XXXXX +// b009 +-+ +// | +=v0009 +// b007 +-+ +// | | +=v0008 +// b010 | +-+ +// | +=v0005 +// b006 +-+ +// | | +=v0007 +// b008 | +-+ +// | +=v0004 +// b005 +-+ +// | +=v0000 +// b003 +-+ +// | +=v0001 +// b002 +-+ +// | | +=v0006 +// b004 | +-+ +// | +=v0003 +// b001 +-+ +// +=v0002 var modeDecodeTable = [...][2]int16{ 0: {0, 0}, 1: {2, ^2}, @@ -68,215 +68,215 @@ var modeDecodeTable = [...][2]int16{ // whiteDecodeTable represents Tables 2 and 3 for a white run. // -// +=XXXXX -// b059 +-+ -// | | +=v1792 -// b096 | | +-+ -// | | | | +=v1984 -// b100 | | | +-+ -// | | | +=v2048 -// b094 | | +-+ -// | | | | +=v2112 -// b101 | | | | +-+ -// | | | | | +=v2176 -// b097 | | | +-+ -// | | | | +=v2240 -// b102 | | | +-+ -// | | | +=v2304 -// b085 | +-+ -// | | +=v1856 -// b098 | | +-+ -// | | | +=v1920 -// b095 | +-+ -// | | +=v2368 -// b103 | | +-+ -// | | | +=v2432 -// b099 | +-+ -// | | +=v2496 -// b104 | +-+ -// | +=v2560 -// b040 +-+ -// | | +=v0029 -// b060 | +-+ -// | +=v0030 -// b026 +-+ -// | | +=v0045 -// b061 | | +-+ -// | | | +=v0046 -// b041 | +-+ -// | +=v0022 -// b016 +-+ -// | | +=v0023 -// b042 | | +-+ -// | | | | +=v0047 -// b062 | | | +-+ -// | | | +=v0048 -// b027 | +-+ -// | +=v0013 -// b008 +-+ -// | | +=v0020 -// b043 | | +-+ -// | | | | +=v0033 -// b063 | | | +-+ -// | | | +=v0034 -// b028 | | +-+ -// | | | | +=v0035 -// b064 | | | | +-+ -// | | | | | +=v0036 -// b044 | | | +-+ -// | | | | +=v0037 -// b065 | | | +-+ -// | | | +=v0038 -// b017 | +-+ -// | | +=v0019 -// b045 | | +-+ -// | | | | +=v0031 -// b066 | | | +-+ -// | | | +=v0032 -// b029 | +-+ -// | +=v0001 -// b004 +-+ -// | | +=v0012 -// b030 | | +-+ -// | | | | +=v0053 -// b067 | | | | +-+ -// | | | | | +=v0054 -// b046 | | | +-+ -// | | | +=v0026 -// b018 | | +-+ -// | | | | +=v0039 -// b068 | | | | +-+ -// | | | | | +=v0040 -// b047 | | | | +-+ -// | | | | | | +=v0041 -// b069 | | | | | +-+ -// | | | | | +=v0042 -// b031 | | | +-+ -// | | | | +=v0043 -// b070 | | | | +-+ -// | | | | | +=v0044 -// b048 | | | +-+ -// | | | +=v0021 -// b009 | +-+ -// | | +=v0028 -// b049 | | +-+ -// | | | | +=v0061 -// b071 | | | +-+ -// | | | +=v0062 -// b032 | | +-+ -// | | | | +=v0063 -// b072 | | | | +-+ -// | | | | | +=v0000 -// b050 | | | +-+ -// | | | | +=v0320 -// b073 | | | +-+ -// | | | +=v0384 -// b019 | +-+ -// | +=v0010 -// b002 +-+ -// | | +=v0011 -// b020 | | +-+ -// | | | | +=v0027 -// b051 | | | | +-+ -// | | | | | | +=v0059 -// b074 | | | | | +-+ -// | | | | | +=v0060 -// b033 | | | +-+ -// | | | | +=v1472 -// b086 | | | | +-+ -// | | | | | +=v1536 -// b075 | | | | +-+ -// | | | | | | +=v1600 -// b087 | | | | | +-+ -// | | | | | +=v1728 -// b052 | | | +-+ -// | | | +=v0018 -// b010 | | +-+ -// | | | | +=v0024 -// b053 | | | | +-+ -// | | | | | | +=v0049 -// b076 | | | | | +-+ -// | | | | | +=v0050 -// b034 | | | | +-+ -// | | | | | | +=v0051 -// b077 | | | | | | +-+ -// | | | | | | | +=v0052 -// b054 | | | | | +-+ -// | | | | | +=v0025 -// b021 | | | +-+ -// | | | | +=v0055 -// b078 | | | | +-+ -// | | | | | +=v0056 -// b055 | | | | +-+ -// | | | | | | +=v0057 -// b079 | | | | | +-+ -// | | | | | +=v0058 -// b035 | | | +-+ -// | | | +=v0192 -// b005 | +-+ -// | | +=v1664 -// b036 | | +-+ -// | | | | +=v0448 -// b080 | | | | +-+ -// | | | | | +=v0512 -// b056 | | | +-+ -// | | | | +=v0704 -// b088 | | | | +-+ -// | | | | | +=v0768 -// b081 | | | +-+ -// | | | +=v0640 -// b022 | | +-+ -// | | | | +=v0576 -// b082 | | | | +-+ -// | | | | | | +=v0832 -// b089 | | | | | +-+ -// | | | | | +=v0896 -// b057 | | | | +-+ -// | | | | | | +=v0960 -// b090 | | | | | | +-+ -// | | | | | | | +=v1024 -// b083 | | | | | +-+ -// | | | | | | +=v1088 -// b091 | | | | | +-+ -// | | | | | +=v1152 -// b037 | | | +-+ -// | | | | +=v1216 -// b092 | | | | +-+ -// | | | | | +=v1280 -// b084 | | | | +-+ -// | | | | | | +=v1344 -// b093 | | | | | +-+ -// | | | | | +=v1408 -// b058 | | | +-+ -// | | | +=v0256 -// b011 | +-+ -// | +=v0002 -// b001 +-+ -// | +=v0003 -// b012 | +-+ -// | | | +=v0128 -// b023 | | +-+ -// | | +=v0008 -// b006 | +-+ -// | | | +=v0009 -// b024 | | | +-+ -// | | | | | +=v0016 -// b038 | | | | +-+ -// | | | | +=v0017 -// b013 | | +-+ -// | | +=v0004 -// b003 +-+ -// | +=v0005 -// b014 | +-+ -// | | | +=v0014 -// b039 | | | +-+ -// | | | | +=v0015 -// b025 | | +-+ -// | | +=v0064 -// b007 +-+ -// | +=v0006 -// b015 +-+ -// +=v0007 +// +=XXXXX +// b059 +-+ +// | | +=v1792 +// b096 | | +-+ +// | | | | +=v1984 +// b100 | | | +-+ +// | | | +=v2048 +// b094 | | +-+ +// | | | | +=v2112 +// b101 | | | | +-+ +// | | | | | +=v2176 +// b097 | | | +-+ +// | | | | +=v2240 +// b102 | | | +-+ +// | | | +=v2304 +// b085 | +-+ +// | | +=v1856 +// b098 | | +-+ +// | | | +=v1920 +// b095 | +-+ +// | | +=v2368 +// b103 | | +-+ +// | | | +=v2432 +// b099 | +-+ +// | | +=v2496 +// b104 | +-+ +// | +=v2560 +// b040 +-+ +// | | +=v0029 +// b060 | +-+ +// | +=v0030 +// b026 +-+ +// | | +=v0045 +// b061 | | +-+ +// | | | +=v0046 +// b041 | +-+ +// | +=v0022 +// b016 +-+ +// | | +=v0023 +// b042 | | +-+ +// | | | | +=v0047 +// b062 | | | +-+ +// | | | +=v0048 +// b027 | +-+ +// | +=v0013 +// b008 +-+ +// | | +=v0020 +// b043 | | +-+ +// | | | | +=v0033 +// b063 | | | +-+ +// | | | +=v0034 +// b028 | | +-+ +// | | | | +=v0035 +// b064 | | | | +-+ +// | | | | | +=v0036 +// b044 | | | +-+ +// | | | | +=v0037 +// b065 | | | +-+ +// | | | +=v0038 +// b017 | +-+ +// | | +=v0019 +// b045 | | +-+ +// | | | | +=v0031 +// b066 | | | +-+ +// | | | +=v0032 +// b029 | +-+ +// | +=v0001 +// b004 +-+ +// | | +=v0012 +// b030 | | +-+ +// | | | | +=v0053 +// b067 | | | | +-+ +// | | | | | +=v0054 +// b046 | | | +-+ +// | | | +=v0026 +// b018 | | +-+ +// | | | | +=v0039 +// b068 | | | | +-+ +// | | | | | +=v0040 +// b047 | | | | +-+ +// | | | | | | +=v0041 +// b069 | | | | | +-+ +// | | | | | +=v0042 +// b031 | | | +-+ +// | | | | +=v0043 +// b070 | | | | +-+ +// | | | | | +=v0044 +// b048 | | | +-+ +// | | | +=v0021 +// b009 | +-+ +// | | +=v0028 +// b049 | | +-+ +// | | | | +=v0061 +// b071 | | | +-+ +// | | | +=v0062 +// b032 | | +-+ +// | | | | +=v0063 +// b072 | | | | +-+ +// | | | | | +=v0000 +// b050 | | | +-+ +// | | | | +=v0320 +// b073 | | | +-+ +// | | | +=v0384 +// b019 | +-+ +// | +=v0010 +// b002 +-+ +// | | +=v0011 +// b020 | | +-+ +// | | | | +=v0027 +// b051 | | | | +-+ +// | | | | | | +=v0059 +// b074 | | | | | +-+ +// | | | | | +=v0060 +// b033 | | | +-+ +// | | | | +=v1472 +// b086 | | | | +-+ +// | | | | | +=v1536 +// b075 | | | | +-+ +// | | | | | | +=v1600 +// b087 | | | | | +-+ +// | | | | | +=v1728 +// b052 | | | +-+ +// | | | +=v0018 +// b010 | | +-+ +// | | | | +=v0024 +// b053 | | | | +-+ +// | | | | | | +=v0049 +// b076 | | | | | +-+ +// | | | | | +=v0050 +// b034 | | | | +-+ +// | | | | | | +=v0051 +// b077 | | | | | | +-+ +// | | | | | | | +=v0052 +// b054 | | | | | +-+ +// | | | | | +=v0025 +// b021 | | | +-+ +// | | | | +=v0055 +// b078 | | | | +-+ +// | | | | | +=v0056 +// b055 | | | | +-+ +// | | | | | | +=v0057 +// b079 | | | | | +-+ +// | | | | | +=v0058 +// b035 | | | +-+ +// | | | +=v0192 +// b005 | +-+ +// | | +=v1664 +// b036 | | +-+ +// | | | | +=v0448 +// b080 | | | | +-+ +// | | | | | +=v0512 +// b056 | | | +-+ +// | | | | +=v0704 +// b088 | | | | +-+ +// | | | | | +=v0768 +// b081 | | | +-+ +// | | | +=v0640 +// b022 | | +-+ +// | | | | +=v0576 +// b082 | | | | +-+ +// | | | | | | +=v0832 +// b089 | | | | | +-+ +// | | | | | +=v0896 +// b057 | | | | +-+ +// | | | | | | +=v0960 +// b090 | | | | | | +-+ +// | | | | | | | +=v1024 +// b083 | | | | | +-+ +// | | | | | | +=v1088 +// b091 | | | | | +-+ +// | | | | | +=v1152 +// b037 | | | +-+ +// | | | | +=v1216 +// b092 | | | | +-+ +// | | | | | +=v1280 +// b084 | | | | +-+ +// | | | | | | +=v1344 +// b093 | | | | | +-+ +// | | | | | +=v1408 +// b058 | | | +-+ +// | | | +=v0256 +// b011 | +-+ +// | +=v0002 +// b001 +-+ +// | +=v0003 +// b012 | +-+ +// | | | +=v0128 +// b023 | | +-+ +// | | +=v0008 +// b006 | +-+ +// | | | +=v0009 +// b024 | | | +-+ +// | | | | | +=v0016 +// b038 | | | | +-+ +// | | | | +=v0017 +// b013 | | +-+ +// | | +=v0004 +// b003 +-+ +// | +=v0005 +// b014 | +-+ +// | | | +=v0014 +// b039 | | | +-+ +// | | | | +=v0015 +// b025 | | +-+ +// | | +=v0064 +// b007 +-+ +// | +=v0006 +// b015 +-+ +// +=v0007 var whiteDecodeTable = [...][2]int16{ 0: {0, 0}, 1: {2, 3}, @@ -387,215 +387,215 @@ var whiteDecodeTable = [...][2]int16{ // blackDecodeTable represents Tables 2 and 3 for a black run. // -// +=XXXXX -// b017 +-+ -// | | +=v1792 -// b042 | | +-+ -// | | | | +=v1984 -// b063 | | | +-+ -// | | | +=v2048 -// b029 | | +-+ -// | | | | +=v2112 -// b064 | | | | +-+ -// | | | | | +=v2176 -// b043 | | | +-+ -// | | | | +=v2240 -// b065 | | | +-+ -// | | | +=v2304 -// b022 | +-+ -// | | +=v1856 -// b044 | | +-+ -// | | | +=v1920 -// b030 | +-+ -// | | +=v2368 -// b066 | | +-+ -// | | | +=v2432 -// b045 | +-+ -// | | +=v2496 -// b067 | +-+ -// | +=v2560 -// b013 +-+ -// | | +=v0018 -// b031 | | +-+ -// | | | | +=v0052 -// b068 | | | | +-+ -// | | | | | | +=v0640 -// b095 | | | | | +-+ -// | | | | | +=v0704 -// b046 | | | +-+ -// | | | | +=v0768 -// b096 | | | | +-+ -// | | | | | +=v0832 -// b069 | | | +-+ -// | | | +=v0055 -// b023 | | +-+ -// | | | | +=v0056 -// b070 | | | | +-+ -// | | | | | | +=v1280 -// b097 | | | | | +-+ -// | | | | | +=v1344 -// b047 | | | | +-+ -// | | | | | | +=v1408 -// b098 | | | | | | +-+ -// | | | | | | | +=v1472 -// b071 | | | | | +-+ -// | | | | | +=v0059 -// b032 | | | +-+ -// | | | | +=v0060 -// b072 | | | | +-+ -// | | | | | | +=v1536 -// b099 | | | | | +-+ -// | | | | | +=v1600 -// b048 | | | +-+ -// | | | +=v0024 -// b018 | +-+ -// | | +=v0025 -// b049 | | +-+ -// | | | | +=v1664 -// b100 | | | | +-+ -// | | | | | +=v1728 -// b073 | | | +-+ -// | | | +=v0320 -// b033 | | +-+ -// | | | | +=v0384 -// b074 | | | | +-+ -// | | | | | +=v0448 -// b050 | | | +-+ -// | | | | +=v0512 -// b101 | | | | +-+ -// | | | | | +=v0576 -// b075 | | | +-+ -// | | | +=v0053 -// b024 | +-+ -// | | +=v0054 -// b076 | | +-+ -// | | | | +=v0896 -// b102 | | | +-+ -// | | | +=v0960 -// b051 | | +-+ -// | | | | +=v1024 -// b103 | | | | +-+ -// | | | | | +=v1088 -// b077 | | | +-+ -// | | | | +=v1152 -// b104 | | | +-+ -// | | | +=v1216 -// b034 | +-+ -// | +=v0064 -// b010 +-+ -// | | +=v0013 -// b019 | | +-+ -// | | | | +=v0023 -// b052 | | | | +-+ -// | | | | | | +=v0050 -// b078 | | | | | +-+ -// | | | | | +=v0051 -// b035 | | | | +-+ -// | | | | | | +=v0044 -// b079 | | | | | | +-+ -// | | | | | | | +=v0045 -// b053 | | | | | +-+ -// | | | | | | +=v0046 -// b080 | | | | | +-+ -// | | | | | +=v0047 -// b025 | | | +-+ -// | | | | +=v0057 -// b081 | | | | +-+ -// | | | | | +=v0058 -// b054 | | | | +-+ -// | | | | | | +=v0061 -// b082 | | | | | +-+ -// | | | | | +=v0256 -// b036 | | | +-+ -// | | | +=v0016 -// b014 | +-+ -// | | +=v0017 -// b037 | | +-+ -// | | | | +=v0048 -// b083 | | | | +-+ -// | | | | | +=v0049 -// b055 | | | +-+ -// | | | | +=v0062 -// b084 | | | +-+ -// | | | +=v0063 -// b026 | | +-+ -// | | | | +=v0030 -// b085 | | | | +-+ -// | | | | | +=v0031 -// b056 | | | | +-+ -// | | | | | | +=v0032 -// b086 | | | | | +-+ -// | | | | | +=v0033 -// b038 | | | +-+ -// | | | | +=v0040 -// b087 | | | | +-+ -// | | | | | +=v0041 -// b057 | | | +-+ -// | | | +=v0022 -// b020 | +-+ -// | +=v0014 -// b008 +-+ -// | | +=v0010 -// b015 | | +-+ -// | | | +=v0011 -// b011 | +-+ -// | | +=v0015 -// b027 | | +-+ -// | | | | +=v0128 -// b088 | | | | +-+ -// | | | | | +=v0192 -// b058 | | | | +-+ -// | | | | | | +=v0026 -// b089 | | | | | +-+ -// | | | | | +=v0027 -// b039 | | | +-+ -// | | | | +=v0028 -// b090 | | | | +-+ -// | | | | | +=v0029 -// b059 | | | +-+ -// | | | +=v0019 -// b021 | | +-+ -// | | | | +=v0020 -// b060 | | | | +-+ -// | | | | | | +=v0034 -// b091 | | | | | +-+ -// | | | | | +=v0035 -// b040 | | | | +-+ -// | | | | | | +=v0036 -// b092 | | | | | | +-+ -// | | | | | | | +=v0037 -// b061 | | | | | +-+ -// | | | | | | +=v0038 -// b093 | | | | | +-+ -// | | | | | +=v0039 -// b028 | | | +-+ -// | | | | +=v0021 -// b062 | | | | +-+ -// | | | | | | +=v0042 -// b094 | | | | | +-+ -// | | | | | +=v0043 -// b041 | | | +-+ -// | | | +=v0000 -// b016 | +-+ -// | +=v0012 -// b006 +-+ -// | | +=v0009 -// b012 | | +-+ -// | | | +=v0008 -// b009 | +-+ -// | +=v0007 -// b004 +-+ -// | | +=v0006 -// b007 | +-+ -// | +=v0005 -// b002 +-+ -// | | +=v0001 -// b005 | +-+ -// | +=v0004 -// b001 +-+ -// | +=v0003 -// b003 +-+ -// +=v0002 +// +=XXXXX +// b017 +-+ +// | | +=v1792 +// b042 | | +-+ +// | | | | +=v1984 +// b063 | | | +-+ +// | | | +=v2048 +// b029 | | +-+ +// | | | | +=v2112 +// b064 | | | | +-+ +// | | | | | +=v2176 +// b043 | | | +-+ +// | | | | +=v2240 +// b065 | | | +-+ +// | | | +=v2304 +// b022 | +-+ +// | | +=v1856 +// b044 | | +-+ +// | | | +=v1920 +// b030 | +-+ +// | | +=v2368 +// b066 | | +-+ +// | | | +=v2432 +// b045 | +-+ +// | | +=v2496 +// b067 | +-+ +// | +=v2560 +// b013 +-+ +// | | +=v0018 +// b031 | | +-+ +// | | | | +=v0052 +// b068 | | | | +-+ +// | | | | | | +=v0640 +// b095 | | | | | +-+ +// | | | | | +=v0704 +// b046 | | | +-+ +// | | | | +=v0768 +// b096 | | | | +-+ +// | | | | | +=v0832 +// b069 | | | +-+ +// | | | +=v0055 +// b023 | | +-+ +// | | | | +=v0056 +// b070 | | | | +-+ +// | | | | | | +=v1280 +// b097 | | | | | +-+ +// | | | | | +=v1344 +// b047 | | | | +-+ +// | | | | | | +=v1408 +// b098 | | | | | | +-+ +// | | | | | | | +=v1472 +// b071 | | | | | +-+ +// | | | | | +=v0059 +// b032 | | | +-+ +// | | | | +=v0060 +// b072 | | | | +-+ +// | | | | | | +=v1536 +// b099 | | | | | +-+ +// | | | | | +=v1600 +// b048 | | | +-+ +// | | | +=v0024 +// b018 | +-+ +// | | +=v0025 +// b049 | | +-+ +// | | | | +=v1664 +// b100 | | | | +-+ +// | | | | | +=v1728 +// b073 | | | +-+ +// | | | +=v0320 +// b033 | | +-+ +// | | | | +=v0384 +// b074 | | | | +-+ +// | | | | | +=v0448 +// b050 | | | +-+ +// | | | | +=v0512 +// b101 | | | | +-+ +// | | | | | +=v0576 +// b075 | | | +-+ +// | | | +=v0053 +// b024 | +-+ +// | | +=v0054 +// b076 | | +-+ +// | | | | +=v0896 +// b102 | | | +-+ +// | | | +=v0960 +// b051 | | +-+ +// | | | | +=v1024 +// b103 | | | | +-+ +// | | | | | +=v1088 +// b077 | | | +-+ +// | | | | +=v1152 +// b104 | | | +-+ +// | | | +=v1216 +// b034 | +-+ +// | +=v0064 +// b010 +-+ +// | | +=v0013 +// b019 | | +-+ +// | | | | +=v0023 +// b052 | | | | +-+ +// | | | | | | +=v0050 +// b078 | | | | | +-+ +// | | | | | +=v0051 +// b035 | | | | +-+ +// | | | | | | +=v0044 +// b079 | | | | | | +-+ +// | | | | | | | +=v0045 +// b053 | | | | | +-+ +// | | | | | | +=v0046 +// b080 | | | | | +-+ +// | | | | | +=v0047 +// b025 | | | +-+ +// | | | | +=v0057 +// b081 | | | | +-+ +// | | | | | +=v0058 +// b054 | | | | +-+ +// | | | | | | +=v0061 +// b082 | | | | | +-+ +// | | | | | +=v0256 +// b036 | | | +-+ +// | | | +=v0016 +// b014 | +-+ +// | | +=v0017 +// b037 | | +-+ +// | | | | +=v0048 +// b083 | | | | +-+ +// | | | | | +=v0049 +// b055 | | | +-+ +// | | | | +=v0062 +// b084 | | | +-+ +// | | | +=v0063 +// b026 | | +-+ +// | | | | +=v0030 +// b085 | | | | +-+ +// | | | | | +=v0031 +// b056 | | | | +-+ +// | | | | | | +=v0032 +// b086 | | | | | +-+ +// | | | | | +=v0033 +// b038 | | | +-+ +// | | | | +=v0040 +// b087 | | | | +-+ +// | | | | | +=v0041 +// b057 | | | +-+ +// | | | +=v0022 +// b020 | +-+ +// | +=v0014 +// b008 +-+ +// | | +=v0010 +// b015 | | +-+ +// | | | +=v0011 +// b011 | +-+ +// | | +=v0015 +// b027 | | +-+ +// | | | | +=v0128 +// b088 | | | | +-+ +// | | | | | +=v0192 +// b058 | | | | +-+ +// | | | | | | +=v0026 +// b089 | | | | | +-+ +// | | | | | +=v0027 +// b039 | | | +-+ +// | | | | +=v0028 +// b090 | | | | +-+ +// | | | | | +=v0029 +// b059 | | | +-+ +// | | | +=v0019 +// b021 | | +-+ +// | | | | +=v0020 +// b060 | | | | +-+ +// | | | | | | +=v0034 +// b091 | | | | | +-+ +// | | | | | +=v0035 +// b040 | | | | +-+ +// | | | | | | +=v0036 +// b092 | | | | | | +-+ +// | | | | | | | +=v0037 +// b061 | | | | | +-+ +// | | | | | | +=v0038 +// b093 | | | | | +-+ +// | | | | | +=v0039 +// b028 | | | +-+ +// | | | | +=v0021 +// b062 | | | | +-+ +// | | | | | | +=v0042 +// b094 | | | | | +-+ +// | | | | | +=v0043 +// b041 | | | +-+ +// | | | +=v0000 +// b016 | +-+ +// | +=v0012 +// b006 +-+ +// | | +=v0009 +// b012 | | +-+ +// | | | +=v0008 +// b009 | +-+ +// | +=v0007 +// b004 +-+ +// | | +=v0006 +// b007 | +-+ +// | +=v0005 +// b002 +-+ +// | | +=v0001 +// b005 | +-+ +// | +=v0004 +// b001 +-+ +// | +=v0003 +// b003 +-+ +// +=v0002 var blackDecodeTable = [...][2]int16{ 0: {0, 0}, 1: {2, 3}, diff --git a/vendor/golang.org/x/image/tiff/lzw/reader.go b/vendor/golang.org/x/image/tiff/lzw/reader.go index 78204ba92..1ccf5858a 100644 --- a/vendor/golang.org/x/image/tiff/lzw/reader.go +++ b/vendor/golang.org/x/image/tiff/lzw/reader.go @@ -3,8 +3,8 @@ // license that can be found in the LICENSE file. // Package lzw implements the Lempel-Ziv-Welch compressed data format, -// described in T. A. Welch, ``A Technique for High-Performance Data -// Compression'', Computer, 17(6) (June 1984), pp 8-19. +// described in T. A. Welch, “A Technique for High-Performance Data +// Compression”, Computer, 17(6) (June 1984), pp 8-19. // // In particular, it implements LZW as used by the TIFF file format, including // an "off by one" algorithmic difference when compared to standard LZW. @@ -30,7 +30,7 @@ Aldus "off by one" algorithm. The Go code doesn't read (invalid) TIFF files written by old versions of libtiff, but the LZW algorithm in this package still differs from the one in -Go's standard package library to accomodate this "off by one" in valid TIFFs. +Go's standard package library to accommodate this "off by one" in valid TIFFs. */ import ( diff --git a/vendor/golang.org/x/image/tiff/reader.go b/vendor/golang.org/x/image/tiff/reader.go index de73f4b99..45cc056f4 100644 --- a/vendor/golang.org/x/image/tiff/reader.go +++ b/vendor/golang.org/x/image/tiff/reader.go @@ -38,6 +38,52 @@ func (e UnsupportedError) Error() string { var errNoPixels = FormatError("not enough pixel data") +const maxChunkSize = 10 << 20 // 10M + +// safeReadtAt is a verbatim copy of internal/saferio.ReadDataAt from the +// standard library, which is used to read data from a reader using a length +// provided by untrusted data, without allocating the entire slice ahead of time +// if it is large (>maxChunkSize). This allows us to avoid allocating giant +// slices before learning that we can't actually read that much data from the +// reader. +func safeReadAt(r io.ReaderAt, n uint64, off int64) ([]byte, error) { + if int64(n) < 0 || n != uint64(int(n)) { + // n is too large to fit in int, so we can't allocate + // a buffer large enough. Treat this as a read failure. + return nil, io.ErrUnexpectedEOF + } + + if n < maxChunkSize { + buf := make([]byte, n) + _, err := r.ReadAt(buf, off) + if err != nil { + // io.SectionReader can return EOF for n == 0, + // but for our purposes that is a success. + if err != io.EOF || n > 0 { + return nil, err + } + } + return buf, nil + } + + var buf []byte + buf1 := make([]byte, maxChunkSize) + for n > 0 { + next := n + if next > maxChunkSize { + next = maxChunkSize + } + _, err := r.ReadAt(buf1[:next], off) + if err != nil { + return nil, err + } + buf = append(buf, buf1[:next]...) + n -= next + off += int64(next) + } + return buf, nil +} + type decoder struct { r io.ReaderAt byteOrder binary.ByteOrder @@ -82,8 +128,7 @@ func (d *decoder) ifdUint(p []byte) (u []uint, err error) { } if datalen := lengths[datatype] * count; datalen > 4 { // The IFD contains a pointer to the real value. - raw = make([]byte, datalen) - _, err = d.r.ReadAt(raw, int64(d.byteOrder.Uint32(p[8:12]))) + raw, err = safeReadAt(d.r, uint64(datalen), int64(d.byteOrder.Uint32(p[8:12]))) } else { raw = p[8 : 8+datalen] } @@ -427,8 +472,9 @@ func newDecoder(r io.Reader) (*decoder, error) { numItems := int(d.byteOrder.Uint16(p[0:2])) // All IFD entries are read in one chunk. - p = make([]byte, ifdLen*numItems) - if _, err := d.r.ReadAt(p, ifdOffset+2); err != nil { + var err error + p, err = safeReadAt(d.r, uint64(ifdLen*numItems), ifdOffset+2) + if err != nil { return nil, err } @@ -656,8 +702,7 @@ func Decode(r io.Reader) (img image.Image, err error) { if b, ok := d.r.(*buffer); ok { d.buf, err = b.Slice(int(offset), int(n)) } else { - d.buf = make([]byte, n) - _, err = d.r.ReadAt(d.buf, offset) + d.buf, err = safeReadAt(d.r, uint64(n), offset) } case cG3: inv := d.firstVal(tPhotometricInterpretation) == pWhiteIsZero diff --git a/vendor/golang.org/x/image/tiff/writer.go b/vendor/golang.org/x/image/tiff/writer.go index c8a01cea7..4272c5aa0 100644 --- a/vendor/golang.org/x/image/tiff/writer.go +++ b/vendor/golang.org/x/image/tiff/writer.go @@ -8,6 +8,7 @@ import ( "bytes" "compress/zlib" "encoding/binary" + "errors" "image" "io" "sort" @@ -338,6 +339,8 @@ func Encode(w io.Writer, m image.Image, opt *Options) error { } case cDeflate: dst = zlib.NewWriter(&buf) + default: + return errors.New("tiff: unsupported compression") } pr := uint32(prNone) diff --git a/vendor/golang.org/x/net/bpf/vm_instructions.go b/vendor/golang.org/x/net/bpf/vm_instructions.go index cf8947c33..0aa307c06 100644 --- a/vendor/golang.org/x/net/bpf/vm_instructions.go +++ b/vendor/golang.org/x/net/bpf/vm_instructions.go @@ -94,7 +94,7 @@ func jumpIfCommon(cond JumpTest, skipTrue, skipFalse uint8, regA uint32, value u func loadAbsolute(ins LoadAbsolute, in []byte) (uint32, bool) { offset := int(ins.Off) - size := int(ins.Size) + size := ins.Size return loadCommon(in, offset, size) } @@ -121,7 +121,7 @@ func loadExtension(ins LoadExtension, in []byte) uint32 { func loadIndirect(ins LoadIndirect, in []byte, regX uint32) (uint32, bool) { offset := int(ins.Off) + int(regX) - size := int(ins.Size) + size := ins.Size return loadCommon(in, offset, size) } diff --git a/vendor/golang.org/x/net/html/parse.go b/vendor/golang.org/x/net/html/parse.go index 038941d70..46a89eda6 100644 --- a/vendor/golang.org/x/net/html/parse.go +++ b/vendor/golang.org/x/net/html/parse.go @@ -184,7 +184,7 @@ func (p *parser) clearStackToContext(s scope) { } } -// parseGenericRawTextElements implements the generic raw text element parsing +// parseGenericRawTextElement implements the generic raw text element parsing // algorithm defined in 12.2.6.2. // https://html.spec.whatwg.org/multipage/parsing.html#parsing-elements-that-contain-only-text // TODO: Since both RAWTEXT and RCDATA states are treated as tokenizer's part @@ -734,7 +734,7 @@ func inHeadIM(p *parser) bool { return false } -// 12.2.6.4.5. +// Section 12.2.6.4.5. func inHeadNoscriptIM(p *parser) bool { switch p.tok.Type { case DoctypeToken: diff --git a/vendor/golang.org/x/net/html/render.go b/vendor/golang.org/x/net/html/render.go index b46d81ca6..497e13204 100644 --- a/vendor/golang.org/x/net/html/render.go +++ b/vendor/golang.org/x/net/html/render.go @@ -85,7 +85,7 @@ func render1(w writer, n *Node) error { if _, err := w.WriteString(""); err != nil { @@ -96,7 +96,7 @@ func render1(w writer, n *Node) error { if _, err := w.WriteString("" case CommentToken: - return "" + return "" case DoctypeToken: - return "" + return "" } return "Invalid(" + strconv.Itoa(int(t.Type)) + ")" } @@ -598,6 +598,11 @@ scriptDataDoubleEscapeEnd: // readComment reads the next comment token starting with "") return } @@ -628,19 +632,52 @@ func (z *Tokenizer) readComment() { if dashCount >= 2 { c = z.readByte() if z.err != nil { - z.data.end = z.raw.end + z.data.end = z.calculateAbruptCommentDataEnd() return - } - if c == '>' { + } else if c == '>' { z.data.end = z.raw.end - len("--!>") return + } else if c == '-' { + dashCount = 1 + beginning = false + continue } } } dashCount = 0 + beginning = false } } +func (z *Tokenizer) calculateAbruptCommentDataEnd() int { + raw := z.Raw() + const prefixLen = len("