diff --git a/.cargo/config.toml b/.cargo/config.toml index 725804aba..9e3acdb03 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,10 +1,9 @@ [alias] -prisma = "run --package prisma --" +prisma = "run --package prisma-cli --" integration-tests = "test --package integration-tests -- --test-threads 1" doc-tests = "cargo test --doc -- --show-output" build-server = "build --package stump_server --bin stump_server --release --" -# this caused me such an unbearable headache... [target.x86_64-apple-darwin] rustflags = [ "-C", "link-arg=-undefined", diff --git a/.dockerignore b/.dockerignore index 3ae4abc41..7618ef07d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,10 +5,9 @@ dist static apps/server/client target -*.lock -*-lock.* *.log *.db +.git # ignore contents in excess directories, some are kept only so cargo # doesn't yell at me diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..65ecf1429 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +node_modules/ +*.min.js +*.map +*.snap +**/prisma/src/db.ts +**/prisma-cli/** +**/dist/** \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..4dd73efb1 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,55 @@ +module.exports = { + env: { + browser: true, + node: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'prettier', + ], + overrides: [ + { + files: ['*.jsx', '*.tsx', '*.ts'], + rules: { + '@typescript-eslint/no-non-null-assertion': 'off', + 'no-console': ['error', { allow: ['warn', 'error', 'debug'] }], + 'react/react-in-jsx-scope': 'off', + }, + }, + { + files: ['*.config.js', '.eslintrc.js'], + rules: { + '@typescript-eslint/no-var-requires': 'off', + 'import/no-commonjs': 'off', + 'sort-keys': 'off', + 'unicorn/prefer-module': 'off', + }, + }, + ], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + project: 'tsconfig.eslint.json', + tsconfigRootDir: __dirname, + }, + plugins: ['@typescript-eslint', 'simple-import-sort', 'prettier', 'sort-keys-fix', 'react'], + root: true, + rules: { + 'import/no-unresolved': 'off', + 'import/no-useless-path-segments': 'off', + 'no-console': 'error', + 'simple-import-sort/exports': 'error', + 'simple-import-sort/imports': 'error', + 'sort-keys-fix/sort-keys-fix': 'warn', + }, + settings: { + react: { + version: 'detect', + }, + }, +} diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index b1efdccc0..000000000 --- a/.gitattributes +++ /dev/null @@ -1,9 +0,0 @@ -# ignore markdown files, there's just gonna be so many of them lol -*.md linguist-detectable=false -# ignore json files, as languages get added there will be *way* too many -*.json linguist-detectable=false -# ignore sql files, migrations get added there will be *way* too many -*.sql linguist-detectable=false - -# I don't want website to be included in language stats -apps/website/** linguist-vendored \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md similarity index 87% rename from CODE_OF_CONDUCT.md rename to .github/CODE_OF_CONDUCT.md index 746cbc855..fdc5b3c17 100644 --- a/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..80fbabd6f --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,72 @@ +# Contributing + +You can contribute by opening new issues or PRs. Do not make random PRs without talking with me first - to ensure nobody's time and effort is wasted, please follow the guidelines below. + +## Guidelines + +1. Check to see if an issue already exists relevant to your feature/topic +2. Create an issue (if an issue does not already exist) and express interest in working it (see the [issues](#issues) section below) +3. Create a fork of the repository. +4. Create a new feature branch **off of the `develop` branch** - _not_ `main`. +5. Add appropiate documentation, tests, etc, if necessary. +6. Ensure you have your code formatters properly configured (both Prettier and Rustfmt). +7. Once you've completed your changes, create a PR from your feature branch into `develop` - _not_ `main`. +8. Be sure to update your feature branch with any new changes **before** making the PR. +9. Follow the PR naming format to help ensure the changelog generator properly picks up your additions: + + ``` + : + ``` + + Where `type` is one of the following: + + - `feat`: A new feature + - `fix`: A bug fix + - `docs`: Documentation only changes + - `refactor`: A code change that neither fixes a bug nor adds a feature + - `perf`: A code change that improves performance + - `test`: Adding missing tests or correcting existing tests + - `ci`: Changes to our CI configuration files and scripts + - `chore`: Other changes that don't modify `src` or `test` files, such as updating `package.json` or `README.md` + - `revert`: Reverts a previous commit + - `WIP`: Work in progress + + The `description` should contain a _succinct_ description of the change: + + - use the imperative, present tense: "change" not "changed" nor "changes" + - don't capitalize the first letter + - no dot (.) at the end + + Examples: + + ``` + feat: add support for Reading Lists + fix: remove broken link + docs: update CONTRIBUTING.md to include PR naming format + ``` + +10. Stick around and make sure your PR passes all checks and gets merged! + +## Issues + +I don't have any strict guidelines for issues, but do please try and be as descriptive as possible. There are a few templates to help you out, but in general: + +- If you're interested in working on an issue, please leave a comment so that I know you're interested. +- If you're opening an issue to request a feature, please try and explain why you think it would be a good addition to the project. If applicable, include example use cases. +- If you're opening an issue to report a bug, try to fill in the template as best you can. If you can, please include a minimal reproduction of the bug (video, code, etc). +- If you're not sure if an issue is relevant appropriate, e.g. if you have more of a question to ask, feel free to pop in the [Discord server](https://discord.gg/63Ybb7J3as) and ask! + +**Please don't ghost an issue you've been assigned** - if you're no longer interested in working on it, that is totally okay! Just leave a comment on the issue so that I know you're no longer interested and I can reassign it to someone else. I will never be offended if you no longer want to work on an issue - I'm just trying to make sure that nobody's time and effort is wasted. + +## A note on merging + +I will not merge your PR until: + +- It aligns with the [guidelines](#guidelines) outlined above + - In most cases, any issues outside of a malformed PR name, I will not fix for you. If you're unsure how to fix it, ask for help. Stale PRs will be closed after 10 days. +- All checks pass +- At least one maintainer has reviewed your PR + +All PRs to `develop` will be squashed. All PRs to `main` will be merge commits. This is to ensure that the commit history is clean and easy to follow, and to ensure that the changelog generator works properly. + +Thanks for considering to contribute! :heart: diff --git a/.github/CONTRIBUTORS.md b/.github/CONTRIBUTORS.md deleted file mode 100644 index a3011f921..000000000 --- a/.github/CONTRIBUTORS.md +++ /dev/null @@ -1,23 +0,0 @@ -## Contributors - -I've created this file as a way to keep track of all of the no-code contributions Stump might recieve. - -If you have contributed to Stump in any non-code way, please add your name to the list below following these guidelines: - -- Please place your name in the corresponding category, according to what you contributed. -- If a category does not exist that suits your contribution, please feel free to create one. -- If you have contributed to Stump in multiple categories, please add your name to each category you contributed to. -- If a single contribution encompasses multiple categories, please select the category that best suits your contribution. **Don't add your name to multiple categories for a single contribution.** -- Please keep the list in alphabetical order and following the general format of: - - [Your Name](GitHub Profile Link / Wherever You Want People To Go To Find Out More About You) - Description of contribution - -**Thank you for contributing to Stump!** - -## UI Design - -> This category is for people who have helped design the user interface of Stump. This can be things like designing a new page or laying out the UX for a new feature. Assets like figma files and mockups won't be stored on the repo, but if you'd like to share them please feel free to link them in your entry here. - -## Planning and Organization - -> This category is for people who have helped plan and organize Stump. This can be things like solving a problem Stump faces during a solutioning / planning session, without actually coding anything. Typically, this will be reserved for those who take on `investigate` issues. diff --git a/.github/actions/build-desktop/action.yml b/.github/actions/build-desktop/action.yml new file mode 100644 index 000000000..615d3cb8e --- /dev/null +++ b/.github/actions/build-desktop/action.yml @@ -0,0 +1,37 @@ +name: 'Build Stump desktop app' +description: 'Compile the Stump desktop app' + +inputs: + platform: + description: 'The plaform of the runner' + required: true + +runs: + using: composite + steps: + - name: Checkout project + uses: actions/checkout@v3 + + # - name: Configure environment + # run: | + # if [[ ${{ inputs.platform }} == 'linux' || ${{ inputs.platform }} == 'windows' ]]; then + # echo "RUN_SETUP=false" >> $GITHUB_ENV + # else + # echo "RUN_SETUP=true" >> $GITHUB_ENV + # fi + + - name: Setup rust + uses: ./.github/actions/setup-cargo + + - name: Generate Prisma client + uses: ./.github/actions/setup-prisma + + - name: Copy bundled web app + uses: actions/download-artifact@v3 + with: + name: webapp + path: ./apps/desktop/dist + + - name: Compile desktop app + shell: bash + run: cargo build --package stump_desktop --release diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml new file mode 100644 index 000000000..3b56ba511 --- /dev/null +++ b/.github/actions/build-docker/action.yml @@ -0,0 +1,108 @@ +name: 'Build docker image' +description: 'Build and load or push a tagged docker image for stump' + +inputs: + username: + description: 'Username for docker login' + required: true + password: + description: 'Token for docker login' + required: true + load: + description: 'Set output-type to docker' + default: 'true' + push: + description: 'Set output-type to registry' + default: 'false' + tags: + description: 'List of tags to assigned to the image' + default: 'nightly' + platforms: + description: 'List of platforms to build' + required: true + discord-webhook: + description: 'Discord webhook to send notifications to' + required: true + +runs: + using: composite + steps: + - name: Get commit short sha + run: echo "GIT_REV=$(git rev-parse --short "$GITHUB_SHA")" >> $GITHUB_ENV + shell: bash + + - name: Format tags + run: | + echo "TAGS=$(echo ${{ inputs.tags }} | sed -e 's/,/,aaronleopold\/stump:/g' | sed -e 's/^/aaronleopold\/stump:/')" >> $GITHUB_ENV + shell: bash + + - name: Setup rust + uses: ./.github/actions/setup-cargo + + - name: Generate Prisma client + uses: ./.github/actions/setup-prisma + + # TODO: uncomment once cache stuff is resolved... + # - name: Setup Docker layers cache + # uses: actions/cache@v3 + # with: + # path: /tmp/.buildx-cache + # key: ${{ runner.os }}-buildx-${{ github.sha }} + # restore-keys: | + # ${{ runner.os }}-buildx- + + # We only need QEMU when an arm* platform is targeted + - name: Check QEMU requirement + id: check-qemu + run: | + if [[ ${{ inputs.platforms }} == *"arm"* ]]; then + echo "SETUP_QEMU=1" >> $GITHUB_OUTPUT + else + echo "SETUP_QEMU=0" >> $GITHUB_OUTPUT + fi + shell: bash + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + if: ${{ steps.check-qemu.outputs.SETUP_QEMU == '1' }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ inputs.username }} + password: ${{ inputs.password }} + + - name: Run buildx build + uses: docker/build-push-action@v4 + with: + context: . + build-args: | + "GIT_REV=${{ env.GIT_REV }}" + file: scripts/release/Dockerfile + platforms: ${{ inputs.platforms }} + load: ${{ inputs.load }} + push: ${{ inputs.push }} + tags: ${{ env.TAGS }} + # TODO: uncomment once cache stuff is resolved... + # cache-from: type=local,src=/tmp/.buildx-cache + # cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + + # https://github.com/docker/build-push-action/issues/252 + # TODO: https://github.com/moby/buildkit/issues/1896 + # TODO: uncomment once cache stuff is resolved... + # - name: Move buildx cache + # run: | + # rm -rf /tmp/.buildx-cache + # mv /tmp/.buildx-cache-new /tmp/.buildx-cache + # shell: bash + + - name: Discord notification + if: ${{ success() && inputs.push == 'true' }} + env: + DISCORD_WEBHOOK: ${{ inputs.discord-webhook }} + uses: 'Ilshidur/action-discord@0.3.2' + with: + args: 'Successfully pushed the following image tags to registry: ${{ env.TAGS }}' diff --git a/.github/actions/build-server/action.yml b/.github/actions/build-server/action.yml new file mode 100644 index 000000000..f8a96739a --- /dev/null +++ b/.github/actions/build-server/action.yml @@ -0,0 +1,37 @@ +name: 'Build Stump server' +description: 'Compile the Stump Rust server' + +inputs: + platform: + description: 'The plaform of the runner' + required: true + +runs: + using: composite + steps: + - name: Checkout project + uses: actions/checkout@v3 + + # - name: Configure environment + # run: | + # if [[ ${{ inputs.platform }} == 'linux' || ${{ inputs.platform }} == 'windows' ]]; then + # echo "RUN_SETUP=false" >> $GITHUB_ENV + # else + # echo "RUN_SETUP=true" >> $GITHUB_ENV + # fi + + - name: Setup rust + uses: ./.github/actions/setup-cargo + + - name: Generate Prisma client + uses: ./.github/actions/setup-prisma + + - name: Copy bundled web app + uses: actions/download-artifact@v3 + with: + name: webapp + path: ./apps/server/dist + + - name: Compile server + shell: bash + run: cargo build --package stump_server --release diff --git a/.github/actions/build-web/action.yml b/.github/actions/build-web/action.yml new file mode 100644 index 000000000..9a39f4517 --- /dev/null +++ b/.github/actions/build-web/action.yml @@ -0,0 +1,27 @@ +name: 'Compile Web Application' +description: 'Compile stump web' + +runs: + using: composite + steps: + - name: Checkout project + uses: actions/checkout@v3 + + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm + + - name: Install dependencies + shell: bash + run: pnpm install + working-directory: apps/web + + - name: Build app + shell: bash + run: pnpm run build + working-directory: apps/web + + - name: Upload bundle + uses: ./.github/actions/upload-artifact + with: + upload-name: webapp + upload-path: apps/web/dist diff --git a/.github/actions/setup-cargo/action.yml b/.github/actions/setup-cargo/action.yml new file mode 100644 index 000000000..84bf59567 --- /dev/null +++ b/.github/actions/setup-cargo/action.yml @@ -0,0 +1,45 @@ +name: 'Setup system dependencies' +description: 'Install system dependencies and setup cache' + +runs: + using: 'composite' + steps: + - name: Configure environment + run: | + if [[ ${{ runner.name }} == 'manjaro-az' || ${{ runner.os }} == 'Windows' ]]; then + echo "RUN_SETUP=false" >> $GITHUB_ENV + else + echo "RUN_SETUP=true" >> $GITHUB_ENV + fi + shell: bash + + - name: System setup + if: ${{ env.RUN_SETUP == 'true' }} + shell: bash + run: CHECK_NODE=0 CHECK_CARGO=0 DEV_SETUP=0 ./scripts/system-setup.sh + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v3 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v3 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v3 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} diff --git a/.github/actions/setup-pnpm/action.yaml b/.github/actions/setup-pnpm/action.yaml new file mode 100644 index 000000000..a21fae754 --- /dev/null +++ b/.github/actions/setup-pnpm/action.yaml @@ -0,0 +1,30 @@ +name: PNPM Setup +description: Setup PNPM and cache PNPM dependencies +runs: + using: 'composite' + steps: + - uses: pnpm/action-setup@v2 + name: Install pnpm + id: pnpm-install + with: + version: 7 + run_install: false + + - name: Get pnpm store directory + id: pnpm-cache-store + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v3 + name: Setup pnpm cache + id: pnpm-cache + with: + path: ${{ steps.pnpm-cache-store.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + shell: bash + run: pnpm install --no-frozen-lockfile diff --git a/.github/actions/setup-prisma/action.yaml b/.github/actions/setup-prisma/action.yaml new file mode 100644 index 000000000..1f9d7ded9 --- /dev/null +++ b/.github/actions/setup-prisma/action.yaml @@ -0,0 +1,17 @@ +name: Prisma Setup +description: Generate/cache Prisma client +runs: + using: 'composite' + steps: + - name: Cache Prisma client + id: cache-prisma + uses: actions/cache@v3 + with: + path: core/src/prisma.rs + key: ${{ runner.os }}-prisma-${{ hashFiles('**/schema.prisma') }} + + - name: Generate Prisma client + # working-directory: core + if: steps.cache-prisma.outputs.cache-hit != 'true' + shell: bash + run: cargo prisma generate --schema=./core/prisma/schema.prisma diff --git a/.github/actions/upload-artifact/action.yml b/.github/actions/upload-artifact/action.yml new file mode 100644 index 000000000..c7a0beb51 --- /dev/null +++ b/.github/actions/upload-artifact/action.yml @@ -0,0 +1,29 @@ +name: 'Upload Local' +description: 'Upload artifact to local action' + +inputs: + upload-name: + required: true + description: 'Name of the upload' + upload-path: + required: true + description: 'Path to the upload data' + +runs: + using: 'composite' + steps: + # https://github.com/actions/upload-artifact/issues/337 + - name: Normalize + if: ${{ runner.os == 'Windows' }} + shell: bash + id: normalize + run: | + UPLOAD_PATH=$(cygpath -w ${{ inputs.upload-path }}) + echo "normalized_path=$UPLOAD_PATH" >> $GITHUB_OUTPUT + + - name: Upload + uses: actions/upload-artifact@v2 + with: + name: ${{ inputs.upload-name }} + path: ${{ env.normalized_path || inputs.upload-path }} + retention-days: 1 diff --git a/.github/workflows/build_nix.yml b/.github/workflows/build_nix.yml new file mode 100644 index 000000000..af420f537 --- /dev/null +++ b/.github/workflows/build_nix.yml @@ -0,0 +1,19 @@ +name: 'Stump Nix CI' + +on: + push: + paths: + - flake.nix + - flake.lock + - .github/workflows/build_nix.yml + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: cachix/install-nix-action@v17 + - name: test + run: nix develop --command "pkg-config" "--libs" "--cflags" "gdk-3.0" "gdk-3.0 >= 3.22" + # - name: Building package + # run: nix develop --command pnpm core run setup && cargo check diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..24a022a70 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,47 @@ +name: 'Stump Checks CI' + +on: + pull_request: + push: + branches: + - main + +# TODO: figure out how to use moon here. +jobs: + check-rust: + name: Rust checks + runs-on: [self-hosted] + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup cargo + uses: ./.github/actions/setup-cargo + + - name: Setup prisma + uses: ./.github/actions/setup-prisma + + - name: Run cargo checks + run: | + cargo fmt --all -- --check + cargo clippy -- -D warnings + # TODO: fix the tests, then uncomment this + # - name: Run tests + # run: | + # cargo integration-tests + + check-typescript: + name: TypeScript checks + runs-on: [self-hosted] + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup PNPM and TypeScript + uses: ./.github/actions/setup-pnpm + + - name: Run TypeScript lints + run: pnpm lint + + # - name: typecheck + # run: pnpm moon run :typecheck diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 000000000..24ed74972 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,173 @@ +name: 'Stump Nightly CI' + +on: + pull_request: + branches: + - develop + - main + push: + branches: + - develop + +# TODO: should I push nightly on main pushes? then on tag, an actual tagged release? + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + +jobs: + docker-build: + name: Build docker image + runs-on: [self-hosted] + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # On PRs, we will only load the image into docker for the quickest platform + # (i.e. linux/amd64). This is mostly a smoke test, just rather ignorant verification + # that the image can be built. On pushes, we will actually build and push for + # all supported platforms. + - name: Configure environment + run: | + echo "LOAD=${{ github.event_name == 'pull_request' }}" >> $GITHUB_ENV + echo "PUSH=${{ github.event_name == 'push' }}" >> $GITHUB_ENV + + if [[ ${{ github.event_name }} == 'pull_request' ]]; then + echo "PLATFORMS=${{ vars.SUPPORTED_PR_DOCKER_PLATFORMS }}" >> $GITHUB_ENV + else + echo "PLATFORMS=${{ vars.SUPPORTED_DOCKER_PLATFORMS }}" >> $GITHUB_ENV + fi + + - name: Setup and build docker image + uses: ./.github/actions/build-docker + with: + username: ${{ env.DOCKER_USERNAME }} + password: ${{ env.DOCKER_PASSWORD }} + tags: 'nightly' + load: ${{ env.LOAD }} + push: ${{ env.PUSH }} + platforms: ${{ env.PLATFORMS }} + discord-webhook: ${{ secrets.DISCORD_WEBHOOK }} + + # TODO: build executables for apple(x86_64,darwin?),linux(x86_64,arm64?), and windows(x86_64) + # These should be uploaded to the nightly release as artifacts. Old artifacts should be deleted + # before uploading new ones. + + build-web: + name: Bundle web app + runs-on: [self-hosted] + if: false # TODO: don't do that + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Build web + uses: ./.github/actions/build-web + + - name: Upload web build + uses: ./.github/actions/upload-artifact + with: + upload-name: webapp + upload-path: apps/web/dist + + build-linux-server: + name: Compile server app (self-hosted linux) + needs: build-web + runs-on: [self-hosted] + if: false # TODO: don't do that + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Build server + uses: ./.github/actions/build-server + with: + platform: 'linux' + + - name: Upload stump server + uses: ./.github/actions/upload-artifact + with: + upload-name: stump_server-linux + upload-path: target/release/stump_server + + build-server: + strategy: + fail-fast: true + matrix: + platform: [macos, windows] + name: Compile server app + needs: build-web + runs-on: ${{ matrix.platform }} + if: false # TODO: don't do that + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Build server + uses: ./.github/actions/build-server + with: + platform: ${{ matrix.platform }} + + - name: Upload stump server + uses: ./.github/actions/upload-artifact + with: + upload-name: stump_server-${{ matrix.platform }} + upload-path: target/release/stump_server + + # build-linux-desktop: + # name: Compile desktop app (self-hosted linux) + # needs: build-web + # runs-on: [self-hosted] + # if: false # TODO: don't do that + # steps: + # - name: Checkout repository + # uses: actions/checkout@v3 + + # - name: Build desktop + # uses: ./.github/actions/build-desktop + # with: + # platform: 'linux' + + # - name: Upload desktop + # uses: ./.github/actions/upload-artifact + # with: + # upload-name: stump-desktop-linux + # upload-path: target/release/bundle + + build-desktop: + strategy: + fail-fast: true + matrix: + platform: [macos, windows] + name: Compile desktop app + needs: build-web + runs-on: ${{ matrix.platform }} + if: false # TODO: don't do that + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Build desktop + uses: ./.github/actions/build-desktop + with: + platform: ${{ matrix.platform }} + + # https://github.com/tauri-apps/tauri-action + # - uses: tauri-apps/tauri-action@v0 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # tagName: stump-desktop-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version + # releaseName: 'Stump Desktop v__VERSION__' + # releaseBody: 'See the assets to download this version and install.' + # releaseDraft: true + # prerelease: true + + # - name: Upload desktop + # uses: ./.github/actions/upload-artifact + # with: + # upload-name: stump-desktop-${{ matrix.platform }} + # upload-path: target/release/bundle diff --git a/.gitignore b/.gitignore index 71ed0038e..f08f63794 100644 --- a/.gitignore +++ b/.gitignore @@ -1,46 +1,55 @@ -target +# OS +.DS_Store -# web -node_modules -.env -.env.* -!.env.template -$houdini +# Logs +logs/ *.log + +# Cache .eslintcache -dist -build +.idea +.npm +.vscode/**/* +!.vscode/settings.json +!.vscode/extensions.json +!.vscode/*.todo + +# Directories +build/ +coverage/ +cjs/ +**/*/dist/**/* +!**/*/dist/.placeholder +dts/ +esm/ +lib/ +mjs/ +tmp/ +umd/ +node_modules/ +target/ + +# Custom +*.min.js +*.map +*.tsbuildinfo +docker-compose.yaml # rust +core/integration-tests/.* +core/integration-tests/*libraries* static -target apps/server/client/* !apps/server/client/.placeholder *.db* *.sqlite* *prisma.rs* -core/integration-tests/*libraries* - -core/integration-tests/.* -# os -.DS_Store - -# editors / idea -.idea - -.next - -# Local Netlify folder -.netlify +# Moon +.moon/cache +.moon/dts +.moon/docker -# vercel -.vercel - -# typescript -*.tsbuildinfo - -_next - -docker-compose.yaml -server_old \ No newline at end of file +# nix +.envrc +.direnv \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit index 20030e549..fb83c8058 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,6 +1,9 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npx lint-staged -pnpm clippy -pnpm checks \ No newline at end of file +# npx lint-staged +moon run :format +moon run :lint +# TODO: fix typecheck +# moon run :typecheck +# cargo clippy --all-targets --workspace -- -D warnings \ No newline at end of file diff --git a/.moon/project.yml b/.moon/project.yml new file mode 100644 index 000000000..eecec7f76 --- /dev/null +++ b/.moon/project.yml @@ -0,0 +1,82 @@ +# https://moonrepo.dev/docs/config/global-project +$schema: 'https://moonrepo.dev/schemas/global-project.json' + +fileGroups: + # Application specific files + app: [] + configs: + - '*.{js,json,yml,yaml}' + sources: + - 'public/**/*' + - 'src/**/*' + - 'types/**/*' + typescript: + - 'public/**/*' + - 'src/**/*' + - 'types/**/*' + rust: + - 'src/**/*.rs' + tests: + - 'tests/**/*.test.*' + - 'tests/**/*.stories.*' + - '**/__tests__/**/*' + assets: + - 'assets/**/*' + - 'images/**/*' + - 'static/**/*' + - '**/*.{scss,css}' + - '**/*.{md,mdx}' + +tasks: + format: + command: + - 'prettier' + - '--config' + - '@in(4)' + - '--ignore-path' + - '@in(3)' + - '--write' + - '.' + inputs: + - 'src/**/*' + - 'tests/**/*' + - '**/*.{md,mdx,yml,yaml,json}' + - '/.prettierignore' + - '/.prettierrc' + + lint: + command: + - 'eslint' + - '--ext' + - '.ts,.tsx,.cts,.mts,.js,.jsx,.cjs,.mjs' + - '--fix' + - '--report-unused-disable-directives' + - '--no-error-on-unmatched-pattern' + - '--exit-on-fatal-error' + - '--ignore-path' + - '@in(2)' + - '.' + inputs: + - '*.config.*' + - '**/.eslintrc.*' + - '/.eslintignore' + - '/.eslintrc.*' + - 'tsconfig.json' + - '/tsconfig.eslint.json' + - '/tsconfig.options.json' + - '@group(app)' + - '@globs(sources)' + - '@globs(tests)' + + typecheck: + command: + - 'tsc' + - '--build' + - '--verbose' + inputs: + - '@group(app)' + - '@globs(sources)' + - '@globs(tests)' + - 'tsconfig.json' + - 'tsconfig.*.json' + - '/tsconfig.options.json' diff --git a/.moon/toolchain.yml b/.moon/toolchain.yml new file mode 100644 index 000000000..e9278c29f --- /dev/null +++ b/.moon/toolchain.yml @@ -0,0 +1,64 @@ +# https://moonrepo.dev/docs/config/toolchain +$schema: 'https://moonrepo.dev/schemas/toolchain.json' + +# Extend and inherit an external configuration file. Must be a valid HTTPS URL or file system path. +# extends: './shared/toolchain.yml' + +# Configures Node.js within the toolchain. moon manages its own version of Node.js +# instead of relying on a version found on the host machine. This ensures deterministic +# and reproducible builds across any machine. +node: + # The version to use. Must be a semantic version that includes major, minor, and patch. + # We suggest using the latest active LTS version: https://nodejs.org/en/about/releases + version: '18.12.0' + packageManager: 'pnpm' + pnpm: + version: '7.18.2' + # Add `node.version` as a constraint in the root `package.json` `engines`. + addEnginesConstraint: true + # Use the `package.json` name as an alias for the respective moon project. + aliasPackageNames: 'name-and-scope' + # Dedupe dependencies after the lockfile has changed. + dedupeOnLockfileChange: false + + # Version format to use when syncing dependencies within the project's `package.json`. + # dependencyVersionFormat: 'workspace' + + # Infer and automatically create moon tasks from `package.json` scripts, per project. + # BEWARE: Tasks and scripts are not 1:1 in functionality, so please refer to the documentation. + inferTasksFromScripts: false + + # Sync a project's `dependsOn` as dependencies within the project's `package.json`. + syncProjectWorkspaceDependencies: true + + # Sync `node.version` to a 3rd-party version manager's config file. + # Accepts "nodenv" (.node-version), "nvm" (.nvmrc), or none. + # syncVersionManagerConfig: 'nvm' + +# Configures how moon integrates with TypeScript. +typescript: + # When `syncProjectReferences` is enabled and a dependent project reference + # *does not* have a `tsconfig.json`, automatically create one. + createMissingConfig: true + + # Name of `tsconfig.json` file in each project root. + # projectConfigFileName: 'tsconfig.json' + + # Name of `tsconfig.json` file in the workspace root. + # rootConfigFileName: 'tsconfig.json' + + # Name of the config file in the workspace root that defines shared compiler + # options for all project reference based config files. + # rootOptionsConfigFileName: 'tsconfig.options.json' + + # Update a project's `tsconfig.json` to route the `outDir` compiler option + # to moon's `.moon/cache` directory. + routeOutDirToCache: true + + # Sync a project's `dependsOn` as project references within the + # project's `tsconfig.json` and the workspace root `tsconfig.json`. + syncProjectReferences: true + + # Sync a project's project references as import aliases to the `paths` + # compiler option in each applicable project. + syncProjectReferencesToPaths: true diff --git a/.moon/workspace.yml b/.moon/workspace.yml new file mode 100644 index 000000000..d5d91b5d0 --- /dev/null +++ b/.moon/workspace.yml @@ -0,0 +1,19 @@ +$schema: 'https://moonrepo.dev/schemas/workspace.json' + +vcs: + manager: 'git' + defaultBranch: 'main' + +projects: + - 'apps/*' + - 'core' + - '!packages/prisma-cli' + - 'packages/*' + +node: + version: '18.12.0' + packageManager: 'pnpm' + addEnginesConstraint: true + dedupeOnLockfileChange: true + syncProjectWorkspaceDependencies: true + syncVersionManagerConfig: 'nvm' diff --git a/.prettierignore b/.prettierignore index 9773f7bbb..9b7fe5ddf 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,6 @@ .svelte-kit node_modules .svelte-kit -build \ No newline at end of file +build +core.ts +dist \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 2fcb430ac..e0fb5b5c6 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,5 @@ { - "semi": true, + "semi": false, "useTabs": true, "tabWidth": 2, "singleQuote": true, diff --git a/.vscode/.todo b/.vscode/.todo new file mode 100644 index 000000000..ea96ecb86 --- /dev/null +++ b/.vscode/.todo @@ -0,0 +1,80 @@ +- [x] Consolidate some GH specific files to `.github` +- [x] Change `common` to `packages` +- [ ] Create `crates` with submodules for epub-rs? +- [ ] ESLint + - [x] Add the base configuration + - [ ] Fix ESLint errors after setup (expect a lot) +- [x] Add moon + - [x] Basic moon config/setup + - [x] Clean up all the `package.json` scripts + - [ ] Note: the scripts that interact with the FS directly are iffy with moon. I'll need to revisit those, so they will remain. +- [ ] Audit dependencies, prune where needed +- [ ] Fix all the peer dependency warnings +- [x] Nix? + - https://github.com/aaronleopold/stump/pull/86 +- [ ] Reorganize and consolidate some of the `client` code, its _very_ messy + - [x] Split `api` into separate package to house all the axios functions + - [ ] Rework gross hooks + callback types +- [x] Remove the `stump.service` file from GH and just add section in docs with that content +- [x] Consolidate some of the config files + - [x] Looking at you messy tsconfigs +- [x] Migrate to Axum v0.6 + - Rebase `axum-upgrade` branch once above chores completed + - https://github.com/tokio-rs/axum/releases/tag/axum-v0.6.0 +- [ ] Look into sqlite session store at some point to replace the in memory store. +- [x] Move `prisma-cli` out of `core`? +- [ ] Refactor all queries to use more current select/include patterns + - https://prisma.brendonovich.dev/reading-data/select-include#many-relation-options +- [x] Change HTTP endpoints to be **unpaged** by default (except opds) +- [ ] Rethink HTTP endpoints allow better range of filter options + - [x] Basic structure implemented, just a matter of prioritizing it time permitting +- [ ] Rework `integration_tests` and add unit testing throughout + - Mock client inbound -> https://github.com/Brendonovich/prisma-client-rust/issues/230 +- [ ] Look into better validation via https://github.com/Keats/validator +- [ ] Refactor UI sidebar and main container to support resizing via https://github.com/bvaughn/react-resizable-panels +- [ ] Either use the `optional_struct` dependency or look into https://github.com/Nukesor/inter-struct (or maybe both?) +- [ ] Refine Stump color palette (gray colors are :barf:) +- [ ] Start building out, but don't use quite yet, custom UI components in new package `components` + - [ ] https://github.com/shadcn/ui +- [ ] Get major UI sections completed to MVP stage: + - [ ] Homepage sections + - [x] Continue Reading, Recently Added, Etc. + - [ ] Statistics + - [ ] Book overview page + - [x] Show progress (if any) + - [ ] Show more series information + - [x] Next in series (using a new `cursor` page param?) + - [ ] Series overview section (on 0th page when browsing a series) + - [ ] Settings pages +- [ ] Remove all those `Loading...` placeholders +- [ ] For listviews, add a tooltip that shows some of the additional information on hover +- [x] Add redirects for unauthed access + - I.e. if I visit `/some-page/1`, and get redirected to `/auth`, should redirect instead with query params to restore state: `/auth?redirect=/some-page/1` +- [ ] Revisit RAR support with upcoming `0.5` unrar release -> https://github.com/muja/unrar.rs/issues/26 + - [ ] Move off of custom `read_bytes` implementation + - [ ] Test bug after -> https://github.com/aaronleopold/stump/issues/38 +- [ ] Finish implementing EPUB support + - [ ] Queries in `client` + - [ ] Lots of parsing in `core` + - [ ] Tracking progress +- [ ] Finish implementing Stump Readers: + - [ ] ImageBased + - [ ] Make toolbar **much** more performant, loading all those images lazily is terrible... + - [ ] AnimatedImageBased + - [ ] This was a PAIN IN THE ASS + - [ ] EPUB +- [ ] Tighten logging noise +- [ ] Revisit rework of `StumpConfig` +- [ ] Setup self hosted runner using gaming computer +- [ ] Finish GH workflows/actions +- [ ] Add some automated system for versioning/releases +- [ ] Once fully migrated off Chakra UI + - [ ] Finalize custom UI components + - [ ] Consider feature freeze and migrate over to SolidJS +- [ ] Add `Stump.toml` template and/or just list all the config options somewhere +- [ ] Fix `desktop-dev` command, it spawns a new window over and over :angry: +- [ ] Make light mode look not disgusting lmao +- [ ] ~Fix poor performance of EditLibraryModal~ + - [ ] Just replace the modals with dedicated pages for editing and creating libraries +- [ ] `local-ip-address` check if release yoinked comes back okay +- [ ] cargo report future-incompatibilities --id 4 --package rustc-serialize@0.3.24 diff --git a/.vscode/settings.json b/.vscode/settings.json index 6c1bcf7d6..1f074a86e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,12 @@ { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.renderWhitespace": "boundary", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer" - } + }, + "tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index b1ef28860..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,16 +0,0 @@ -# Contributing - -You can contribute by opening new issues or PRs. Please do not drop random PRs without talking with me first, please follow these steps: - -1. Check to see if an issue already exists relevant to your feature/topic -2. Create an issue (if an issue does not already exist) and express interest in working it -3. Create a fork of the repository. -4. Create a new feature branch from `develop`. -5. Add appropiate documentation to public items. -6. Ensure you have your code formatter properly configured. -7. Once you add your contributions, create a PR from your feature branch into `develop` - _not_ `main`. -8. Be sure to update your feature branch with any new changes **before** making the PR. - -If you are interested in making a non-code contribution, please be sure to add yourself to the [`CONTRIBUTORS.md`](https://github.com/aaronleopold/stump/tree/develop/.github/CONTRIBUTORS.md) file once your contribution has been finalized. - -Thanks for contributing to my project!! diff --git a/Cargo.lock b/Cargo.lock index eb1c23ea2..4beb62177 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" [[package]] name = "addr2line" -version = "0.17.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" dependencies = [ "gimli", ] @@ -23,28 +23,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - [[package]] name = "ahash" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] @@ -73,20 +67,11 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" -version = "1.0.65" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] name = "arrayref" @@ -108,22 +93,12 @@ checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" [[package]] name = "async-lock" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" dependencies = [ "event-listener", -] - -[[package]] -name = "async-recursion" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "futures-lite", ] [[package]] @@ -135,7 +110,7 @@ dependencies = [ "anyhow", "async-lock", "async-trait", - "base64 0.13.0", + "base64 0.13.1", "bincode", "blake3", "chrono", @@ -150,8 +125,7 @@ dependencies = [ [[package]] name = "async-stream" version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +source = "git+https://github.com/tokio-rs/async-stream?rev=e1373e4dede24f7700452e499a46561fb45ea515#e1373e4dede24f7700452e499a46561fb45ea515" dependencies = [ "async-stream-impl", "futures-core", @@ -160,23 +134,22 @@ dependencies = [ [[package]] name = "async-stream-impl" version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +source = "git+https://github.com/tokio-rs/async-stream?rev=e1373e4dede24f7700452e499a46561fb45ea515#e1373e4dede24f7700452e499a46561fb45ea515" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] name = "async-trait" -version = "0.1.57" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" +checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -200,7 +173,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] @@ -234,7 +207,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -247,13 +220,13 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.5.16" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043" +checksum = "1304eab461cf02bd70b083ed8273388f9724c549b316ba3d1e213ce0e9e7fb7e" dependencies = [ "async-trait", "axum-core", - "base64 0.13.0", + "base64 0.20.0", "bitflags", "bytes", "futures-util", @@ -261,16 +234,18 @@ dependencies = [ "http", "http-body", "hyper", - "itoa 1.0.3", + "itoa 1.0.5", "matchit", "memchr", "mime", "percent-encoding", "pin-project-lite", + "rustversion", "serde", "serde_json", + "serde_path_to_error", "serde_urlencoded", - "sha-1", + "sha1", "sync_wrapper", "tokio", "tokio-tungstenite", @@ -282,9 +257,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.2.8" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b" +checksum = "f487e40dc9daee24d8a1779df88522f159a54a980f99cfbe43db0be0bd3444a8" dependencies = [ "async-trait", "bytes", @@ -292,15 +267,16 @@ dependencies = [ "http", "http-body", "mime", + "rustversion", "tower-layer", "tower-service", ] [[package]] name = "axum-extra" -version = "0.3.7" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69034b3b0fd97923eee2ce8a47540edb21e07f48f87f67d44bb4271cec622bdb" +checksum = "f9a320103719de37b7b4da4c8eb629d4573f6bcfd3dfe80d3208806895ccf81d" dependencies = [ "axum", "bytes", @@ -309,6 +285,8 @@ dependencies = [ "http", "mime", "pin-project-lite", + "serde", + "serde_html_form", "tokio", "tower", "tower-http", @@ -318,21 +296,21 @@ dependencies = [ [[package]] name = "axum-macros" -version = "0.2.3" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6293dae2ec708e679da6736e857cf8532886ef258e92930f38279c12641628b8" +checksum = "cc7d7c3e69f305217e317a28172aab29f275667f2e1c15b87451e134fe27c7b1" dependencies = [ "heck 0.4.0", "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] name = "axum-sessions" -version = "0.3.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edac5a983d2aa76c3764a3e0a59e5fa885f5af468de4e4ce2f9182223cf281c" +checksum = "4b114309d293dd8a6fedebf09d5b8bbb0f7647b3d204ca0dd333b5f797aed5c8" dependencies = [ "async-session", "axum", @@ -346,9 +324,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" dependencies = [ "addr2line", "cc", @@ -367,9 +345,15 @@ checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" [[package]] name = "bcrypt" @@ -377,18 +361,18 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f691e63585950d8c1c43644d11bab9073e40f5060dd2822734ae7c3dc69a3a80" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "blowfish", - "getrandom 0.2.7", + "getrandom 0.2.8", ] [[package]] name = "bigdecimal" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1e50562e37200edf7c6c43e54a08e64a5553bfb59d9c297d5572512aa517256" +checksum = "6aaf33151a6429fe9211d1b276eafdf70cdff28b071e76c0b0e1503221ea3744" dependencies = [ - "num-bigint 0.3.3", + "num-bigint 0.4.3", "num-integer", "num-traits 0.2.15", "serde", @@ -415,15 +399,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitmaps" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" -dependencies = [ - "typenum", -] - [[package]] name = "blake3" version = "0.3.8" @@ -497,24 +472,40 @@ dependencies = [ [[package]] name = "bstr" -version = "0.2.17" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +checksum = "b45ea9b00a7b3f2988e9a65ad3917e62123c38dba709b666506207be96d1790b" dependencies = [ "memchr", + "serde", +] + +[[package]] +name = "builtin-psl-connectors" +version = "0.1.0" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" +dependencies = [ + "connection-string", + "either", + "enumflags2", + "indoc", + "lsp-types", + "once_cell", + "psl-core", + "regex", ] [[package]] name = "bumpalo" -version = "3.11.0" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "bytemuck" -version = "1.12.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" +checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" [[package]] name = "byteorder" @@ -524,15 +515,15 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] name = "bzip2" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" dependencies = [ "bzip2-sys", "libc", @@ -570,31 +561,24 @@ checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" dependencies = [ "glib-sys", "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] name = "cargo_toml" -version = "0.11.8" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72c3ff59e3b7d24630206bb63a73af65da4ed5df1f76ee84dfafb9fee2ba60e" +checksum = "497049e9477329f8f6a559972ee42e117487d01d1e8c2cc9f836ea6fa23a9e1a" dependencies = [ "serde", - "serde_derive", "toml", ] -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" - [[package]] name = "cc" -version = "1.0.73" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" dependencies = [ "jobserver", ] @@ -626,9 +610,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.10.3" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aacacf4d96c24b2ad6eb8ee6df040e4f27b0d0b39a5710c30091baa830485db" +checksum = "b0357a6402b295ca3a86bc148e84df46c02e41f41fef186bda662557ef6328aa" dependencies = [ "smallvec", ] @@ -647,16 +631,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ "iana-time-zone", "js-sys", "num-integer", "num-traits 0.2.15", "serde", - "time 0.1.44", + "time 0.1.43", "wasm-bindgen", "winapi", ] @@ -670,59 +654,11 @@ dependencies = [ "generic-array", ] -[[package]] -name = "clap" -version = "3.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" -dependencies = [ - "atty", - "bitflags", - "clap_derive", - "clap_lex", - "indexmap", - "once_cell", - "strsim", - "termcolor", - "textwrap", -] - -[[package]] -name = "clap_derive" -version = "3.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" -dependencies = [ - "heck 0.4.0", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "cloudabi" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -dependencies = [ - "bitflags", -] - [[package]] name = "cocoa" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" dependencies = [ "bitflags", "block", @@ -749,6 +685,16 @@ dependencies = [ "objc", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -828,17 +774,17 @@ dependencies = [ [[package]] name = "cookie" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ - "base64 0.13.0", + "base64 0.20.0", "hmac 0.12.1", "percent-encoding", "rand 0.8.5", "sha2 0.10.6", "subtle", - "time 0.3.15", + "time 0.3.17", "version_check", ] @@ -924,22 +870,22 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.11" +version = "0.9.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" dependencies = [ "autocfg", "cfg-if 1.0.0", "crossbeam-utils", - "memoffset", + "memoffset 0.7.1", "scopeguard", ] [[package]] name = "crossbeam-queue" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -947,37 +893,18 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" dependencies = [ "cfg-if 1.0.0", ] [[package]] -name = "crossterm" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" -dependencies = [ - "bitflags", - "crossterm_winapi", - "libc", - "mio", - "parking_lot 0.12.1", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.0" +name = "crunchy" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" -dependencies = [ - "winapi", -] +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-common" @@ -1021,9 +948,9 @@ dependencies = [ "matches", "phf 0.8.0", "proc-macro2", - "quote", + "quote 1.0.23", "smallvec", - "syn", + "syn 1.0.107", ] [[package]] @@ -1032,18 +959,18 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e" dependencies = [ - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] name = "ctor" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdffe87e1d521a10f9696f833fe502293ea446d7f256c06128293a4119bdf4cb" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" dependencies = [ - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -1054,24 +981,57 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" [[package]] name = "cuid" -version = "0.1.0" -source = "git+https://github.com/prisma/cuid-rust?rev=4ffb2e47c772af62fed3ddc92bb7fc444d19e159#4ffb2e47c772af62fed3ddc92bb7fc444d19e159" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a2891c056384f8461606f2579208164d67475fb17a0f442ac8d5981d3c2807" dependencies = [ - "hostname 0.1.5", + "hostname", "lazy_static", - "parking_lot 0.10.2", - "rand 0.7.3", + "rand 0.8.5", ] [[package]] -name = "cuid" -version = "1.2.0" +name = "cxx" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2a2891c056384f8461606f2579208164d67475fb17a0f442ac8d5981d3c2807" +checksum = "51d1075c37807dcf850c379432f0df05ba52cc30f279c5cfc43cc221ce7f8579" dependencies = [ - "hostname 0.3.1", - "lazy_static", - "rand 0.8.5", + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5044281f61b27bc598f2f6647d480aed48d2bf52d6eb0b627d84c0361b17aa70" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote 1.0.23", + "scratch", + "syn 1.0.107", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b50bc93ba22c27b0d31128d2d130a0a6b3d267ae27ef7e4fae2167dfe8781c" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e61fda7e62115119469c7b3591fd913ecca96fb766cfd3f2e2502ab7bc87a5" +dependencies = [ + "proc-macro2", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -1080,8 +1040,18 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa" +dependencies = [ + "darling_core 0.14.2", + "darling_macro 0.14.2", ] [[package]] @@ -1093,9 +1063,23 @@ dependencies = [ "fnv", "ident_case", "proc-macro2", - "quote", + "quote 1.0.23", + "strsim", + "syn 1.0.107", +] + +[[package]] +name = "darling_core" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote 1.0.23", "strsim", - "syn", + "syn 1.0.107", ] [[package]] @@ -1104,74 +1088,50 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core", - "quote", - "syn", + "darling_core 0.13.4", + "quote 1.0.23", + "syn 1.0.107", +] + +[[package]] +name = "darling_macro" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" +dependencies = [ + "darling_core 0.14.2", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] name = "data-encoding" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" [[package]] -name = "datamodel" +name = "datamodel-renderer" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ - "bigdecimal", - "chrono", - "datamodel-connector", - "diagnostics", - "dml", - "either", - "enumflags2", - "indoc", - "itertools", + "base64 0.13.1", "once_cell", - "parser-database", + "psl", "regex", - "schema-ast", - "serde", - "serde_json", - "sql-datamodel-connector", -] - -[[package]] -name = "datamodel-connector" -version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" -dependencies = [ - "diagnostics", - "enumflags2", - "lsp-types", - "parser-database", - "serde_json", - "url", ] [[package]] name = "dbus" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8bcdd56d2e5c4ed26a529c5a9029f5db8290d433497506f958eae3be148eb6" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" dependencies = [ "libc", "libdbus-sys", "winapi", ] -[[package]] -name = "deflate" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4" -dependencies = [ - "adler32", - "byteorder", -] - [[package]] name = "derive_more" version = "0.99.17" @@ -1180,17 +1140,18 @@ checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case 0.4.0", "proc-macro2", - "quote", + "quote 1.0.23", "rustc_version 0.4.0", - "syn", + "syn 1.0.107", ] [[package]] name = "diagnostics" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "colored", + "indoc", "pest", ] @@ -1205,9 +1166,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer 0.10.3", "crypto-common", @@ -1285,28 +1246,30 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] name = "dml" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "chrono", - "cuid 0.1.0", + "cuid", + "either", "enumflags2", "indoc", - "native-types", "prisma-value", + "psl-core", + "schema-ast", "serde", "serde_json", - "uuid 0.8.2", + "uuid 1.2.2", ] [[package]] name = "dmmf" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "bigdecimal", - "datamodel", "indexmap", "prisma-models", + "psl", "schema", "schema-builder", "serde", @@ -1328,6 +1291,12 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dunce" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c" + [[package]] name = "either" version = "1.8.0" @@ -1380,21 +1349,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] name = "epub" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4086fc0bc91524e0a88bc13fa622e3b9fce38d5a91454e0667db97a4f39dc3" +checksum = "83c5ac32621967f51e8b82def1a8a86bf4f4e4ab21b6e22f3486d42121fa6581" dependencies = [ "anyhow", "percent-encoding", "regex", "xml-rs", - "zip", + "zip 0.6.3", ] [[package]] @@ -1403,22 +1372,11 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" -[[package]] -name = "eventsource-stream" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" -dependencies = [ - "futures-core", - "nom", - "pin-project-lite", -] - [[package]] name = "exr" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a7880199e74c6d3fe45579df2f436c5913a71405494cb89d59234d86b47dc5" +checksum = "8eb5f255b5980bb0c8cf676b675d1a99be40f316881444f44e0462eaf5df5ded" dependencies = [ "bit_field", "flume", @@ -1456,19 +1414,19 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" dependencies = [ - "memoffset", + "memoffset 0.6.5", "rustc_version 0.3.3", ] [[package]] name = "filetime" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" +checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.16", + "redox_syscall", "windows-sys", ] @@ -1480,9 +1438,9 @@ checksum = "86d4de0081402f5e88cdac65c8dcdcc73118c1a7a465e2a05f0da05843a8ea33" [[package]] name = "flate2" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ "crc32fast", "miniz_oxide", @@ -1549,9 +1507,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" dependencies = [ "futures-channel", "futures-core", @@ -1564,9 +1522,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" dependencies = [ "futures-core", "futures-sink", @@ -1574,15 +1532,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" [[package]] name = "futures-executor" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" dependencies = [ "futures-core", "futures-task", @@ -1591,32 +1549,47 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] [[package]] name = "futures-macro" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] name = "futures-sink" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" [[package]] name = "futures-task" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" [[package]] name = "futures-timer" @@ -1626,9 +1599,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" dependencies = [ "futures-channel", "futures-core", @@ -1690,7 +1663,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] @@ -1707,7 +1680,7 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] @@ -1719,21 +1692,21 @@ dependencies = [ "gdk-sys", "glib-sys", "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", "x11", ] [[package]] name = "generator" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc184cace1cea8335047a471cc1da80f18acf8a76f3bab2028d499e328948ec7" +checksum = "d266041a359dfa931b370ef684cceb84b166beb14f7f0421f4a6a3d0c446d12e" dependencies = [ "cc", "libc", "log", "rustversion", - "windows 0.32.0", + "windows 0.39.0", ] [[package]] @@ -1759,9 +1732,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -1782,9 +1755,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.26.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793" [[package]] name = "gio" @@ -1812,7 +1785,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", "winapi", ] @@ -1847,8 +1820,8 @@ dependencies = [ "proc-macro-crate", "proc-macro-error", "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -1858,20 +1831,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" dependencies = [ "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" dependencies = [ "aho-corasick", "bstr", @@ -1888,13 +1861,13 @@ checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" dependencies = [ "glib-sys", "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] name = "graphql-parser" version = "0.3.0" -source = "git+https://github.com/prisma/graphql-parser?rev=6a3f58bd879065588e710cb02b5bd30c1ce182c3#6a3f58bd879065588e710cb02b5bd30c1ce182c3" +source = "git+https://github.com/prisma/graphql-parser#6a3f58bd879065588e710cb02b5bd30c1ce182c3" dependencies = [ "combine 3.8.1", "indexmap", @@ -1939,7 +1912,7 @@ dependencies = [ "gobject-sys", "libc", "pango-sys", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] @@ -1952,15 +1925,15 @@ dependencies = [ "proc-macro-crate", "proc-macro-error", "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] name = "h2" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" dependencies = [ "bytes", "fnv", @@ -1977,9 +1950,12 @@ dependencies = [ [[package]] name = "half" -version = "1.8.2" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] [[package]] name = "hashbrown" @@ -2014,7 +1990,7 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "bitflags", "bytes", "headers-core", @@ -2057,6 +2033,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "hex" version = "0.4.3" @@ -2079,17 +2064,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.5", -] - -[[package]] -name = "hostname" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e" -dependencies = [ - "libc", - "winutil", + "digest 0.10.6", ] [[package]] @@ -2113,8 +2088,8 @@ dependencies = [ "mac", "markup5ever", "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -2125,7 +2100,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa 1.0.3", + "itoa 1.0.5", ] [[package]] @@ -2165,9 +2140,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.20" +version = "0.14.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" dependencies = [ "bytes", "futures-channel", @@ -2178,7 +2153,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.3", + "itoa 1.0.5", "pin-project-lite", "socket2", "tokio", @@ -2202,25 +2177,36 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.50" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" dependencies = [ "android_system_properties", "core-foundation-sys", + "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "winapi", ] +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + [[package]] name = "ico" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a4b3331534254a9b64095ae60d3dc2a8225a7a70229cd5888be127cdc1f6804" +checksum = "031530fe562d8c8d71c0635013d6d155bbfe8ba0aa4b4d2d24ce8af6b71047bd" dependencies = [ "byteorder", - "png 0.11.0", + "png", ] [[package]] @@ -2241,11 +2227,10 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +checksum = "a05705bc64e0b66a806c3740bd6578ea66051b157ec42dc219c785cbf185aef3" dependencies = [ - "crossbeam-utils", "globset", "lazy_static", "log", @@ -2257,25 +2242,11 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "im" -version = "15.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" -dependencies = [ - "bitmaps", - "rand_core 0.6.4", - "rand_xoshiro", - "sized-chunks", - "typenum", - "version_check", -] - [[package]] name = "image" -version = "0.24.4" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8e4fb07cf672b1642304e731ef8a6a4c7891d67bb4fd4f5ce58cd6ed86803c" +checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" dependencies = [ "bytemuck", "byteorder", @@ -2285,35 +2256,35 @@ dependencies = [ "jpeg-decoder", "num-rational 0.4.1", "num-traits 0.2.15", - "png 0.17.6", + "png", "scoped_threadpool", "tiff", ] [[package]] name = "include_dir" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "482a2e29200b7eed25d7fdbd14423326760b7f6658d21a4cf12d55a50713c69f" +checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e" dependencies = [ "include_dir_macros", ] [[package]] name = "include_dir_macros" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e074c19deab2501407c91ba1860fa3d6820bfde307db6d8cb851b55a10be89b" +checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.23", ] [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown 0.12.3", @@ -2322,9 +2293,9 @@ dependencies = [ [[package]] name = "indoc" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3" +checksum = "da2d6f23ffea9d7e76c53eee25dfb67bcd8fde7f1198b0855350698c9f07c780" [[package]] name = "infer" @@ -2335,15 +2306,6 @@ dependencies = [ "cfb", ] -[[package]] -name = "inflate" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5f9f47468e9a76a6452271efadc88fe865a82be91fe75e6c0c57b87ccea59d4" -dependencies = [ - "adler32", -] - [[package]] name = "instant" version = "0.1.12" @@ -2354,21 +2316,25 @@ dependencies = [ ] [[package]] -name = "integration-tests" -version = "0.0.0" +name = "introspection-connector" +version = "0.1.0" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ - "prisma-client-rust", + "anyhow", + "async-trait", + "enumflags2", + "psl", "serde", - "stump_core", - "tempfile", - "tokio", + "serde_json", + "thiserror", + "user-facing-errors", ] [[package]] name = "ipnet" -version = "2.5.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" [[package]] name = "itertools" @@ -2387,9 +2353,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" [[package]] name = "javascriptcore-rs" @@ -2416,9 +2382,9 @@ dependencies = [ [[package]] name = "jni" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" dependencies = [ "cesu8", "combine 4.6.6", @@ -2445,9 +2411,9 @@ dependencies = [ [[package]] name = "jpeg-decoder" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" dependencies = [ "rayon", ] @@ -2463,9 +2429,9 @@ dependencies = [ [[package]] name = "json-patch" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f995a3c8f2bc3dd52a18a583e90f9ec109c047fa1603a853e46bcda14d2e279d" +checksum = "eb3fa5a61630976fc4c353c70297f2e93f1930e3ccee574d59d618ccbd5154ce" dependencies = [ "serde", "serde_json", @@ -2475,7 +2441,7 @@ dependencies = [ [[package]] name = "json-rpc-api-build" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "backtrace", "heck 0.3.3", @@ -2524,15 +2490,15 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.134" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libdbus-sys" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c185b5b7ad900923ef3a8ff594083d4d9b5aea80bb4f32b8342363138c0d456b" +checksum = "2264f9d90a9b4e60a2dc722ad899ea0374f03c2e96e755fe22a8f551d4d5fb3c" dependencies = [ "pkg-config", ] @@ -2567,12 +2533,23 @@ dependencies = [ ] [[package]] -name = "lock_api" -version = "0.3.4" +name = "link-cplusplus" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" dependencies = [ - "scopeguard", + "cc", +] + +[[package]] +name = "local-ip-address" +version = "0.5.1" +source = "git+https://github.com/EstebanBorai/local-ip-address.git?tag=v0.5.1#efb3a0b074ea9540bb055f1fe8f0fc5a5591b6bf" +dependencies = [ + "libc", + "neli", + "thiserror", + "windows-sys", ] [[package]] @@ -2647,7 +2624,7 @@ dependencies = [ "dirs-next", "objc-foundation", "objc_id", - "time 0.3.15", + "time 0.3.17", ] [[package]] @@ -2705,9 +2682,9 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "matchit" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" +checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" [[package]] name = "memchr" @@ -2724,6 +2701,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "metrics" version = "0.18.1" @@ -2769,8 +2755,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49e30813093f757be5cf21e50389a24dc7dbb22c49f23b7e8f51d69b508a5ffa" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -2802,46 +2788,24 @@ checksum = "fd1f4b69bef1e2b392b2d4a12902f2af90bb438ba4a66aa222d1023fa6561b50" dependencies = [ "atomic-shim", "crossbeam-epoch", - "crossbeam-utils", - "hashbrown 0.11.2", - "metrics 0.19.0", - "num_cpus", - "parking_lot 0.11.2", - "quanta", - "sketches-ddsketch", -] - -[[package]] -name = "miette" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28d6092d7e94a90bb9ea8e6c26c99d5d112d49dda2afdb4f7ea8cf09e1a5a6d" -dependencies = [ - "miette-derive", - "once_cell", - "thiserror", - "unicode-width", -] - -[[package]] -name = "miette-derive" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2485ed7d1fe80704928e3eb86387439609bd0c6bb96db8208daa364cfd1e09" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "crossbeam-utils", + "hashbrown 0.11.2", + "metrics 0.19.0", + "num_cpus", + "parking_lot 0.11.2", + "quanta", + "sketches-ddsketch", ] [[package]] name = "migration-connector" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "chrono", - "datamodel", "enumflags2", + "introspection-connector", + "psl", "sha2 0.9.9", "tracing", "tracing-error", @@ -2851,7 +2815,7 @@ dependencies = [ [[package]] name = "migration-core" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "async-trait", "chrono", @@ -2895,18 +2859,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.5.4" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", @@ -2917,7 +2881,7 @@ dependencies = [ [[package]] name = "mobc" version = "0.7.3" -source = "git+https://github.com/prisma/mobc?tag=1.0.5#d50fd5de25f80880b0f533bcb48cc65f2c4960b0" +source = "git+https://github.com/prisma/mobc?tag=1.0.6#80462c4870a2bf6aab49da15c88c021bae531da8" dependencies = [ "async-trait", "futures-channel", @@ -2938,14 +2902,14 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", ] [[package]] name = "native-tls" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ "lazy_static", "libc", @@ -2959,15 +2923,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "native-types" -version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "ndk" version = "0.6.0" @@ -2996,6 +2951,16 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "neli" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9053554eb5dcb7e10d9cdab1206965bde870eed5d0d341532ca035e3ba221508" +dependencies = [ + "byteorder", + "libc", +] + [[package]] name = "new_debug_unreachable" version = "1.0.4" @@ -3019,9 +2984,9 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] name = "nom" -version = "7.1.1" +version = "7.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c" dependencies = [ "memchr", "minimal-lexical", @@ -3029,15 +2994,25 @@ dependencies = [ [[package]] name = "notify-rust" -version = "4.5.10" +version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368e89ea58df747ce88be669ae44e79783c1d30bfd540ad0fc520b3f41f0b3b0" +checksum = "3ce656bb6d22a93ae276a23de52d1aec5ba4db3ece3c0eb79dfd5add7384db6a" dependencies = [ "dbus", "mac-notification-sys", "tauri-winrt-notification", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num" version = "0.1.42" @@ -3066,9 +3041,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.3.3" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" dependencies = [ "autocfg", "num-integer", @@ -3149,11 +3124,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] @@ -3174,17 +3149,8 @@ checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ "proc-macro-crate", "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -3228,18 +3194,18 @@ dependencies = [ [[package]] name = "object" -version = "0.29.0" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +checksum = "2b8c786513eb403643f2a88c244c2aaa270ef2153f55094587d0c48a3cf22a83" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "opaque-debug" @@ -3249,9 +3215,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "open" -version = "3.0.3" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a3100141f1733ea40b53381b0ae3117330735ef22309a190ac57b9576ea716" +checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8" dependencies = [ "pathdiff", "windows-sys", @@ -3259,9 +3225,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.42" +version = "0.10.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13" +checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -3279,8 +3245,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -3291,18 +3257,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "111.22.0+1.1.1q" +version = "111.24.0+1.1.1s" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f31f0d509d1c1ae9cada2f9539ff8f37933831fd5098879e482aa687d659853" +checksum = "3498f259dab01178c6228c6b00dcef0ed2a2d5e20d648c017861227773ea4abd" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.76" +version = "0.9.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce" +checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" dependencies = [ "autocfg", "cc", @@ -3332,6 +3298,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "optional_struct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936ca8fe00ec6e057e4b5199eda9780666b6fd29c553f4dec75f31495c618fba" +dependencies = [ + "quote 0.3.15", + "syn 0.11.11", +] + [[package]] name = "ordered-float" version = "2.10.0" @@ -3360,19 +3336,19 @@ dependencies = [ [[package]] name = "os_pipe" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dceb7e43f59c35ee1548045b2c72945a5a3bb6ce6d6f07cdc13dc8f6bc4930a" +checksum = "c6a252f1f8c11e84b3ab59d7a488e48e4478a93937e027076638c49536204639" dependencies = [ "libc", - "winapi", + "windows-sys", ] [[package]] -name = "os_str_bytes" -version = "6.3.0" +name = "overload" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "pango" @@ -3396,18 +3372,14 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] -name = "parking_lot" -version = "0.10.2" +name = "parking" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" -dependencies = [ - "lock_api 0.3.4", - "parking_lot_core 0.7.2", -] +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" [[package]] name = "parking_lot" @@ -3416,8 +3388,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", - "lock_api 0.4.9", - "parking_lot_core 0.8.5", + "lock_api", + "parking_lot_core 0.8.6", ] [[package]] @@ -3426,47 +3398,33 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "lock_api 0.4.9", - "parking_lot_core 0.9.3", -] - -[[package]] -name = "parking_lot_core" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" -dependencies = [ - "cfg-if 0.1.10", - "cloudabi", - "libc", - "redox_syscall 0.1.57", - "smallvec", - "winapi", + "lock_api", + "parking_lot_core 0.9.6", ] [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.2.16", + "redox_syscall", "smallvec", "winapi", ] [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.16", + "redox_syscall", "smallvec", "windows-sys", ] @@ -3474,7 +3432,7 @@ dependencies = [ [[package]] name = "parser-database" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "diagnostics", "either", @@ -3485,9 +3443,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" +checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" [[package]] name = "pathdiff" @@ -3503,9 +3461,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.4.0" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc7bc69c062e492337d74d59b120c274fd3d261b6bf6d3207d499b4b379c41a" +checksum = "4257b4a04d91f7e9e6290be5d3da4804dd5784fafde3a497d73eb2b4a158c30a" dependencies = [ "thiserror", "ucd-trie", @@ -3513,9 +3471,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.4.0" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b75706b9642ebcb34dab3bc7750f811609a0eb1dd8b88c2d15bf628c1c65b2" +checksum = "241cda393b0cdd65e62e07e12454f1f25d57017dcc514b1514cd3c4645e3a0a6" dependencies = [ "pest", "pest_generator", @@ -3523,26 +3481,26 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.4.0" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f9272122f5979a6511a749af9db9bfc810393f63119970d7085fed1c4ea0db" +checksum = "46b53634d8c8196302953c74d5352f33d0c512a9499bd2ce468fc9f4128fa27c" dependencies = [ "pest", "pest_meta", "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] name = "pest_meta" -version = "2.4.0" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8717927f9b79515e565a64fe46c38b8cd0427e64c40680b14a7365ab09ac8d" +checksum = "0ef4f1332a8d4678b41966bb4cc1d0676880e84183a1ecc3f4b69f03e99c7a51" dependencies = [ "once_cell", "pest", - "sha1", + "sha2 0.10.6", ] [[package]] @@ -3617,8 +3575,8 @@ dependencies = [ "phf_shared 0.8.0", "proc-macro-hack", "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -3631,8 +3589,8 @@ dependencies = [ "phf_shared 0.10.0", "proc-macro-hack", "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -3669,8 +3627,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -3687,9 +3645,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "plist" @@ -3697,31 +3655,19 @@ version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "indexmap", "line-wrap", "serde", - "time 0.3.15", + "time 0.3.17", "xml-rs", ] [[package]] name = "png" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b0cabbbd20c2d7f06dbf015e06aad59b6ca3d9ed14848783e98af9aaf19925" -dependencies = [ - "bitflags", - "deflate", - "inflate", - "num-iter", -] - -[[package]] -name = "png" -version = "0.17.6" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0e7f4c94ec26ff209cee506314212639d6c91b80afb82984819fafce9df01c" +checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" dependencies = [ "bitflags", "crc32fast", @@ -3731,9 +3677,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "precomputed-hash" @@ -3742,7 +3688,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] -name = "prisma" +name = "prisma-cli" version = "0.1.0" dependencies = [ "prisma-client-rust-cli", @@ -3750,18 +3696,23 @@ dependencies = [ [[package]] name = "prisma-client-rust" -version = "0.6.1" -source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=79ab6bd700199b92103711f01d4df42e4cef62a6#79ab6bd700199b92103711f01d4df42e4cef62a6" +version = "0.6.4" +source = "git+https://github.com/Brendonovich/prisma-client-rust.git?tag=0.6.4#1d1cedde04ad673e375400105c54a85584d898f8" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "bigdecimal", "chrono", - "datamodel", + "convert_case 0.6.0", + "diagnostics", + "dml", "dmmf", + "futures", "include_dir", "indexmap", "migration-core", + "paste", "prisma-models", + "psl", "query-connector", "query-core", "rspc", @@ -3774,61 +3725,62 @@ dependencies = [ "tokio", "tracing", "user-facing-errors", - "uuid 0.8.2", + "uuid 1.2.2", ] [[package]] name = "prisma-client-rust-cli" -version = "0.6.1" -source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=79ab6bd700199b92103711f01d4df42e4cef62a6#79ab6bd700199b92103711f01d4df42e4cef62a6" +version = "0.6.4" +source = "git+https://github.com/Brendonovich/prisma-client-rust.git?tag=0.6.4#1d1cedde04ad673e375400105c54a85584d898f8" dependencies = [ - "datamodel", + "directories", + "flate2", + "http", "prisma-client-rust-sdk", "proc-macro2", - "quote", + "quote 1.0.23", + "regex", + "reqwest", "serde", "serde_json", "serde_path_to_error", - "syn", + "syn 1.0.107", + "thiserror", ] [[package]] name = "prisma-client-rust-sdk" -version = "0.6.1" -source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=79ab6bd700199b92103711f01d4df42e4cef62a6#79ab6bd700199b92103711f01d4df42e4cef62a6" +version = "0.6.4" +source = "git+https://github.com/Brendonovich/prisma-client-rust.git?tag=0.6.4#1d1cedde04ad673e375400105c54a85584d898f8" dependencies = [ "convert_case 0.5.0", - "datamodel", - "directories", + "dml", "dmmf", - "flate2", - "http", "prisma-models", "proc-macro2", + "psl", "query-core", - "quote", - "regex", + "quote 1.0.23", "request-handlers", - "reqwest", "serde", "serde_json", "serde_path_to_error", - "syn", + "syn 1.0.107", + "thiserror", ] [[package]] name = "prisma-models" version = "0.0.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "bigdecimal", "chrono", - "datamodel", "itertools", "once_cell", "prisma-value", + "psl", "serde", - "serde_derive", "serde_json", "thiserror", ] @@ -3836,7 +3788,7 @@ dependencies = [ [[package]] name = "prisma-value" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "base64 0.12.3", "bigdecimal", @@ -3845,7 +3797,7 @@ dependencies = [ "regex", "serde", "serde_json", - "uuid 0.8.2", + "uuid 1.2.2", ] [[package]] @@ -3867,8 +3819,8 @@ checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", "version_check", ] @@ -3879,21 +3831,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.23", "version_check", ] [[package]] name = "proc-macro-hack" -version = "0.5.19" +version = "0.5.20+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.46" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" dependencies = [ "unicode-ident", ] @@ -3901,15 +3853,39 @@ dependencies = [ [[package]] name = "psl" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" +dependencies = [ + "builtin-psl-connectors", + "dml", + "psl-core", +] + +[[package]] +name = "psl-core" +version = "0.1.0" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ - "datamodel", + "bigdecimal", + "chrono", + "diagnostics", + "enumflags2", + "indoc", + "itertools", + "lsp-types", + "once_cell", + "parser-database", + "prisma-value", + "regex", + "schema-ast", + "serde", + "serde_json", + "url", ] [[package]] name = "quaint" version = "0.2.0-alpha.13" -source = "git+https://github.com/prisma/quaint?rev=fb4fe90682b4fecb485fd0d6975dd15a3bc9616b#fb4fe90682b4fecb485fd0d6975dd15a3bc9616b" +source = "git+https://github.com/prisma/quaint?rev=6df49f14efe99696e577ffb9902c83b09bec8de2#6df49f14efe99696e577ffb9902c83b09bec8de2" dependencies = [ "async-trait", "base64 0.12.3", @@ -3931,7 +3907,7 @@ dependencies = [ "tracing", "tracing-core", "url", - "uuid 0.8.2", + "uuid 1.2.2", ] [[package]] @@ -3945,7 +3921,7 @@ dependencies = [ "mach", "once_cell", "raw-cpuid", - "wasi 0.10.0+wasi-snapshot-preview1", + "wasi 0.10.2+wasi-snapshot-preview1", "web-sys", "winapi", ] @@ -3953,7 +3929,7 @@ dependencies = [ [[package]] name = "query-connector" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "anyhow", "async-trait", @@ -3967,13 +3943,13 @@ dependencies = [ "serde_json", "thiserror", "user-facing-errors", - "uuid 0.8.2", + "uuid 1.2.2", ] [[package]] name = "query-core" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "async-trait", "base64 0.12.3", @@ -3981,18 +3957,13 @@ dependencies = [ "chrono", "connection-string", "crossbeam-queue", - "cuid 0.1.0", - "datamodel", - "datamodel-connector", + "cuid", + "enumflags2", "futures", - "im", "indexmap", "itertools", "lazy_static", "lru", - "metrics 0.18.1", - "metrics-exporter-prometheus", - "metrics-util 0.12.1", "once_cell", "opentelemetry", "parking_lot 0.12.1", @@ -4000,7 +3971,9 @@ dependencies = [ "pin-utils", "prisma-models", "prisma-value", + "psl", "query-connector", + "query-engine-metrics", "schema", "schema-builder", "serde", @@ -4014,7 +3987,24 @@ dependencies = [ "tracing-subscriber", "url", "user-facing-errors", - "uuid 0.8.2", + "uuid 1.2.2", +] + +[[package]] +name = "query-engine-metrics" +version = "0.1.0" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" +dependencies = [ + "metrics 0.18.1", + "metrics-exporter-prometheus", + "metrics-util 0.12.1", + "once_cell", + "parking_lot 0.12.1", + "serde", + "serde_json", + "tracing", + "tracing-futures", + "tracing-subscriber", ] [[package]] @@ -4028,9 +4018,15 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.21" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" + +[[package]] +name = "quote" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] @@ -4133,7 +4129,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", ] [[package]] @@ -4154,15 +4150,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_xoshiro" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "raw-cpuid" version = "10.6.0" @@ -4183,21 +4170,19 @@ dependencies = [ [[package]] name = "rayon" -version = "1.5.3" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" dependencies = [ - "autocfg", - "crossbeam-deque", "either", "rayon-core", ] [[package]] name = "rayon-core" -version = "1.9.3" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -4214,12 +4199,6 @@ dependencies = [ "rand_core 0.3.1", ] -[[package]] -name = "redox_syscall" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" - [[package]] name = "redox_syscall" version = "0.2.16" @@ -4235,16 +4214,16 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.7", - "redox_syscall 0.2.16", + "getrandom 0.2.8", + "redox_syscall", "thiserror", ] [[package]] name = "regex" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" dependencies = [ "aho-corasick", "memchr", @@ -4262,9 +4241,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "remove_dir_all" @@ -4278,16 +4257,16 @@ dependencies = [ [[package]] name = "request-handlers" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "bigdecimal", "connection-string", - "datamodel", "dmmf", "futures", "graphql-parser", "indexmap", "itertools", + "psl", "query-core", "serde", "serde_json", @@ -4299,11 +4278,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" +checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "bytes", "encoding_rs", "futures-core", @@ -4326,7 +4305,6 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-native-tls", - "tokio-util", "tower-service", "url", "wasm-bindgen", @@ -4335,22 +4313,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "reqwest-eventsource" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f03f570355882dd8d15acc3a313841e6e90eddbc76a93c748fd82cc13ba9f51" -dependencies = [ - "eventsource-stream", - "futures-core", - "futures-timer", - "mime", - "nom", - "pin-project-lite", - "reqwest", - "thiserror", -] - [[package]] name = "rfd" version = "0.10.0" @@ -4421,6 +4383,41 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-embed" +version = "6.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "283ffe2f866869428c92e0d61c2f35dfb4355293cdfdc48f49e895c15f1333d1" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ab23d42d71fb9be1b643fe6765d292c5e14d46912d13f3ae2815ca048ea04d" +dependencies = [ + "proc-macro2", + "quote 1.0.23", + "rust-embed-utils", + "shellexpand", + "syn 1.0.107", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1669d81dfabd1b5f8e2856b8bbe146c6192b0ba22162edc738ac0a5de18f054" +dependencies = [ + "sha2 0.10.6", + "walkdir", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -4448,20 +4445,20 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.14", + "semver 1.0.16", ] [[package]] name = "rustversion" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" [[package]] name = "ryu" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" [[package]] name = "safemem" @@ -4480,28 +4477,27 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" dependencies = [ - "lazy_static", "windows-sys", ] [[package]] name = "schema" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ - "datamodel-connector", "once_cell", "prisma-models", + "psl", ] [[package]] name = "schema-ast" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "diagnostics", "pest", @@ -4511,21 +4507,21 @@ dependencies = [ [[package]] name = "schema-builder" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ - "datamodel-connector", "itertools", "lazy_static", "once_cell", "prisma-models", + "psl", "schema", ] [[package]] name = "scoped-tls" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scoped_threadpool" @@ -4539,6 +4535,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + [[package]] name = "security-framework" version = "2.7.0" @@ -4593,9 +4595,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" dependencies = [ "serde", ] @@ -4611,9 +4613,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.145" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" dependencies = [ "serde_derive", ] @@ -4642,45 +4644,58 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.145" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", +] + +[[package]] +name = "serde_html_form" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d051fb33111db0e81673ed8c55db741952a19ad81dc584960c8aec836498ba5" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa 1.0.5", + "ryu", + "serde", ] [[package]] name = "serde_json" -version = "1.0.85" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" dependencies = [ "indexmap", - "itoa 1.0.3", + "itoa 1.0.5", "ryu", "serde", ] [[package]] name = "serde_path_to_error" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "184c643044780f7ceb59104cef98a5a6f12cb2288a7bc701ab93a362b49fd47d" +checksum = "26b04f22b563c91331a10074bda3dd5492e3cc39d56bd557e91c0af42b6c7341" dependencies = [ "serde", ] [[package]] name = "serde_repr" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -4690,7 +4705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.3", + "itoa 1.0.5", "ryu", "serde", ] @@ -4702,7 +4717,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" dependencies = [ "serde", - "serde_with_macros", + "serde_with_macros 1.5.2", +] + +[[package]] +name = "serde_with" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d904179146de381af4c93d3af6ca4984b3152db687dacb9c3c35e86f39809c" +dependencies = [ + "base64 0.13.1", + "chrono", + "hex", + "indexmap", + "serde", + "serde_json", + "serde_with_macros 2.2.0", + "time 0.3.17", ] [[package]] @@ -4711,10 +4742,22 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ - "darling", + "darling 0.13.4", + "proc-macro2", + "quote 1.0.23", + "syn 1.0.107", +] + +[[package]] +name = "serde_with_macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1966009f3c05f095697c537312f5415d1e3ed31ce0a56942bac4c771c5c335e" +dependencies = [ + "darling 0.14.2", "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -4735,8 +4778,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -4749,17 +4792,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "sha-1" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" -dependencies = [ - "cfg-if 1.0.0", - "cpufeatures", - "digest 0.10.5", -] - [[package]] name = "sha1" version = "0.10.5" @@ -4768,7 +4800,7 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.5", + "digest 0.10.6", ] [[package]] @@ -4792,7 +4824,7 @@ checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.5", + "digest 0.10.6", ] [[package]] @@ -4815,24 +4847,12 @@ dependencies = [ ] [[package]] -name = "signal-hook" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.3" +name = "shellexpand" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" dependencies = [ - "libc", - "mio", - "signal-hook", + "dirs", ] [[package]] @@ -4850,16 +4870,6 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" -[[package]] -name = "sized-chunks" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" -dependencies = [ - "bitmaps", - "typenum", -] - [[package]] name = "sketches-ddsketch" version = "0.1.3" @@ -4931,7 +4941,7 @@ dependencies = [ "serde_json", "specta-macros", "termcolor", - "uuid 1.1.2", + "uuid 1.2.2", ] [[package]] @@ -4942,8 +4952,8 @@ checksum = "a5ad5db5dea9fa5816615d722b682781214fed875fe56c1bcd0a67ee9c477d84" dependencies = [ "Inflector", "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", "termcolor", ] @@ -4959,106 +4969,111 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" dependencies = [ - "lock_api 0.4.9", + "lock_api", ] [[package]] -name = "sql-datamodel-connector" +name = "sql-ddl" +version = "0.1.0" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" + +[[package]] +name = "sql-introspection-connector" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ - "connection-string", - "datamodel-connector", - "either", + "anyhow", + "async-trait", + "bigdecimal", + "datamodel-renderer", "enumflags2", - "lsp-types", - "native-types", + "introspection-connector", "once_cell", + "psl", + "quaint", "regex", + "serde", "serde_json", + "sql-schema-describer", + "thiserror", + "tracing", + "tracing-futures", + "user-facing-errors", ] -[[package]] -name = "sql-ddl" -version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" - [[package]] name = "sql-migration-connector" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "chrono", "connection-string", - "datamodel", "either", "enumflags2", "indoc", "migration-connector", - "native-types", "once_cell", + "psl", "quaint", "regex", "serde_json", "sql-ddl", + "sql-introspection-connector", "sql-schema-describer", "tokio", "tracing", "tracing-futures", "url", "user-facing-errors", - "uuid 0.8.2", + "uuid 1.2.2", ] [[package]] name = "sql-query-connector" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "anyhow", "async-trait", "bigdecimal", "chrono", - "cuid 0.1.0", - "datamodel", + "cuid", "futures", "itertools", "once_cell", "opentelemetry", "prisma-models", "prisma-value", + "psl", "quaint", "query-connector", "rand 0.7.3", "serde", "serde_json", - "sql-datamodel-connector", "thiserror", "tokio", "tracing", "tracing-futures", "tracing-opentelemetry", "user-facing-errors", - "uuid 0.8.2", + "uuid 1.2.2", ] [[package]] name = "sql-schema-describer" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "async-trait", "bigdecimal", "enumflags2", "indexmap", "indoc", - "native-types", "once_cell", - "prisma-value", + "psl", "quaint", "regex", "serde", - "serde_json", "tracing", "tracing-error", "tracing-futures", @@ -5066,9 +5081,9 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87e292b4291f154971a43c3774364e2cbcaec599d3f5bf6fa9d122885dbc38a" +checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" dependencies = [ "itertools", "nom", @@ -5113,7 +5128,7 @@ dependencies = [ "phf_generator 0.10.0", "phf_shared 0.10.0", "proc-macro2", - "quote", + "quote 1.0.23", ] [[package]] @@ -5139,23 +5154,8 @@ checksum = "339f799d8b549e3744c7ac7feb216383e4005d94bdb22561b3ab8f3b808ae9fb" dependencies = [ "heck 0.3.3", "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "stump_cli" -version = "0.0.0" -dependencies = [ - "clap", - "crossterm", - "futures-util", - "reqwest", - "reqwest-eventsource", - "thiserror", - "tokio", - "tokio-graceful-shutdown", - "tui", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -5163,7 +5163,7 @@ name = "stump_core" version = "0.0.0" dependencies = [ "async-trait", - "cuid 1.2.0", + "cuid", "data-encoding", "dirs", "epub", @@ -5172,6 +5172,7 @@ dependencies = [ "image", "infer", "itertools", + "optional_struct", "prisma-client-rust", "rayon", "ring", @@ -5187,10 +5188,11 @@ dependencies = [ "trash", "unrar", "urlencoding", + "utoipa", "walkdir", "webp", "xml-rs", - "zip", + "zip 0.5.13", ] [[package]] @@ -5214,22 +5216,26 @@ dependencies = [ "axum-extra", "axum-macros", "axum-sessions", - "base64 0.13.0", + "base64 0.13.1", "bcrypt", "chrono", "futures-util", "hyper", + "local-ip-address", "openssl", "prisma-client-rust", "rand 0.8.5", "serde", "serde_json", + "serde_with 2.2.0", "stump_core", "thiserror", "tokio", "tokio-util", "tower-http", "tracing", + "utoipa", + "utoipa-swagger-ui", ] [[package]] @@ -5240,12 +5246,23 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.101" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +dependencies = [ + "quote 0.3.15", + "synom", + "unicode-xid", +] + +[[package]] +name = "syn" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.23", "unicode-ident", ] @@ -5255,6 +5272,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +dependencies = [ + "unicode-xid", +] + [[package]] name = "system-deps" version = "5.0.0" @@ -5270,22 +5296,22 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.0.2" +version = "6.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709" +checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff" dependencies = [ - "cfg-expr 0.10.3", + "cfg-expr 0.11.0", "heck 0.4.0", "pkg-config", "toml", - "version-compare 0.1.0", + "version-compare 0.1.1", ] [[package]] name = "tao" -version = "0.14.0" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43336f5d1793543ba96e2a1e75f3a5c7dcd592743be06a0ab3a190f4fcb4b934" +checksum = "ac8e6399427c8494f9849b58694754d7cc741293348a6836b6c8d2c5aa82d8e6" dependencies = [ "bitflags", "cairo-rs", @@ -5316,12 +5342,12 @@ dependencies = [ "once_cell", "parking_lot 0.12.1", "paste", - "png 0.17.6", + "png", "raw-window-handle", "scopeguard", "serde", "unicode-segmentation", - "uuid 1.1.2", + "uuid 1.2.2", "windows 0.39.0", "windows-implement", "x11-dl", @@ -5340,9 +5366,9 @@ dependencies = [ [[package]] name = "tauri" -version = "1.1.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbf22abd61d95ca9b2becd77f9db4c093892f73e8a07d21d8b0b2bf71a7bcea" +checksum = "5b48820ee3bb6a5031a83b2b6e11f8630bdc5a2f68cb841ab8ebc7a15a916679" dependencies = [ "anyhow", "attohttpc", @@ -5369,7 +5395,7 @@ dependencies = [ "raw-window-handle", "regex", "rfd", - "semver 1.0.14", + "semver 1.0.16", "serde", "serde_json", "serde_repr", @@ -5385,7 +5411,7 @@ dependencies = [ "thiserror", "tokio", "url", - "uuid 1.1.2", + "uuid 1.2.2", "webkit2gtk", "webview2-com", "windows 0.39.0", @@ -5393,15 +5419,15 @@ dependencies = [ [[package]] name = "tauri-build" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0991fb306849897439dbd4a72e4cbed2413e2eb26cb4b3ba220b94edba8b4b88" +checksum = "8807c85d656b2b93927c19fe5a5f1f1f348f96c2de8b90763b3c2d561511f9b4" dependencies = [ "anyhow", "cargo_toml", "heck 0.4.0", "json-patch", - "semver 1.0.14", + "semver 1.0.16", "serde_json", "tauri-utils", "winres", @@ -5409,70 +5435,69 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "356fa253e40ae4d6ff02075011f2f2bb4066f5c9d8c1e16ca6912d7b75903ba6" +checksum = "14388d484b6b1b5dc0f6a7d6cc6433b3b230bec85eaa576adcdf3f9fafa49251" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "brotli", "ico", "json-patch", "plist", - "png 0.17.6", + "png", "proc-macro2", - "quote", + "quote 1.0.23", "regex", - "semver 1.0.14", + "semver 1.0.16", "serde", "serde_json", "sha2 0.10.6", "tauri-utils", "thiserror", - "time 0.3.15", - "uuid 1.1.2", + "time 0.3.17", + "uuid 1.2.2", "walkdir", ] [[package]] name = "tauri-macros" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6051fd6940ddb22af452340d03c66a3e2f5d72e0788d4081d91e31528ccdc4d" +checksum = "069319e5ecbe653a799b94b0690d9f9bf5d00f7b1d3989aa331c524d4e354075" dependencies = [ "heck 0.4.0", "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-runtime" -version = "0.11.1" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49439a5ea47f474572b854972f42eda2e02a470be5ca9609cc83bb66945abe2" +checksum = "c507d954d08ac8705d235bc70ec6975b9054fb95ff7823af72dbb04186596f3b" dependencies = [ "gtk", "http", "http-range", - "infer", "rand 0.8.5", "raw-window-handle", "serde", "serde_json", "tauri-utils", "thiserror", - "uuid 1.1.2", + "uuid 1.2.2", "webview2-com", "windows 0.39.0", ] [[package]] name = "tauri-runtime-wry" -version = "0.11.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dce920995fd49907aa9bea7249ed1771454f11f7611924c920a1f75fb614d4" +checksum = "36b1c5764a41a13176a4599b5b7bd0881bea7d94dfe45e1e755f789b98317e30" dependencies = [ "cocoa", "gtk", @@ -5481,7 +5506,7 @@ dependencies = [ "raw-window-handle", "tauri-runtime", "tauri-utils", - "uuid 1.1.2", + "uuid 1.2.2", "webkit2gtk", "webview2-com", "windows 0.39.0", @@ -5490,25 +5515,26 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8fdae6f29cef959809a3c3afef510c5b715a446a597ab8b791497585363f39" +checksum = "5abbc109a6eb45127956ffcc26ef0e875d160150ac16cfa45d26a6b2871686f1" dependencies = [ "brotli", "ctor", "glob", "heck 0.4.0", "html5ever", + "infer", "json-patch", "kuchiki", "memchr", "phf 0.10.1", "proc-macro2", - "quote", - "semver 1.0.14", + "quote 1.0.23", + "semver 1.0.16", "serde", "serde_json", - "serde_with", + "serde_with 1.14.0", "thiserror", "url", "walkdir", @@ -5545,7 +5571,7 @@ dependencies = [ "cfg-if 1.0.0", "fastrand", "libc", - "redox_syscall 0.2.16", + "redox_syscall", "remove_dir_all", "winapi", ] @@ -5570,12 +5596,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" - [[package]] name = "thin-slice" version = "0.1.1" @@ -5584,22 +5604,22 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -5622,9 +5642,9 @@ dependencies = [ [[package]] name = "tiff" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259662e32d1e219321eb309d5f9d898b779769d81b76e762c07c8e5d38fcb65" +checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" dependencies = [ "flate2", "jpeg-decoder", @@ -5633,32 +5653,40 @@ dependencies = [ [[package]] name = "time" -version = "0.1.44" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] [[package]] name = "time" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ - "itoa 1.0.3", - "libc", - "num_threads", + "itoa 1.0.5", + "serde", + "time-core", "time-macros", ] +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + [[package]] name = "time-macros" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] [[package]] name = "tinyvec" @@ -5677,9 +5705,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.2" +version = "1.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae" dependencies = [ "autocfg", "bytes", @@ -5687,39 +5715,23 @@ dependencies = [ "memchr", "mio", "num_cpus", + "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "winapi", -] - -[[package]] -name = "tokio-graceful-shutdown" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ff81b9e7ba1103cf0bd37dffe8c0e7c243a7944a6d5b7e8de77c8c7a6b71718" -dependencies = [ - "async-recursion", - "async-trait", - "futures", - "log", - "miette", - "pin-project-lite", - "thiserror", - "tokio", - "tokio-util", + "windows-sys", ] [[package]] name = "tokio-macros" -version = "1.8.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -5734,9 +5746,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.17.2" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181" +checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" dependencies = [ "futures-util", "log", @@ -5760,9 +5772,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" dependencies = [ "serde", ] @@ -5785,9 +5797,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba" +checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" dependencies = [ "bitflags", "bytes", @@ -5810,9 +5822,9 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" @@ -5822,9 +5834,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.36" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", "log", @@ -5840,26 +5852,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" dependencies = [ "crossbeam-channel", - "time 0.3.15", + "time 0.3.17", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] name = "tracing-core" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", "valuable", @@ -5912,12 +5924,12 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" dependencies = [ - "ansi_term", "matchers", + "nu-ansi-term", "once_cell", "regex", "sharded-slab", @@ -5955,37 +5967,24 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" - -[[package]] -name = "tui" -version = "0.19.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" -dependencies = [ - "bitflags", - "cassowary", - "crossterm", - "unicode-segmentation", - "unicode-width", -] +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.17.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" +checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "byteorder", "bytes", "http", "httparse", "log", "rand 0.8.5", - "sha-1", + "sha1", "thiserror", "url", "utf-8", @@ -5993,9 +5992,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "ucd-trie" @@ -6020,9 +6019,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "unicode-normalization" @@ -6045,6 +6044,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" + [[package]] name = "unicode_categories" version = "0.1.1" @@ -6111,17 +6116,17 @@ checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" [[package]] name = "user-facing-error-macros" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] name = "user-facing-errors" version = "0.1.0" -source = "git+https://github.com/Brendonovich/prisma-engines?rev=5bacc96c3527f6a9e50c8011528fb64ac04e350b#5bacc96c3527f6a9e50c8011528fb64ac04e350b" +source = "git+https://github.com/Brendonovich/prisma-engines?rev=6bad339fc5b8bbc77e028eeae2038cf2ade2e6be#6bad339fc5b8bbc77e028eeae2038cf2ade2e6be" dependencies = [ "backtrace", "indoc", @@ -6138,23 +6143,63 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utoipa" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15f6da6a2b471134ca44b7d18e8a76d73035cf8b3ed24c4dd5ca6a63aa439c5" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2e33027986a4707b3f5c37ed01b33d0e5a53da30204b52ff18f80600f1d0ec" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote 1.0.23", + "syn 1.0.107", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3d4f4da6408f0f20ff58196ed619c94306ab32635aeca3d3fa0768c0bd0de2" +dependencies = [ + "axum", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "utoipa", + "zip 0.6.3", +] + [[package]] name = "uuid" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.7", - "serde", + "getrandom 0.2.8", ] [[package]] name = "uuid" -version = "1.1.2" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", + "serde", ] [[package]] @@ -6177,9 +6222,9 @@ checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" [[package]] name = "version-compare" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" [[package]] name = "version_check" @@ -6193,6 +6238,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "walkdir" version = "2.3.2" @@ -6222,9 +6273,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasi" @@ -6252,8 +6303,8 @@ dependencies = [ "lazy_static", "log", "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", "wasm-bindgen-shared", ] @@ -6275,7 +6326,7 @@ version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" dependencies = [ - "quote", + "quote 1.0.23", "wasm-bindgen-macro-support", ] @@ -6286,8 +6337,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6310,9 +6361,9 @@ dependencies = [ [[package]] name = "webkit2gtk" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29952969fb5e10fe834a52eb29ad0814ccdfd8387159b0933edf1344a1c9cdcc" +checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370" dependencies = [ "bitflags", "cairo-rs", @@ -6352,7 +6403,7 @@ dependencies = [ "pango-sys", "pkg-config", "soup2-sys", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] @@ -6384,8 +6435,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaebe196c01691db62e9e4ca52c5ef1e4fd837dcae27dae3ada599b5a8fd05ac" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -6446,19 +6497,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbedf6db9096bc2364adce0ae0aa636dcd89f3c3f2cd67947062aaf0ca2a10ec" -dependencies = [ - "windows_aarch64_msvc 0.32.0", - "windows_i686_gnu 0.32.0", - "windows_i686_msvc 0.32.0", - "windows_x86_64_gnu 0.32.0", - "windows_x86_64_msvc 0.32.0", -] - [[package]] name = "windows" version = "0.37.0" @@ -6502,7 +6540,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7" dependencies = [ - "syn", + "syn 1.0.107", "windows-tokens", ] @@ -6514,15 +6552,17 @@ checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" [[package]] name = "windows-sys" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.1", ] [[package]] @@ -6532,16 +6572,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" [[package]] -name = "windows_aarch64_msvc" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" +name = "windows_aarch64_gnullvm" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" [[package]] name = "windows_aarch64_msvc" @@ -6556,16 +6590,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" [[package]] -name = "windows_i686_gnu" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" - -[[package]] -name = "windows_i686_gnu" -version = "0.36.1" +name = "windows_aarch64_msvc" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" [[package]] name = "windows_i686_gnu" @@ -6580,16 +6608,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" [[package]] -name = "windows_i686_msvc" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" - -[[package]] -name = "windows_i686_msvc" -version = "0.36.1" +name = "windows_i686_gnu" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" [[package]] name = "windows_i686_msvc" @@ -6604,16 +6626,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" [[package]] -name = "windows_x86_64_gnu" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" +name = "windows_i686_msvc" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" [[package]] name = "windows_x86_64_gnu" @@ -6628,16 +6644,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" [[package]] -name = "windows_x86_64_msvc" -version = "0.32.0" +name = "windows_x86_64_gnu" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" [[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" +name = "windows_x86_64_gnullvm" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" [[package]] name = "windows_x86_64_msvc" @@ -6651,6 +6667,12 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + [[package]] name = "winreg" version = "0.10.1" @@ -6669,26 +6691,18 @@ dependencies = [ "toml", ] -[[package]] -name = "winutil" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e" -dependencies = [ - "winapi", -] - [[package]] name = "wry" -version = "0.21.1" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff5c1352b4266fdf92c63479d2f58ab4cd29dc4e78fbc1b62011ed1227926945" +checksum = "4c1ad8e2424f554cc5bdebe8aa374ef5b433feff817aebabca0389961fc7ef98" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "block", "cocoa", "core-graphics", "crossbeam-channel", + "dunce", "gdk", "gio", "glib", @@ -6704,6 +6718,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.6", + "soup2", "tao", "thiserror", "url", @@ -6716,9 +6731,9 @@ dependencies = [ [[package]] name = "x11" -version = "2.20.0" +version = "2.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7ae97874a928d821b061fce3d1fc52f08071dd53c89a6102bc06efcac3b2908" +checksum = "c2638d5b9c17ac40575fb54bb461a4b1d2a8d1b4ffcc4ff237d254ec59ddeb82" dependencies = [ "libc", "pkg-config", @@ -6726,9 +6741,9 @@ dependencies = [ [[package]] name = "x11-dl" -version = "2.20.0" +version = "2.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c83627bc137605acc00bb399c7b908ef460b621fc37c953db2b09f88c449ea6" +checksum = "b1536d6965a5d4e573c7ef73a2c15ebcd0b2de3347bdf526c34c297c00ac40f0" dependencies = [ "lazy_static", "libc", @@ -6761,5 +6776,17 @@ dependencies = [ "crc32fast", "flate2", "thiserror", - "time 0.1.44", + "time 0.1.43", +] + +[[package]] +name = "zip" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537ce7411d25e54e8ae21a7ce0b15840e7bfcff15b51d697ec3266cc76bdf080" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", ] diff --git a/Cargo.toml b/Cargo.toml index 7f8813ad3..37ed18db0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,28 +1,27 @@ [workspace] resolver = "2" members = [ - "core", - "core/prisma", - "core/integration-tests", "apps/desktop/src-tauri", - "apps/tui", - "apps/server" + "apps/server", + "core", + # "core/integration-tests", + "packages/prisma-cli", ] [workspace.package] version = "0.0.0" -rust-version = "1.64.0" +rust-version = "1.68.0" [workspace.dependencies] -# prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.1", features = [ -# 'rspc', -# # 'sqlite-create-many', -# # "migrations", -# # "sqlite", -# ] } -prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "79ab6bd700199b92103711f01d4df42e4cef62a6", features = [ +prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust.git", tag = "0.6.4", features = [ 'rspc', - # 'sqlite-create-many', + 'sqlite-create-many', + "migrations", + "sqlite", +], default-features = false } +prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust.git", tag = "0.6.4", features = [ + "rspc", + "sqlite-create-many", "migrations", "sqlite", ], default-features = false } @@ -36,11 +35,15 @@ tokio = { version = "1.21.2", features = [ # needed for detecting shutdown signals (e.g. ctrl+c) "signal", ] } +async-stream = "0.3.3" ### DEV UTILS ### -# specta = "0.0.2" specta = "0.0.4" ### Error Handling + Logging ### tracing = "0.1.36" -thiserror = "1.0.37" \ No newline at end of file +thiserror = "1.0.37" + +[patch.crates-io] +# for some reason, async-stream 0.3.4 is getting used instead of 0.3.3 but ONLY IN DOCKER?? So I have to patch it IG?? kms +async-stream = { git = "https://github.com/tokio-rs/async-stream", rev = "e1373e4dede24f7700452e499a46561fb45ea515" } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 3d1213e1f..000000000 --- a/Dockerfile +++ /dev/null @@ -1,145 +0,0 @@ -# ------------------------------------------------------------------------------ -# Frontend Build Stage -# ------------------------------------------------------------------------------ - -FROM node:16-alpine3.14 as frontend -ARG TARGETARCH - -WORKDIR /app - -# Note: I don't like copying ~everything~ but since I now use types exported from -# the core, and use pnpm specific means of accessing it via the workspace, I kind -# of need to maintain the structure of the workspace and use pnpm -COPY . . - -RUN npm install -g pnpm - -RUN pnpm i -RUN pnpm web build - -RUN mv ./apps/web/dist build - -# ------------------------------------------------------------------------------ -# Cargo Build Stage -# ------------------------------------------------------------------------------ - -###################### -### aarch64 / arm64 ## -###################### - -FROM messense/rust-musl-cross:aarch64-musl AS arm64-backend - -WORKDIR /app - -COPY .cargo .cargo -COPY . . - -ENV CARGO_NET_GIT_FETCH_WITH_CLI=true - -RUN rustup target add aarch64-unknown-linux-musl - -RUN set -ex; \ - sed -i 's|\/.*\/core\/prisma\/schema.prisma|\/app\/core\/prisma\/schema.prisma|g' core/src/prisma.rs; \ - sed -i 's|\/.*\/core\/prisma\/migrations|\/app\/core\/prisma\/migrations|g' core/src/prisma.rs - -RUN cargo build --package stump_server --bin stump_server --release --target aarch64-unknown-linux-musl && \ - cp target/aarch64-unknown-linux-musl/release/stump_server ./stump - -###################### -### armv7 / arm/v7 ### -###################### - -# Note: the name here isn't entirely accurate to my understanding. But I can't figure -# out how to have the name be v7 inclusive so -FROM messense/rust-musl-cross:armv7-musleabihf@sha256:3e133558686fd5059ce25749cece40a81d87dad2c7a68727c36a1bcacba6752c AS arm-backend - -WORKDIR /app - -COPY .cargo .cargo -COPY . . - -ENV CARGO_NET_GIT_FETCH_WITH_CLI=true - -RUN rustup target add armv7-unknown-linux-musleabihf - -RUN cargo build --package stump_server --bin stump_server --release --target armv7-unknown-linux-musleabihf && \ - cp target/armv7-unknown-linux-musleabihf/release/stump_server ./stump - -###################### -### x86_64 / amd64 ### -###################### - -FROM messense/rust-musl-cross:x86_64-musl AS amd64-backend - -WORKDIR /app - -ENV CARGO_NET_GIT_FETCH_WITH_CLI=true - -COPY .cargo .cargo -COPY . . - -RUN rustup update && rustup target add x86_64-unknown-linux-musl - -# prisma uses some `include_str!` macros that are mapped to locations on the host machine. so -# when we build in docker, we need to correct these paths according to the docker workdir. -# it's a bit of a hack, but it works lol -RUN set -ex; \ - sed -i 's|\/.*\/core\/prisma\/schema.prisma|\/app\/core\/prisma\/schema.prisma|g' core/src/prisma.rs; \ - sed -i 's|\/.*\/core\/prisma\/migrations|\/app\/core\/prisma\/migrations|g' core/src/prisma.rs - -RUN cargo build --package stump_server --bin stump_server --release --target x86_64-unknown-linux-musl && \ - cp target/x86_64-unknown-linux-musl/release/stump_server ./stump - -###################### -## Conditional step ## -###################### - -# Conditional to skip non-targetarch build stages -FROM ${TARGETARCH}-backend AS core-builder - -# ------------------------------------------------------------------------------ -# Final Stage -# ------------------------------------------------------------------------------ -FROM alpine:latest - -# libc6-compat -RUN apk add --no-cache libstdc++ binutils - -# Create the user/group for stump -RUN addgroup -g 1000 stump -RUN adduser -D -s /bin/sh -u 1000 -G stump stump - -WORKDIR / - -# create the config, data and app directories -RUN mkdir -p config && \ - mkdir -p data && \ - mkdir -p app - -# FIXME: this does not seem to be working... -# make the stump user own the directories -RUN chown stump /config && \ - chown stump /data && \ - chown stump /app - -USER stump - -# copy the binary -COPY --chown=stump:stump --from=core-builder /app/stump ./app/stump - -# copy the react build -COPY --from=frontend /app/build ./app/client - -# TODO: replace this with something more elegant lol maybe a bash case statement -RUN ln -s /lib/ld-musl-aarch64.so.1 /lib/ld-linux-aarch64.so.1; exit 0 - -# Default Stump environment variables -ENV STUMP_CONFIG_DIR=/config -ENV STUMP_CLIENT_DIR=/app/client -ENV STUMP_PROFILE=release -ENV STUMP_PORT=10801 -ENV STUMP_IN_DOCKER=true - -WORKDIR /app - -CMD ["./stump"] diff --git a/README.md b/README.md index f5a890a94..d5b3286a8 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ - - Docker Pulls + + Docker Pulls

@@ -29,6 +29,7 @@ Stump is a free and open source comics, manga and digital book server with OPDS Screenshot of Stump

+
Table of Contents

@@ -39,14 +40,13 @@ Stump is a free and open source comics, manga and digital book server with OPDS - [Where to start?](#where-to-start) - [Project Structure 📦](#project-structure-) - [/apps](#apps) - - [/common](#common) + - [/packages](#packages) - [/core](#core) - [Similar Projects 👯](#similar-projects-) - [Acknowledgements 🙏](#acknowledgements-) -

-
+ -> **🚧 Disclaimer 🚧**: Stump is _very much_ an ongoing **WIP**, under active development. Anyone is welcome to try it out, but please keep in mind that installation and general usage at this point should be for **testing purposes only**. Do **not** expect a fully featured, bug-free experience if you spin up a development environment or use a testing Docker image. Before the first release, I will likely flatten the migrations anyways, which would break anyone's Stump installations. If you'd like to contribute and help expedite Stump's first release, please see the [contributing guide](https://www.stumpapp.dev/contributing) for more information on how you can help. Otherwise, stay tuned for the first release! +> **🚧 Disclaimer 🚧**: Stump is _very much_ an ongoing **WIP**, under active development. Anyone is welcome to try it out, but please keep in mind that installation and general usage at this point should be for **testing purposes only**. Do **not** expect a fully featured, bug-free experience if you spin up a development environment or use a testing Docker image. Before the first release, I will likely flatten the migrations anyways, which would break anyone's Stump installations. If you'd like to contribute and help expedite Stump's first release, please review the [developer guide](#developing-). Otherwise, stay tuned for the first release! ## Roadmap 🗺 @@ -65,9 +65,12 @@ The following items are the major targets for Stump's first release: Things you can expect to see after the first release: -- 🖥️ Desktop app ([Tauri](https://tauri.app/)) +- 🖥️ Desktop app ([Tauri](https://github.com/aaronleopold/stump/tree/main/apps/desktop)) - 📱 Mobile app ([Tachiyomi](https://github.com/aaronleopold/tachiyomi-extensions) and/or [custom application](https://github.com/aaronleopold/stump/tree/main/apps/mobile)) -- 📺 A utility [TUI](https://github.com/aaronleopold/stump/tree/main/apps/tui) for managing a Stump instance from the command line + +Things you might see in the future: + +- 📺 A utility [TUI](https://github.com/aaronleopold/stump/tree/main/apps/tui) for managing a Stump instance(s) from the command line I am very open to suggestions and ideas, so feel free to reach out if you have anything you'd like to see! @@ -77,13 +80,13 @@ I am very open to suggestions and ideas, so feel free to reach out if you have a Stump isn't ready for normal, non-development usage yet. Once a release has been made, this will be updated. For now, follow the [Developing](#developing-) section to build from source and run locally. -There is a [docker image](https://hub.docker.com/repository/docker/aaronleopold/stump-preview) available for those interested. However, **this is only meant for testing purposes and will not be updated frequently**, so do not expect a fully featured, bug-free experience if you spin up a container. +There is a [docker image](https://hub.docker.com/repository/docker/aaronleopold/stump) available for those interested. However, **this is only meant for testing purposes and will not be updated frequently**, so do not expect a fully featured, bug-free experience if you spin up a container. Also keep in mind migrations won't be stacked until a release, so each update until then might require a wipe of the database file. For more information about getting started, how Stump works and how it manages your library, and much more, please visit [stumpapp.dev](https://stumpapp.dev/guides). ## Developer Guide 💻 -Contributions are very **encouraged** and **welcome**! Please review the [contributing guide](https://www.stumpapp.dev/contributing) for more thorough information on how to get started. +Contributions are very **encouraged** and **welcome**! Please review the [CONTRIBUTING.md](https://github.com/aaronleopold/stump/tree/develop/.github/CONTRIBUTING.md) before getting started. A quick summary of the steps required to get going: @@ -99,9 +102,13 @@ pnpm run setup 4. Start one of the apps: +I use [moonrepo](https://moonrepo.dev/) for Stump's repository management + ```bash -pnpm dev:web # Web app -pnpm dev:desktop # Desktop app +# webapp + server +moon run :dev +# desktop app + server +moon run server:start desktop:desktop-dev ``` And that's it! @@ -116,15 +123,12 @@ Some other good places to start: - Translation, so Stump is accessible to non-English speakers. - An automated translation system would be immensely helpful! If you're knowledgeable in this area, please reach out! -- Writing comprehensive integration tests. - - [look here](https://github.com/aaronleopold/stump/tree/develop/core/integration-tests) +- Writing comprehensive [integration tests](https://github.com/aaronleopold/stump/tree/develop/core/integration-tests). - Designing and/or implementing UI elements. - Docker build optimizations (it is currently _horrendously_ slow). -- CI pipelines / workflows (given above issue with Docker is resolved). +- CI pipelines / workflows. - And lots more! -I keep track of all non-code contributions in the [CONTRIBUTORS.md](https://github.com/aaronleopold/stump/tree/develop/.github/CONTRIBUTORS.md) file. If you contribute in that manner, please add yourself to the list! - [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/6434946-9cf51d71-d680-46f5-89da-7b6cf7213a20?action=collection%2Ffork&collection-url=entityId%3D6434946-9cf51d71-d680-46f5-89da-7b6cf7213a20%26entityType%3Dcollection%26workspaceId%3D722014ea-55eb-4a49-b29d-814300c1016d) ## Project Structure 📦 @@ -137,15 +141,16 @@ Stump has a monorepo structure that follows a similar pattern to that of [Spaced - `server`: An [Axum](https://github.com/tokio-rs/axum) server. - `web`: The React application that is served by the Axum server. -### /common +### /packages - `client`: Everything needed to create a react-based client for Stump. Contains Zustand and React Query configuration, used by the `interface` package, as well as the generated TypeScript types. - `config`: Configuration files for the project, e.g. `tsconfig.json`, etc. - `interface`: Stump's main React-based interface, shared between the web and desktop applications. +- `prisma-cli`: A small rust app to run the prisma cli (generating the prisma client) ### /core -- `core`: Stump's 'core' functionality is located here, written in Rust. The `server` was previously part of the core, but was extracted to support integration testing. +- `core`: Stump's 'core' functionality is located here, written in Rust. The `server` was previously part of the core, but was extracted for better isolation. ## Similar Projects 👯 diff --git a/apps/desktop/dist/.placeholder b/apps/desktop/dist/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/apps/desktop/moon.yml b/apps/desktop/moon.yml new file mode 100644 index 000000000..27ae2bb82 --- /dev/null +++ b/apps/desktop/moon.yml @@ -0,0 +1,43 @@ +type: 'application' + +workspace: + inheritedTasks: + exclude: ['buildPackage'] + +fileGroups: + app: + - 'src/**/*' + - 'src-tauri/**/*' + +language: 'rust' + +tasks: + # Note: naming it not 'dev' so I can run web+server easier + desktop-dev: + command: 'pnpm tauri dev' + local: true + + lint: + command: 'cargo clippy --package stump_desktop -- -D warnings' + options: + mergeArgs: 'replace' + mergeDeps: 'replace' + mergeInputs: 'replace' + + format: + command: 'cargo fmt --package stump_desktop' + options: + mergeArgs: 'replace' + mergeDeps: 'replace' + mergeInputs: 'replace' + + # # TODO: need to have more targets. + # build: + # # tauri build --target universal-apple-darwin + # command: 'pnpm tauri build' + # local: true + # deps: + # - '~:build-webapp' + + # build-webapp: + # command: 'pnpm vite build' diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 5424a2b54..4070fd707 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -7,31 +7,31 @@ "tauri": "tauri", "vite": "vite --", "dev": "tauri dev", - "build": "tauri build", - "build:mac": "tauri build --target universal-apple-darwin", + "build": "pnpm build:web && tauri build", + "build:mac": "pnpm build:web && tauri build --target universal-apple-darwin", "build:web": "vite build" }, "dependencies": { "@stump/client": "workspace:*", "@stump/interface": "workspace:*", - "@tanstack/react-query": "^4.10.3", - "@tauri-apps/api": "^1.1.0", + "@tanstack/react-query": "^4.20.4", + "@tauri-apps/api": "^1.2.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { - "@tailwindcss/typography": "^0.5.7", - "@tauri-apps/cli": "^1.1.1", - "@types/react": "^18.0.21", - "@types/react-dom": "^18.0.6", + "@tailwindcss/typography": "^0.5.9", + "@tauri-apps/cli": "^1.2.3", + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.11", "@vitejs/plugin-react": "^2.0.0", "autoprefixer": "^10.4.12", - "postcss": "^8.4.17", + "postcss": "^8.4.21", "tailwind": "^4.0.0", "tailwind-scrollbar-hide": "^1.1.7", - "tailwindcss": "^3.1.8", - "typescript": "^4.8.4", - "vite": "^3.1.6", + "tailwindcss": "^3.2.7", + "typescript": "^4.9.5", + "vite": "^3.2.5", "vite-plugin-tsconfig-paths": "^1.1.0" } } \ No newline at end of file diff --git a/apps/desktop/postcss.config.js b/apps/desktop/postcss.config.js index e873f1a4f..65994d328 100644 --- a/apps/desktop/postcss.config.js +++ b/apps/desktop/postcss.config.js @@ -1,6 +1,6 @@ module.exports = { plugins: { - tailwindcss: {}, autoprefixer: {}, + tailwindcss: {}, }, -}; +} diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 85a04c966..12c18fafb 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -7,12 +7,12 @@ license = "MIT" edition = "2021" [build-dependencies] -tauri-build = { version = "1.0.4", features = [] } +tauri-build = { version = "1.1.1", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.0.5", features = ["api-all", "devtools"] } +tauri = { version = "1.1.1", features = ["api-all", "devtools"] } ### MISC ### discord-rich-presence = "0.2.3" diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 75aca7823..406ebc18d 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,69 +1,68 @@ { - "$schema": "../node_modules/@tauri-apps/cli/schema.json", - "build": { - "beforeBuildCommand": "pnpm build:web", - "beforeDevCommand": "pnpm vite --clearScreen=false", - "devPath": "http://localhost:3000", - "distDir": "../dist" - }, - "package": { - "productName": "Stump", - "version": "0.0.0" - }, - "tauri": { - "allowlist": { - "all": true - }, - "bundle": { - "active": true, - "category": "DeveloperTool", - "copyright": "", - "deb": { - "depends": [] - }, - "externalBin": [], - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ], - "identifier": "com.oromei.stump", - "longDescription": "", - "macOS": { - "entitlements": null, - "exceptionDomain": "", - "frameworks": [], - "providerShortName": null, - "signingIdentity": null - }, - "resources": [], - "shortDescription": "", - "targets": "all", - "windows": { - "certificateThumbprint": null, - "digestAlgorithm": "sha256", - "timestampUrl": "" - } - }, - "security": { - "csp": null - }, - "updater": { - "active": false - }, - "windows": [ - { - "fullscreen": false, - "height": 700, - "resizable": true, - "title": "Stump", - "width": 1200, - "decorations": true, - "transparent": false, - "center": true - } - ] - } + "$schema": "../node_modules/@tauri-apps/cli/schema.json", + "build": { + "beforeDevCommand": "pnpm vite --clearScreen=false", + "devPath": "http://localhost:3000", + "distDir": "../../web/dist" + }, + "package": { + "productName": "Stump", + "version": "0.0.0" + }, + "tauri": { + "allowlist": { + "all": true + }, + "bundle": { + "active": true, + "category": "DeveloperTool", + "copyright": "", + "deb": { + "depends": [] + }, + "externalBin": [], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "identifier": "com.oromei.stump", + "longDescription": "", + "macOS": { + "entitlements": null, + "exceptionDomain": "", + "frameworks": [], + "providerShortName": null, + "signingIdentity": null + }, + "resources": [], + "shortDescription": "", + "targets": "all", + "windows": { + "certificateThumbprint": null, + "digestAlgorithm": "sha256", + "timestampUrl": "" + } + }, + "security": { + "csp": null + }, + "updater": { + "active": false + }, + "windows": [ + { + "fullscreen": false, + "height": 700, + "resizable": true, + "title": "Stump", + "width": 1200, + "decorations": true, + "transparent": false, + "center": true + } + ] + } } \ No newline at end of file diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index ba538ea56..0f7ba85fe 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -1,58 +1,52 @@ -import { useEffect, useState } from 'react'; - -import { Platform, StumpQueryProvider } from '@stump/client'; -import { os, invoke } from '@tauri-apps/api'; - -import StumpInterface from '@stump/interface'; - -import '@stump/interface/styles'; +import { Platform } from '@stump/client' +import StumpInterface from '@stump/interface' +import { invoke, os } from '@tauri-apps/api' +import { useEffect, useState } from 'react' export default function App() { function getPlatform(platform: string): Platform { switch (platform) { case 'darwin': - return 'macOS'; + return 'macOS' case 'win32': - return 'windows'; + return 'windows' case 'linux': - return 'linux'; + return 'linux' default: - return 'browser'; + return 'browser' } } const setDiscordPresence = (status?: string, details?: string) => - invoke('set_discord_presence', { status, details }); + invoke('set_discord_presence', { details, status }) const setUseDiscordPresence = (connect: boolean) => - invoke('set_use_discord_connection', { connect }); + invoke('set_use_discord_connection', { connect }) - const [platform, setPlatform] = useState('unknown'); - const [mounted, setMounted] = useState(false); + const [platform, setPlatform] = useState('unknown') + const [mounted, setMounted] = useState(false) useEffect(() => { os.platform().then((platform) => { - setPlatform(getPlatform(platform)); + setPlatform(getPlatform(platform)) // TODO: remove this, should be handled in the interface :D - setUseDiscordPresence(true); - setDiscordPresence(); + setUseDiscordPresence(true) + setDiscordPresence() // ^^ - setMounted(true); - }); - }, []); + setMounted(true) + }) + }, []) // I want to wait until platform is properly set before rendering the interface if (!mounted) { - return null; + return null } return ( - - - - ); + + ) } diff --git a/apps/desktop/tailwind.config.js b/apps/desktop/tailwind.config.js index b902b45ed..6ccd50bf8 100644 --- a/apps/desktop/tailwind.config.js +++ b/apps/desktop/tailwind.config.js @@ -1 +1 @@ -module.exports = require('../../common/config/tailwind.js')('desktop'); +module.exports = require('../../packages/components/tailwind.js')('desktop') diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index a7354a4e4..e528e9f08 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -1,7 +1,34 @@ { - "extends": "../../common/config/base.tsconfig.json", - "compilerOptions": { - "types": ["vite/client"] - }, - "include": ["src"] + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": [ + "vite/client" + ], + "outDir": "../../.moon/cache/types/apps/desktop", + "paths": { + "@stump/client": [ + "../../packages/client/src/index.ts" + ], + "@stump/client/*": [ + "../../packages/client/src/*" + ], + "@stump/interface": [ + "../../packages/interface/src/index.ts" + ], + "@stump/interface/*": [ + "../../packages/interface/src/*" + ] + } + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../packages/client" + }, + { + "path": "../../packages/interface" + } + ] } diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index 9851602b6..724934fa6 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -1,26 +1,25 @@ +import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; import tsconfigPaths from 'vite-plugin-tsconfig-paths'; -import react from '@vitejs/plugin-react'; - import { name, version } from './package.json'; // TODO: move this to common/config? // https://vitejs.dev/config/ export default defineConfig({ - server: { - port: 3000, - }, - plugins: [react(), tsconfigPaths()], - root: 'src', - publicDir: '../../../common/interface/public', base: '/', - define: { - pkgJson: { name, version }, - }, build: { - outDir: '../dist', assetsDir: './assets', manifest: true, + outDir: '../dist', + }, + define: { + pkgJson: { name, version }, + }, + plugins: [react(), tsconfigPaths()], + publicDir: '../../../packages/interface/public', + root: 'src', + server: { + port: 3000, }, }); diff --git a/apps/mobile/package.json b/apps/mobile/package.json index f190c32c4..f1b26fee9 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -1,8 +1,8 @@ { - "name": "@stump/mobile", - "version": "0.0.0", - "description": "", - "license": "MIT", - "scripts": {}, - "keywords": [] -} \ No newline at end of file + "name": "@stump/mobile", + "version": "0.0.0", + "description": "", + "license": "MIT", + "scripts": {}, + "keywords": [] +} diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json new file mode 100644 index 000000000..9811ff19f --- /dev/null +++ b/apps/mobile/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.options.json", + "include": [ + "**/*" + ], + "references": [], + "compilerOptions": { + "outDir": "../../.moon/cache/types/apps/mobile" + } +} diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml index 1a9f4d537..b5174a2e6 100644 --- a/apps/server/Cargo.toml +++ b/apps/server/Cargo.toml @@ -1,42 +1,50 @@ [package] name = "stump_server" -version.workspace = true +version = { workspace = true } edition = "2021" default-run = "stump_server" [dependencies] stump_core = { path = "../../core" } -prisma-client-rust.workspace = true -axum = { version = "0.5.16", features = ["ws"] } -axum-macros = "0.2.3" -axum-extra = { version = "0.3.7", features = [ +prisma-client-rust = { workspace = true } +axum = { version = "0.6.1", features = ["ws"] } +axum-macros = "0.3.0" +axum-extra = { version = "0.4.2", features = [ "spa", # "cookie" + "query" ] } -tower-http = { version = "0.3.4", features = [ +tower-http = { version = "0.3.5", features = [ "fs", "cors", "set-header" ] } hyper = "0.14.20" serde_json = "1.0.85" +serde_with = "2.1.0" # used for the ws stuff futures-util = "0.3.24" # axum-typed-websockets = "0.4.0" -tokio.workspace = true +tokio = { workspace = true } tokio-util = "0.7.4" -serde.workspace = true -axum-sessions = "0.3.1" +serde = { workspace = true } +axum-sessions = "0.4.1" async-trait = "0.1.53" -async-stream = "0.3.3" +async-stream = { workspace = true } +# TODO: figure out this super fucking annoying cargo dependency resolution issue. This is the second time +# cargo, in docker, has ignored the workspace version of this dep and instead used the latest version from crates.io +# local-ip-address = "0.5.1" +local-ip-address = { git = "https://github.com/EstebanBorai/local-ip-address.git", tag = "v0.5.1" } ### Dev Utils ### rand = "0.8.5" +utoipa = { version = "3.0.3", features = ["axum_extras"] } +utoipa-swagger-ui = { version = "3.0.2", features = ["axum"] } ### Error Handling + Logging ### -tracing.workspace = true -thiserror.workspace = true +tracing = { workspace = true } +thiserror = { workspace = true } ### Auth ### bcrypt = "0.10.1" @@ -53,4 +61,4 @@ openssl = { version = "0.10.40", features = ["vendored"] } openssl = { version = "0.10.40", features = ["vendored"] } [build-dependencies] -chrono = "0.4.19" \ No newline at end of file +chrono = "0.4.19" diff --git a/apps/server/build.rs b/apps/server/build.rs index cd1b98f2f..3fa46e9f4 100644 --- a/apps/server/build.rs +++ b/apps/server/build.rs @@ -1,9 +1,9 @@ use chrono::prelude::{DateTime, Utc}; -use std::process::Command; +use std::{env, process::Command}; fn get_git_rev() -> Option { let output = Command::new("git") - .args(&["rev-parse", "--short", "HEAD"]) + .args(["rev-parse", "--short", "HEAD"]) .output() .ok()?; @@ -23,7 +23,12 @@ fn get_compile_date() -> String { fn main() { println!("cargo:rustc-env=STATIC_BUILD_DATE={}", get_compile_date()); - if let Some(rev) = get_git_rev() { + let maybe_rev = match env::var("GIT_REV") { + Ok(rev) => Some(rev), + _ => get_git_rev(), + }; + + if let Some(rev) = maybe_rev { println!("cargo:rustc-env=GIT_REV={}", rev); } } diff --git a/apps/server/moon.yml b/apps/server/moon.yml new file mode 100644 index 000000000..2a7d799a7 --- /dev/null +++ b/apps/server/moon.yml @@ -0,0 +1,56 @@ +type: 'application' + +workspace: + inheritedTasks: + exclude: ['buildPackage'] + +fileGroups: + app: + - 'src/**/*' + +language: 'rust' + +tasks: + dev: + command: 'cargo watch --ignore packages -x "run --manifest-path=apps/server/Cargo.toml --package stump_server"' + local: true + options: + runFromWorkspaceRoot: true + + start: + command: 'cargo run --release --package stump_server' + local: true + + build: + command: 'cargo build --release --package stump_server' + local: true + deps: + - 'web:build' + - '~:get-webapp' + + lint: + command: 'cargo clippy --package stump_server -- -D warnings' + options: + mergeArgs: 'replace' + mergeDeps: 'replace' + mergeInputs: 'replace' + + format: + command: 'cargo fmt --package stump_server' + options: + mergeArgs: 'replace' + mergeDeps: 'replace' + mergeInputs: 'replace' + + clean: + command: 'cargo clean' + + delete-webapp: + command: 'rm -rf ./dist' + platform: 'system' + + get-webapp: + command: 'cp -r ../web/dist ./dist' + platform: 'system' + deps: + - '~:delete-webapp' diff --git a/apps/server/package.json b/apps/server/package.json index 02006a075..a122cdf55 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -3,14 +3,8 @@ "private": true, "version": "0.0.0", "scripts": { - "check": "cargo check", - "start": "cargo run --release", - "dev": "cargo watch -x run", "build": "pnpm get-client && cargo build --release && pnpm move-client", "get-client": "trash \"dist/*\" \"!dist/.placeholder\" && cpy \"../web/dist/**/*\" ./dist/", - "move-client": "trash ../../target/release/dist && cp -r ./dist ../../target/release/dist", - "fmt": "cargo fmt --all --manifest-path=./Cargo.toml --", - "benchmarks": "cargo test --benches", - "test": "cargo test" + "move-client": "trash ../../target/release/dist && cp -r ./dist ../../target/release/dist" } } \ No newline at end of file diff --git a/apps/server/src/config/cors.rs b/apps/server/src/config/cors.rs index 8089ae143..4bc1cc149 100644 --- a/apps/server/src/config/cors.rs +++ b/apps/server/src/config/cors.rs @@ -4,15 +4,34 @@ use axum::http::{ header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}, HeaderValue, Method, }; +use local_ip_address::local_ip; use tower_http::cors::{AllowOrigin, CorsLayer}; -use tracing::error; +use tracing::{error, trace}; -const DEBUG_ALLOWED_ORIGINS: &[&str] = &["http://localhost:3000", "http://0.0.0.0:3000"]; +use crate::config::utils::is_debug; const DEFAULT_ALLOWED_ORIGINS: &[&str] = &["tauri://localhost", "https://tauri.localhost"]; +const DEBUG_ALLOWED_ORIGINS: &[&str] = &[ + "tauri://localhost", + "https://tauri.localhost", + "http://localhost:3000", + "http://0.0.0.0:3000", +]; + +fn merge_origins(origins: &[&str], local_origins: Vec) -> Vec { + origins + .iter() + .map(|origin| origin.to_string()) + .chain(local_origins.into_iter()) + .map(|origin| origin.parse()) + .filter_map(|res| res.ok()) + .collect::>() +} + +pub fn get_cors_layer(port: u16) -> CorsLayer { + let is_debug = is_debug(); -pub fn get_cors_layer() -> CorsLayer { let allowed_origins = match env::var("STUMP_ALLOWED_ORIGINS") { Ok(val) => { if val.is_empty() { @@ -37,31 +56,49 @@ pub fn get_cors_layer() -> CorsLayer { Err(_) => None, }; + let local_ip = local_ip() + .map_err(|e| { + error!("Failed to get local ip: {:?}", e); + e + }) + .map(|ip| ip.to_string()) + .unwrap_or_default(); + + // Format the local IP with both http and https, and the port. If is_debug is true, + // then also add port 3000. + let local_orgins = if !local_ip.is_empty() { + let mut base = vec![ + format!("http://{local_ip}:{port}"), + format!("https://{local_ip}:{port}"), + ]; + + if is_debug { + base.append(&mut vec![ + format!("http://{local_ip}:3000",), + format!("https://{local_ip}:3000"), + ]); + } + + base + } else { + vec![] + }; + let mut cors_layer = CorsLayer::new(); if let Some(origins_list) = allowed_origins { + // TODO: consider adding some config to allow for this list to be appended to defaults, rather than + // completely overriding them. cors_layer = cors_layer.allow_origin(AllowOrigin::list(origins_list)); - } else if env::var("STUMP_PROFILE").unwrap_or_else(|_| "release".into()) == "debug" { - cors_layer = cors_layer.allow_origin( - DEBUG_ALLOWED_ORIGINS - .iter() - .map(|origin| origin.parse()) - .filter_map(|res| res.ok()) - .collect::>(), - ); + } else if is_debug { + let debug_origins = merge_origins(DEBUG_ALLOWED_ORIGINS, local_orgins); + cors_layer = cors_layer.allow_origin(debug_origins); } else { - cors_layer = cors_layer.allow_origin( - DEFAULT_ALLOWED_ORIGINS - .iter() - .map(|origin| origin.parse()) - .filter_map(|res| res.ok()) - .collect::>(), - ); + let release_origins = merge_origins(DEFAULT_ALLOWED_ORIGINS, local_orgins); + cors_layer = cors_layer.allow_origin(release_origins); } - // TODO: finalize what cors should be... fucking hate cors lmao - cors_layer - // .allow_methods(Any) + cors_layer = cors_layer .allow_methods([ Method::GET, Method::PUT, @@ -71,5 +108,10 @@ pub fn get_cors_layer() -> CorsLayer { Method::CONNECT, ]) .allow_headers([ACCEPT, AUTHORIZATION, CONTENT_TYPE]) - .allow_credentials(true) + .allow_credentials(true); + + #[cfg(debug_assertions)] + trace!(?cors_layer, "Cors configuration complete"); + + cors_layer } diff --git a/apps/server/src/config/session.rs b/apps/server/src/config/session.rs index 4a51b827d..ed8cb0cff 100644 --- a/apps/server/src/config/session.rs +++ b/apps/server/src/config/session.rs @@ -25,13 +25,20 @@ pub fn get_session_layer() -> SessionLayer { .with_session_ttl(Some(Duration::from_secs(3600 * 24 * 3))) .with_cookie_path("/"); - if env::var("STUMP_PROFILE").unwrap_or_else(|_| "release".into()) == "release" { - sesssion_layer - .with_same_site_policy(SameSite::None) - .with_secure(true) - } else { - sesssion_layer - .with_same_site_policy(SameSite::Lax) - .with_secure(false) - } + sesssion_layer + .with_same_site_policy(SameSite::Lax) + .with_secure(false) + + // FIXME: I think this can be configurable, but most people are going to be insecurely + // running this, which means `secure` needs to be false otherwise the cookie won't + // be sent. + // if env::var("STUMP_PROFILE").unwrap_or_else(|_| "release".into()) == "release" { + // sesssion_layer + // .with_same_site_policy(SameSite::None) + // .with_secure(true) + // } else { + // sesssion_layer + // .with_same_site_policy(SameSite::Lax) + // .with_secure(false) + // } } diff --git a/apps/server/src/config/state.rs b/apps/server/src/config/state.rs index 7b4866c08..2c46888f7 100644 --- a/apps/server/src/config/state.rs +++ b/apps/server/src/config/state.rs @@ -1,8 +1,15 @@ use std::sync::Arc; -use axum::Extension; -use stump_core::config::Ctx; +use axum::extract::State; +use axum_macros::FromRequestParts; +use stump_core::prelude::Ctx; // TODO: I don't feel like I need this module... Unless I add things to it.. +pub type AppState = Arc; -pub type State = Extension>; +// TODO: is this how to fix the FIXME note in auth extractor? +#[derive(FromRequestParts, Clone)] +pub struct _AppState { + #[allow(unused)] + core_ctx: State, +} diff --git a/apps/server/src/config/utils.rs b/apps/server/src/config/utils.rs index 89f287b7d..d7d64539a 100644 --- a/apps/server/src/config/utils.rs +++ b/apps/server/src/config/utils.rs @@ -3,3 +3,7 @@ use std::env; pub(crate) fn get_client_dir() -> String { env::var("STUMP_CLIENT_DIR").unwrap_or_else(|_| "./dist".to_string()) } + +pub(crate) fn is_debug() -> bool { + env::var("STUMP_PROFILE").unwrap_or_else(|_| "release".into()) == "debug" +} diff --git a/apps/server/src/errors.rs b/apps/server/src/errors.rs index f196cc7cb..f7f292639 100644 --- a/apps/server/src/errors.rs +++ b/apps/server/src/errors.rs @@ -8,9 +8,10 @@ use prisma_client_rust::{ }; use stump_core::{ event::InternalCoreTask, - types::{errors::ProcessFileError, CoreError}, + prelude::{CoreError, ProcessFileError}, }; use tokio::sync::mpsc; +use utoipa::ToSchema; use std::net; use thiserror::Error; @@ -71,7 +72,7 @@ impl IntoResponse for AuthError { } #[allow(unused)] -#[derive(Debug, Error)] +#[derive(Debug, Error, ToSchema)] pub enum ApiError { #[error("{0}")] BadRequest(String), @@ -94,9 +95,18 @@ pub enum ApiError { #[error("{0}")] Redirect(String), #[error("{0}")] + #[schema(value_type = String)] PrismaError(#[from] QueryError), } +impl ApiError { + pub fn forbidden_discreet() -> ApiError { + ApiError::Forbidden(String::from( + "You do not have permission to access this resource.", + )) + } +} + impl From for ApiError { fn from(err: CoreError) -> Self { match err { diff --git a/apps/server/src/main.rs b/apps/server/src/main.rs index 361a5251f..548ae982b 100644 --- a/apps/server/src/main.rs +++ b/apps/server/src/main.rs @@ -1,6 +1,6 @@ use std::net::SocketAddr; -use axum::{Extension, Router}; +use axum::Router; use errors::{ServerError, ServerResult}; use stump_core::{config::logging::init_tracing, StumpCore}; use tracing::{error, info, trace}; @@ -34,6 +34,7 @@ async fn main() -> ServerResult<()> { return Err(ServerError::ServerStartError(err.to_string())); } let stump_environment = stump_environment.unwrap(); + let port = stump_environment.port.unwrap_or(10801); // Note: init_tracing after loading the environment so the correct verbosity // level is used for logging. @@ -50,16 +51,18 @@ async fn main() -> ServerResult<()> { } let server_ctx = core.get_context(); + let app_state = server_ctx.arced(); + let cors_layer = cors::get_cors_layer(port); info!("{}", core.get_shadow_text()); let app = Router::new() - .merge(routers::mount()) - .layer(Extension(server_ctx.arced())) + .merge(routers::mount(app_state.clone())) + .with_state(app_state.clone()) .layer(session::get_session_layer()) - .layer(cors::get_cors_layer()); + .layer(cors_layer); - let addr = SocketAddr::from(([0, 0, 0, 0], stump_environment.port.unwrap_or(10801))); + let addr = SocketAddr::from(([0, 0, 0, 0], port)); info!("⚡️ Stump HTTP server starting on http://{}", addr); axum::Server::bind(&addr) diff --git a/apps/server/src/middleware/auth.rs b/apps/server/src/middleware/auth.rs index 9e187e6e1..4eef02336 100644 --- a/apps/server/src/middleware/auth.rs +++ b/apps/server/src/middleware/auth.rs @@ -1,10 +1,8 @@ -use std::sync::Arc; - use async_trait::async_trait; use axum::{ body::BoxBody, - extract::{FromRequest, RequestParts}, - http::{header, Method, StatusCode}, + extract::{FromRef, FromRequestParts}, + http::{header, request::Parts, Method, StatusCode}, response::{IntoResponse, Response}, }; use axum_sessions::SessionHandle; @@ -12,28 +10,43 @@ use prisma_client_rust::{ prisma_errors::query_engine::{RecordNotFound, UniqueKeyViolation}, QueryError, }; -use stump_core::{config::Ctx, prisma::user, types::User}; +use stump_core::{db::models::User, prisma::user}; use tracing::{error, trace}; -use crate::utils::{decode_base64_credentials, verify_password}; +use crate::{ + config::state::AppState, + utils::{decode_base64_credentials, verify_password}, +}; pub struct Auth; #[async_trait] -impl FromRequest for Auth +impl FromRequestParts for Auth where - B: Send, + AppState: FromRef, + S: Send + Sync, { type Rejection = Response; - async fn from_request(req: &mut RequestParts) -> Result { + async fn from_request_parts( + parts: &mut Parts, + state: &S, + ) -> Result { // Note: this is fine, right? I mean, it's not like we're doing anything // on a OPTIONS request, right? Right? 👀 - if req.method() == Method::OPTIONS { + if parts.method == Method::OPTIONS { return Ok(Self); } - let session_handle = req.extensions().get::().unwrap(); + let state = AppState::from_ref(state); + let session_handle = + parts.extensions.get::().ok_or_else(|| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to extract session handle", + ) + .into_response() + })?; let session = session_handle.read().await; if let Some(user) = session.get::("user") { @@ -44,23 +57,16 @@ where // drop so we don't deadlock when writing to the session lol oy vey drop(session); - let ctx = req.extensions().get::>().unwrap(); - - // TODO: figure me out plz - // let cookie_jar = req.extensions().get::().unwrap(); - - // if let Some(cookie) = cookie_jar.get("stump_session") { - // println!("cookie: {:?}", cookie); - // } - - let auth_header = req - .headers() + let auth_header = parts + .headers .get(header::AUTHORIZATION) .and_then(|value| value.to_str().ok()); - let is_opds = req.uri().path().starts_with("/opds"); + let is_opds = parts.uri.path().starts_with("/opds"); + let has_auth_header = auth_header.is_some(); + trace!(is_opds, has_auth_header, uri = ?parts.uri, "Checking auth header"); - if auth_header.is_none() { + if !has_auth_header { if is_opds { return Err(BasicAuth.into_response()); } @@ -69,7 +75,6 @@ where } let auth_header = auth_header.unwrap(); - if !auth_header.starts_with("Basic ") || auth_header.len() <= 6 { return Err((StatusCode::UNAUTHORIZED).into_response()); } @@ -83,7 +88,7 @@ where (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response() })?; - let user = ctx + let user = state .db .user() .find_unique(user::username::equals(decoded_credentials.username.clone())) @@ -136,23 +141,26 @@ where /// /// Router::new() /// .layer(from_extractor::()) -/// .layer(from_extractor::()); +/// .layer(from_extractor_with_state::(app_state)); /// ``` pub struct AdminGuard; #[async_trait] -impl FromRequest for AdminGuard +impl FromRequestParts for AdminGuard where - B: Send, + S: Send + Sync, { type Rejection = StatusCode; - async fn from_request(req: &mut RequestParts) -> Result { - if req.method() == Method::OPTIONS { + async fn from_request_parts( + parts: &mut Parts, + _: &S, + ) -> Result { + if parts.method == Method::OPTIONS { return Ok(Self); } - let session_handle = req.extensions().get::().unwrap(); + let session_handle = parts.extensions.get::().unwrap(); let session = session_handle.read().await; if let Some(user) = session.get::("user") { diff --git a/apps/server/src/routers/api/auth.rs b/apps/server/src/routers/api/auth.rs deleted file mode 100644 index f8f1cf922..000000000 --- a/apps/server/src/routers/api/auth.rs +++ /dev/null @@ -1,132 +0,0 @@ -use axum::{ - routing::{get, post}, - Extension, Json, Router, -}; -use axum_sessions::extractors::{ReadableSession, WritableSession}; -use stump_core::{ - prisma::{user, user_preferences}, - types::{enums::UserRole, LoginOrRegisterArgs, User}, -}; - -use crate::{ - config::state::State, - errors::{ApiError, ApiResult}, - utils::{self, verify_password}, -}; - -pub(crate) fn mount() -> Router { - Router::new().nest( - "/auth", - Router::new() - .route("/me", get(viewer)) - .route("/login", post(login)) - .route("/logout", post(logout)) - .route("/register", post(register)), - ) -} - -async fn viewer(session: ReadableSession) -> ApiResult> { - if let Some(user) = session.get::("user") { - Ok(Json(user)) - } else { - Err(ApiError::Unauthorized) - } -} - -// Wow, this is really ugly syntax for state extraction imo... -async fn login( - Json(input): Json, - Extension(ctx): State, - mut session: WritableSession, -) -> ApiResult> { - let db = ctx.get_db(); - - if let Some(user) = session.get::("user") { - if input.username == user.username { - return Ok(Json(user)); - } - } - - let fetched_user = db - .user() - .find_unique(user::username::equals(input.username.to_owned())) - .with(user::user_preferences::fetch()) - .exec() - .await?; - - if let Some(db_user) = fetched_user { - let matches = verify_password(&db_user.hashed_password, &input.password)?; - if !matches { - return Err(ApiError::Unauthorized); - } - - let user: User = db_user.into(); - session.insert("user", user.clone()).unwrap(); - - return Ok(Json(user)); - } - - Err(ApiError::Unauthorized) -} - -async fn logout(mut session: WritableSession) -> ApiResult<()> { - session.destroy(); - Ok(()) -} - -pub async fn register( - Json(input): Json, - Extension(ctx): State, - session: ReadableSession, -) -> ApiResult> { - let db = ctx.get_db(); - - let has_users = db.user().find_first(vec![]).exec().await?.is_some(); - - let mut user_role = UserRole::default(); - - // server owners must register member accounts - if session.get::("user").is_none() && has_users { - return Err(ApiError::Forbidden( - "Must be server owner to register member accounts".to_string(), - )); - } else if !has_users { - // register the user as owner - user_role = UserRole::ServerOwner; - } - - let hashed_password = bcrypt::hash(&input.password, utils::get_hash_cost())?; - - let created_user = db - .user() - .create( - input.username.to_owned(), - hashed_password, - vec![user::role::set(user_role.into())], - ) - .exec() - .await?; - - // FIXME: these next two queries will be removed once nested create statements are - // supported on the prisma client. Until then, this ugly mess is necessary. - let _user_preferences = db - .user_preferences() - .create(vec![user_preferences::user::connect(user::id::equals( - created_user.id.clone(), - ))]) - .exec() - .await?; - - // This *really* shouldn't fail, so I am using unwrap here. It also doesn't - // matter too much in the long run since this query will go away once above fixme - // is resolved. - let user = db - .user() - .find_unique(user::id::equals(created_user.id)) - .with(user::user_preferences::fetch()) - .exec() - .await? - .unwrap(); - - Ok(Json(user.into())) -} diff --git a/apps/server/src/routers/api/job.rs b/apps/server/src/routers/api/job.rs deleted file mode 100644 index a7de614c0..000000000 --- a/apps/server/src/routers/api/job.rs +++ /dev/null @@ -1,68 +0,0 @@ -use axum::{ - extract::Path, - middleware::from_extractor, - routing::{delete, get}, - Extension, Json, Router, -}; -use stump_core::{event::InternalCoreTask, job::JobReport}; -use tokio::sync::oneshot; -use tracing::debug; - -use crate::{ - config::state::State, - errors::{ApiError, ApiResult}, - middleware::auth::{AdminGuard, Auth}, -}; - -pub(crate) fn mount() -> Router { - Router::new() - .nest( - "/jobs", - Router::new() - .route("/", get(get_job_reports).delete(delete_job_reports)) - .route("/:id/cancel", delete(cancel_job)), - ) - .layer(from_extractor::()) - .layer(from_extractor::()) -} - -/// Get all running/pending jobs. -async fn get_job_reports(Extension(ctx): State) -> ApiResult>> { - let (task_tx, task_rx) = oneshot::channel(); - - ctx.internal_task(InternalCoreTask::GetJobReports(task_tx)) - .map_err(|e| { - ApiError::InternalServerError(format!( - "Failed to submit internal task: {}", - e - )) - })?; - - let res = task_rx.await.map_err(|e| { - ApiError::InternalServerError(format!("Failed to get job report: {}", e)) - })??; - - Ok(Json(res)) -} - -async fn delete_job_reports(Extension(ctx): State) -> ApiResult<()> { - let result = ctx.db.job().delete_many(vec![]).exec().await?; - debug!("Deleted {} job reports", result); - Ok(()) -} - -async fn cancel_job(Extension(ctx): State, Path(job_id): Path) -> ApiResult<()> { - let (task_tx, task_rx) = oneshot::channel(); - - ctx.internal_task(InternalCoreTask::CancelJob { - job_id, - return_sender: task_tx, - }) - .map_err(|e| { - ApiError::InternalServerError(format!("Failed to submit internal task: {}", e)) - })?; - - Ok(task_rx.await.map_err(|e| { - ApiError::InternalServerError(format!("Failed to cancel job: {}", e)) - })??) -} diff --git a/apps/server/src/routers/api/library.rs b/apps/server/src/routers/api/library.rs deleted file mode 100644 index 3aaeeb19c..000000000 --- a/apps/server/src/routers/api/library.rs +++ /dev/null @@ -1,483 +0,0 @@ -use axum::{ - extract::{Path, Query}, - middleware::from_extractor, - routing::get, - Extension, Json, Router, -}; -use axum_sessions::extractors::ReadableSession; -use prisma_client_rust::{raw, Direction}; -use serde::Deserialize; -use std::{path, str::FromStr}; -use tracing::{debug, error, trace}; - -use stump_core::{ - db::utils::PrismaCountTrait, - fs::{image, media_file}, - job::LibraryScanJob, - prisma::{ - library, library_options, media, - series::{self, OrderByParam as SeriesOrderByParam}, - tag, - }, - types::{ - CreateLibraryArgs, FindManyTrait, LibrariesStats, Library, LibraryScanMode, - Pageable, PagedRequestParams, QueryOrder, Series, UpdateLibraryArgs, - }, -}; - -use crate::{ - config::state::State, - errors::{ApiError, ApiResult}, - middleware::auth::Auth, - utils::{ - get_session_admin_user, - http::{ImageResponse, PageableTrait}, - }, -}; - -// TODO: .layer(from_extractor::()) where needed. Might need to remove some -// of the nesting -pub(crate) fn mount() -> Router { - Router::new() - .route("/libraries", get(get_libraries).post(create_library)) - .route("/libraries/stats", get(get_libraries_stats)) - .nest( - "/libraries/:id", - Router::new() - .route( - "/", - get(get_library_by_id) - .put(update_library) - .delete(delete_library), - ) - .route("/scan", get(scan_library)) - .route("/series", get(get_library_series)) - .route("/thumbnail", get(get_library_thumbnail)), - ) - .layer(from_extractor::()) -} - -/// Get all libraries -async fn get_libraries( - Extension(ctx): State, - pagination: Query, -) -> ApiResult>>> { - let libraries = ctx - .db - .library() - .find_many(vec![]) - .with(library::tags::fetch(vec![])) - .with(library::library_options::fetch()) - .order_by(library::name::order(Direction::Asc)) - .exec() - .await? - .into_iter() - .map(|l| l.into()) - .collect::>(); - - let unpaged = pagination.unpaged.unwrap_or(false); - - if unpaged { - return Ok(Json(libraries.into())); - } - - Ok(Json((libraries, pagination.page_params()).into())) -} - -/// Get stats for all libraries -async fn get_libraries_stats(Extension(ctx): State) -> ApiResult> { - let db = ctx.get_db(); - - // TODO: maybe add more, like missingBooks, idk - let stats = db - ._query_raw::(raw!( - "SELECT COUNT(*) as book_count, IFNULL(SUM(media.size),0) as total_bytes, IFNULL(series_count,0) as series_count FROM media INNER JOIN (SELECT COUNT(*) as series_count FROM series)" - )) - .exec() - .await? - .into_iter() - .next(); - - if stats.is_none() { - return Err(ApiError::InternalServerError( - "Failed to compute stats for libraries".to_string(), - )); - } - - Ok(Json(stats.unwrap())) -} - -/// Get a library by id, if the current user has access to it. Library `series`, `media` -/// and `tags` relations are loaded on this route. -async fn get_library_by_id( - Path(id): Path, - Extension(ctx): State, -) -> ApiResult> { - let db = ctx.get_db(); - - // FIXME: this query is a pain to add series->media relation counts. - // This should be much better in https://github.com/Brendonovich/prisma-client-rust/issues/24 - // but for now I kinda have to load all the media... - let library = db - .library() - .find_unique(library::id::equals(id.clone())) - .with(library::series::fetch(vec![])) - .with(library::library_options::fetch()) - .with(library::tags::fetch(vec![])) - .exec() - .await?; - - if library.is_none() { - return Err(ApiError::NotFound(format!( - "Library with id {} not found", - id - ))); - } - - let library = library.unwrap(); - - Ok(Json(library.into())) -} - -// FIXME: this is absolutely atrocious... -// This should be much better once https://github.com/Brendonovich/prisma-client-rust/issues/24 is added -// but for now I will have this disgustingly gross and ugly work around... -///Returns the series in a given library. Will *not* load the media relation. -async fn get_library_series( - Path(id): Path, - pagination: Query, - Extension(ctx): State, -) -> ApiResult>>> { - let db = ctx.get_db(); - - let unpaged = pagination.unpaged.unwrap_or(false); - let page_params = pagination.page_params(); - let order_by_param: SeriesOrderByParam = - QueryOrder::from(page_params.clone()).try_into()?; - - let base_query = db - .series() - // TODO: add media relation count.... - .find_many(vec![series::library_id::equals(Some(id.clone()))]) - .order_by(order_by_param); - - let series = match unpaged { - true => base_query.exec().await?, - false => base_query.paginated(page_params.clone()).exec().await?, - }; - - let series_ids = series.iter().map(|s| s.id.clone()).collect(); - - let media_counts = db.series_media_count(series_ids).await?; - - let series = series - .iter() - .map(|s| { - let media_count = match media_counts.get(&s.id) { - Some(count) => count.to_owned(), - _ => 0, - } as i64; - - (s.to_owned(), media_count).into() - }) - .collect::>(); - - if unpaged { - return Ok(Json(series.into())); - } - - let series_count = db.series_count(id).await?; - - Ok(Json((series, series_count, page_params).into())) -} - -// /// Get the thumbnail image for a library by id, if the current user has access to it. -async fn get_library_thumbnail( - Path(id): Path, - Extension(ctx): State, -) -> ApiResult { - let db = ctx.get_db(); - - let library_series = db - .series() - .find_many(vec![series::library_id::equals(Some(id.clone()))]) - .with(series::media::fetch(vec![]).order_by(media::name::order(Direction::Asc))) - .exec() - .await?; - - // TODO: error handling - - let series = library_series.first().unwrap(); - - let media = series.media()?.first().unwrap(); - - Ok(media_file::get_page(media.path.as_str(), 1)?.into()) -} - -#[derive(Deserialize)] -struct ScanQueryParam { - scan_mode: Option, -} - -/// Queue a ScannerJob to scan the library by id. The job, when started, is -/// executed in a separate thread. -async fn scan_library( - Path(id): Path, - Extension(ctx): State, - query: Query, - session: ReadableSession, // TODO: admin middleware -) -> Result<(), ApiError> { - let db = ctx.get_db(); - let _user = get_session_admin_user(&session)?; - - let lib = db - .library() - .find_unique(library::id::equals(id.clone())) - .exec() - .await?; - - if lib.is_none() { - return Err(ApiError::NotFound(format!( - "Library with id {} not found", - id - ))); - } - - let lib = lib.unwrap(); - - let scan_mode = query.scan_mode.to_owned().unwrap_or_default(); - let scan_mode = LibraryScanMode::from_str(&scan_mode) - .map_err(|e| ApiError::BadRequest(format!("Invalid scan mode: {}", e)))?; - - // TODO: should this just be an error? - if scan_mode != LibraryScanMode::None { - let job = LibraryScanJob { - path: lib.path, - scan_mode, - }; - - return Ok(ctx.spawn_job(Box::new(job))?); - } - - Ok(()) -} - -// /// Create a new library. Will queue a ScannerJob to scan the library, and return the library -async fn create_library( - Json(input): Json, - Extension(ctx): State, -) -> ApiResult> { - let db = ctx.get_db(); - - // TODO: check library is not a parent of another library - if !path::Path::new(&input.path).exists() { - return Err(ApiError::BadRequest(format!( - "The library directory does not exist: {}", - input.path - ))); - } - - // TODO: refactor once nested create is supported - // https://github.com/Brendonovich/prisma-client-rust/issues/44 - - let library_options_arg = input.library_options.to_owned().unwrap_or_default(); - - // FIXME: until nested create, library_options.library_id will be NULL in the database... unless I run ANOTHER - // update. Which I am not doing lol. - let library_options = db - .library_options() - .create(vec![ - library_options::convert_rar_to_zip::set( - library_options_arg.convert_rar_to_zip, - ), - library_options::hard_delete_conversions::set( - library_options_arg.hard_delete_conversions, - ), - library_options::create_webp_thumbnails::set( - library_options_arg.create_webp_thumbnails, - ), - library_options::library_pattern::set( - library_options_arg.library_pattern.to_string(), - ), - ]) - .exec() - .await?; - - let lib = db - .library() - .create( - input.name.to_owned(), - input.path.to_owned(), - library_options::id::equals(library_options.id), - vec![library::description::set(input.description.to_owned())], - ) - .exec() - .await?; - - // FIXME: try and do multiple connects again soon, batching is WAY better than - // previous solution but still... - if let Some(tags) = input.tags.to_owned() { - let tag_connects = tags.into_iter().map(|tag| { - db.library().update( - library::id::equals(lib.id.clone()), - vec![library::tags::connect(vec![tag::id::equals(tag.id)])], - ) - }); - - db._batch(tag_connects).await?; - } - - let scan_mode = input.scan_mode.unwrap_or_default(); - - // `scan` is not a required field, however it will default to BATCHED if not provided - if scan_mode != LibraryScanMode::None { - ctx.spawn_job(Box::new(LibraryScanJob { - path: lib.path.clone(), - scan_mode, - }))?; - } - - Ok(Json(lib.into())) -} - -/// Update a library by id, if the current user is a SERVER_OWNER. -async fn update_library( - Extension(ctx): State, - Path(id): Path, - Json(input): Json, -) -> ApiResult> { - let db = ctx.get_db(); - - if !path::Path::new(&input.path).exists() { - return Err(ApiError::BadRequest(format!( - "Updated path does not exist: {}", - input.path - ))); - } - - let library_options = input.library_options.to_owned(); - - db.library_options() - .update( - library_options::id::equals(library_options.id.unwrap_or_default()), - vec![ - library_options::convert_rar_to_zip::set( - library_options.convert_rar_to_zip, - ), - library_options::hard_delete_conversions::set( - library_options.hard_delete_conversions, - ), - library_options::create_webp_thumbnails::set( - library_options.create_webp_thumbnails, - ), - ], - ) - .exec() - .await?; - - let mut batches = vec![]; - - // FIXME: this is disgusting. I don't understand why the library::tag::connect doesn't - // work with multiple tags, nor why providing multiple library::tag::connect params - // doesn't work. Regardless, absolutely do NOT keep this. Correction required, - // highly inefficient queries. - - if let Some(tags) = input.tags.to_owned() { - for tag in tags { - batches.push(db.library().update( - library::id::equals(id.clone()), - vec![library::tags::connect(vec![tag::id::equals( - tag.id.to_owned(), - )])], - )); - } - } - - if let Some(removed_tags) = input.removed_tags.to_owned() { - for tag in removed_tags { - batches.push(db.library().update( - library::id::equals(id.clone()), - vec![library::tags::disconnect(vec![tag::id::equals( - tag.id.to_owned(), - )])], - )); - } - } - - if !batches.is_empty() { - db._batch(batches).await?; - } - - let updated = db - .library() - .update( - library::id::equals(id), - vec![ - library::name::set(input.name.to_owned()), - library::path::set(input.path.to_owned()), - library::description::set(input.description.to_owned()), - ], - ) - .with(library::tags::fetch(vec![])) - .exec() - .await?; - - let scan_mode = input.scan_mode.unwrap_or_default(); - - // `scan` is not a required field, however it will default to BATCHED if not provided - if scan_mode != LibraryScanMode::None { - ctx.spawn_job(Box::new(LibraryScanJob { - path: updated.path.clone(), - scan_mode, - }))?; - } - - Ok(Json(updated.into())) -} - -/// Delete a library by id, if the current user is a SERVER_OWNER. -async fn delete_library( - Path(id): Path, - Extension(ctx): State, -) -> ApiResult> { - let db = ctx.get_db(); - - trace!("Attempting to delete library with ID {}", &id); - - let deleted = db - .library() - .delete(library::id::equals(id.clone())) - .include(library::include!({ - series: include { - media: select { - id - } - } - })) - .exec() - .await?; - - let media_ids = deleted - .series - .into_iter() - .flat_map(|series| series.media) - .map(|media| media.id) - .collect::>(); - - if !media_ids.is_empty() { - trace!("List of deleted media IDs: {:?}", media_ids); - - debug!( - "Attempting to delete {} media thumbnails (if present)", - media_ids.len() - ); - - if let Err(err) = image::remove_thumbnails(&media_ids) { - error!("Failed to remove thumbnails for library media: {:?}", err); - } else { - debug!("Removed thumbnails for library media (if present)"); - } - } - - Ok(Json(deleted.id)) -} diff --git a/apps/server/src/routers/api/media.rs b/apps/server/src/routers/api/media.rs deleted file mode 100644 index 8a9bf7857..000000000 --- a/apps/server/src/routers/api/media.rs +++ /dev/null @@ -1,361 +0,0 @@ -use axum::{ - extract::{Path, Query}, - middleware::from_extractor, - routing::{get, put}, - Extension, Json, Router, -}; -use axum_sessions::extractors::ReadableSession; -use prisma_client_rust::{raw, Direction}; -use stump_core::{ - config::get_config_dir, - db::utils::PrismaCountTrait, - fs::{image, media_file}, - prisma::{ - media::{self, OrderByParam as MediaOrderByParam}, - read_progress, user, - }, - types::{ - ContentType, FindManyTrait, Media, Pageable, PagedRequestParams, QueryOrder, - ReadProgress, - }, -}; -use tracing::trace; - -use crate::{ - config::state::State, - errors::{ApiError, ApiResult}, - middleware::auth::Auth, - utils::{ - get_session_user, - http::{ImageResponse, NamedFile, PageableTrait}, - }, -}; - -pub(crate) fn mount() -> Router { - Router::new() - .route("/media", get(get_media)) - .route("/media/duplicates", get(get_duplicate_media)) - .route("/media/keep-reading", get(get_reading_media)) - .nest( - "/media/:id", - Router::new() - .route("/", get(get_media_by_id)) - .route("/file", get(get_media_file)) - .route("/convert", get(convert_media)) - .route("/thumbnail", get(get_media_thumbnail)) - .route("/page/:page", get(get_media_page)) - .route("/progress/:page", put(update_media_progress)), - ) - .layer(from_extractor::()) -} - -/// Get all media accessible to the requester. This is a paginated request, and -/// has various pagination params available. -async fn get_media( - pagination: Query, - Extension(ctx): State, - session: ReadableSession, -) -> ApiResult>>> { - let db = ctx.get_db(); - let user_id = get_session_user(&session)?.id; - - let unpaged = pagination.unpaged.unwrap_or(false); - let page_params = pagination.page_params(); - let order_by_param: MediaOrderByParam = - QueryOrder::from(page_params.clone()).try_into()?; - - let base_query = db - .media() - .find_many(vec![]) - .with(media::read_progresses::fetch(vec![ - read_progress::user_id::equals(user_id), - ])) - .order_by(order_by_param); - - if unpaged { - return Ok(Json( - base_query - .exec() - .await? - .into_iter() - .map(|m| m.into()) - .collect::>() - .into(), - )); - } - - let count = db.media_count().await?; - - let media = base_query - .paginated(page_params.clone()) - .exec() - .await? - .into_iter() - .map(|m| m.into()) - .collect::>(); - - Ok(Json((media, count, page_params).into())) -} - -/// Get all media with identical checksums. This heavily implies duplicate files. -/// This is a paginated request, and has various pagination params available. -async fn get_duplicate_media( - pagination: Query, - Extension(ctx): State, - _session: ReadableSession, -) -> ApiResult>>> { - let db = ctx.get_db(); - - let media: Vec = db - ._query_raw(raw!("SELECT * FROM media WHERE checksum IN (SELECT checksum FROM media GROUP BY checksum HAVING COUNT(*) > 1)")) - .exec() - .await?; - - let unpaged = pagination.unpaged.unwrap_or(false); - - if unpaged { - return Ok(Json(media.into())); - } - - Ok(Json((media, pagination.page_params()).into())) -} - -// TODO: I will need to add epub progress in here SOMEHOW... this will be rather -// difficult... -// TODO: paginate? -/// Get all media which the requester has progress for that is less than the -/// total number of pages available (i.e not completed). -async fn get_reading_media( - Extension(ctx): State, - session: ReadableSession, -) -> ApiResult>> { - let db = ctx.get_db(); - let user_id = get_session_user(&session)?.id; - - Ok(Json( - db.media() - .find_many(vec![media::read_progresses::some(vec![ - read_progress::user_id::equals(user_id.clone()), - read_progress::page::gt(0), - ])]) - .with(media::read_progresses::fetch(vec![ - read_progress::user_id::equals(user_id), - read_progress::page::gt(0), - ])) - .order_by(media::updated_at::order(Direction::Desc)) - .exec() - .await? - .into_iter() - .filter(|m| match m.read_progresses() { - // Read progresses relation on media is one to many, there is a dual key - // on read_progresses table linking a user and media. Therefore, there should - // only be 1 item in this vec for each media resulting from the query. - Ok(progresses) => { - if progresses.len() != 1 { - return false; - } - - let progress = &progresses[0]; - - if let Some(_epubcfi) = progress.epubcfi.as_ref() { - // TODO: figure something out... might just need a `completed` field in progress TBH. - false - } else { - progress.page < m.pages - } - }, - _ => false, - }) - .map(|m| m.into()) - .collect(), - )) -} - -async fn get_media_by_id( - Path(id): Path, - Extension(ctx): State, - session: ReadableSession, -) -> ApiResult> { - let db = ctx.get_db(); - let user_id = get_session_user(&session)?.id; - - let book = db - .media() - .find_unique(media::id::equals(id.clone())) - .with(media::read_progresses::fetch(vec![ - read_progress::user_id::equals(user_id), - ])) - .exec() - .await?; - - if book.is_none() { - return Err(ApiError::NotFound(format!( - "Media with id {} not found", - id - ))); - } - - Ok(Json(book.unwrap().into())) -} - -async fn get_media_file( - Path(id): Path, - Extension(ctx): State, -) -> ApiResult { - let db = ctx.get_db(); - - let media = db - .media() - .find_unique(media::id::equals(id.clone())) - .exec() - .await?; - - if media.is_none() { - return Err(ApiError::NotFound(format!( - "Media with id {} not found", - id - ))); - } - - let media = media.unwrap(); - - Ok(NamedFile::open(media.path.clone()).await?) -} - -// TODO: remove this, implement it? maybe? -async fn convert_media( - Path(id): Path, - Extension(ctx): State, -) -> Result<(), ApiError> { - let db = ctx.get_db(); - - let media = db - .media() - .find_unique(media::id::equals(id.clone())) - .exec() - .await?; - - if media.is_none() { - return Err(ApiError::NotFound(format!( - "Media with id {} not found", - id - ))); - } - - let media = media.unwrap(); - - if media.extension != "cbr" || media.extension != "rar" { - return Err(ApiError::BadRequest(format!( - "Media with id {} is not a rar file. Stump only supports converting rar/cbr files to zip/cbz.", - id - ))); - } - - // TODO: write me... - unimplemented!() -} - -async fn get_media_page( - Path((id, page)): Path<(String, i32)>, - Extension(ctx): State, - session: ReadableSession, -) -> ApiResult { - let db = ctx.get_db(); - let user_id = get_session_user(&session)?.id; - - let book = db - .media() - .find_unique(media::id::equals(id.clone())) - .with(media::read_progresses::fetch(vec![ - read_progress::user_id::equals(user_id), - ])) - .exec() - .await?; - - match book { - Some(book) => { - if page > book.pages { - // FIXME: probably won't work lol - Err(ApiError::Redirect(format!( - "/book/{}/read?page={}", - id, book.pages - ))) - } else { - Ok(media_file::get_page(&book.path, page)?.into()) - } - }, - None => Err(ApiError::NotFound(format!( - "Media with id {} not found", - id - ))), - } -} - -async fn get_media_thumbnail( - Path(id): Path, - Extension(ctx): State, - session: ReadableSession, -) -> ApiResult { - let db = ctx.get_db(); - let user_id = get_session_user(&session)?.id; - - let webp_path = get_config_dir() - .join("thumbnails") - .join(format!("{}.webp", id)); - - if webp_path.exists() { - trace!("Found webp thumbnail for media {}", id); - return Ok((ContentType::WEBP, image::get_image_bytes(webp_path)?).into()); - } - - let book = db - .media() - .find_unique(media::id::equals(id.clone())) - .with(media::read_progresses::fetch(vec![ - read_progress::user_id::equals(user_id), - ])) - .exec() - .await?; - - if book.is_none() { - return Err(ApiError::NotFound(format!( - "Media with id {} not found", - id - ))); - } - - let book = book.unwrap(); - - Ok(media_file::get_page(book.path.as_str(), 1)?.into()) -} - -// FIXME: this doesn't really handle certain errors correctly, e.g. media/user not found -async fn update_media_progress( - Path((id, page)): Path<(String, i32)>, - Extension(ctx): State, - session: ReadableSession, -) -> ApiResult> { - let db = ctx.get_db(); - let user_id = get_session_user(&session)?.id; - - // update the progress, otherwise create it - Ok(Json( - db.read_progress() - .upsert( - read_progress::UniqueWhereParam::UserIdMediaIdEquals( - user_id.clone(), - id.clone(), - ), - ( - page, - media::id::equals(id.clone()), - user::id::equals(user_id.clone()), - vec![], - ), - vec![read_progress::page::set(page)], - ) - .exec() - .await? - .into(), - )) -} diff --git a/apps/server/src/routers/api/mod.rs b/apps/server/src/routers/api/mod.rs index 5d8220274..64ec1115f 100644 --- a/apps/server/src/routers/api/mod.rs +++ b/apps/server/src/routers/api/mod.rs @@ -1,60 +1,9 @@ -use axum::{ - routing::{get, post}, - Extension, Json, Router, -}; -use stump_core::types::{ClaimResponse, StumpVersion}; +use axum::Router; -use crate::{config::state::State, errors::ApiResult}; +use crate::config::state::AppState; -mod auth; -mod epub; -mod filesystem; -mod job; -mod library; -mod log; -mod media; -mod series; -mod tag; -mod user; -mod reading_list; +pub(crate) mod v1; -pub(crate) fn mount() -> Router { - Router::new().nest( - "/api", - Router::new() - .merge(auth::mount()) - .merge(epub::mount()) - .merge(library::mount()) - .merge(media::mount()) - .merge(filesystem::mount()) - .merge(job::mount()) - .merge(log::mount()) - .merge(series::mount()) - .merge(tag::mount()) - .merge(user::mount()) - .merge(reading_list::mount()) - .route("/claim", get(claim)) - .route("/ping", get(ping)) - .route("/version", post(version)), - ) -} - -async fn claim(Extension(ctx): State) -> ApiResult> { - let db = ctx.get_db(); - - Ok(Json(ClaimResponse { - is_claimed: db.user().find_first(vec![]).exec().await?.is_some(), - })) -} - -async fn ping() -> ApiResult { - Ok("pong".to_string()) -} - -async fn version() -> ApiResult> { - Ok(Json(StumpVersion { - semver: env!("CARGO_PKG_VERSION").to_string(), - rev: std::env::var("GIT_REV").ok(), - compile_time: env!("STATIC_BUILD_DATE").to_string(), - })) +pub(crate) fn mount(app_state: AppState) -> Router { + Router::new().nest("/api", Router::new().nest("/v1", v1::mount(app_state))) } diff --git a/apps/server/src/routers/api/reading_list.rs b/apps/server/src/routers/api/reading_list.rs deleted file mode 100644 index e0a34f8bf..000000000 --- a/apps/server/src/routers/api/reading_list.rs +++ /dev/null @@ -1,126 +0,0 @@ -use axum::{ - routing::{get, post, put, delete}, - extract::Path, - Extension, Json, Router, -}; -use axum_sessions::extractors::{ReadableSession, WritableSession}; -use stump_core::{ - prisma::{reading_list, media, user}, - types::{User, readinglist::ReadingList, Media, readinglist::CreateReadingList}, -}; -use tracing::log::trace; -use crate::{ - config::state::State, - errors::{ApiError, ApiResult}, - utils::{get_session_user}, -}; - -pub(crate) fn mount() -> Router { - Router::new() - .route("/reading-list", get(get_reading_list).post(create_reading_list)) - .nest( - "/reading-list/:id", - Router::new() - .route("/", get(get_reading_list_by_id).put(update_reading_list).delete(delete_reading_list_by_id)), - ) -} - -async fn get_reading_list( - Extension(ctx): State, - session: ReadableSession, -) -> ApiResult>> { - let user_id = get_session_user(&session)?.id; - - Ok(Json( - ctx.db - .reading_list() - .find_many(vec![ - reading_list::creating_user_id::equals(user_id), - ]) - .exec() - .await? - .into_iter() - .map(|u| u.into()) - .collect::>(), - )) -} - -async fn create_reading_list( - Extension(ctx): State, - Json(input): Json, - session: ReadableSession, -) -> ApiResult> { - let db = ctx.get_db(); - let user_id = get_session_user(&session)?.id; - - let created_reading_list = db - .reading_list() - .create( - input.id.to_owned(), - user::id::equals(user_id.clone()), - vec![reading_list::media::connect(input.media_ids.iter().map(|id| media::id::equals(id.to_string())).collect())] - ) - .exec() - .await?; - - Ok(Json(created_reading_list.into())) -} - -async fn get_reading_list_by_id( - Path(id): Path, - Extension(ctx): State, - session: ReadableSession, -) -> ApiResult> { - let user_id = get_session_user(&session)?.id; - let db = ctx.get_db(); - - let reading_list_id = db - .reading_list() - .find_unique(reading_list::id::equals(id.clone())) - .exec() - .await?; - - if reading_list_id.is_none() { - return Err(ApiError::NotFound(format!( - "Reading List with id {} not found", - id - ))); - } - - Ok(Json(reading_list_id.unwrap().into())) -} - -async fn update_reading_list( - Path(id): Path, - Extension(ctx): State, - Json(input): Json, -) -> ApiResult> { - let db = ctx.get_db(); - - let created_reading_list: _ = db - .reading_list() - .update(reading_list::id::equals(id.clone()), vec![ - reading_list::media::connect(input.media_ids.iter().map(|id| media::id::equals(id.to_string())).collect()) - ]) - .exec() - .await?; - - Ok(Json(created_reading_list.into())) -} - -async fn delete_reading_list_by_id( - Path(id): Path, - Extension(ctx): State, -) -> ApiResult> { - let db = ctx.get_db(); - - trace!("Attempting to delete reading list with ID {}", &id); - - let deleted = db - .reading_list() - .delete(reading_list::id::equals(id.clone())) - .exec() - .await?; - - Ok(Json(deleted.id)) -} \ No newline at end of file diff --git a/apps/server/src/routers/api/series.rs b/apps/server/src/routers/api/series.rs deleted file mode 100644 index 3549d3b87..000000000 --- a/apps/server/src/routers/api/series.rs +++ /dev/null @@ -1,301 +0,0 @@ -use axum::{ - extract::{Path, Query}, - middleware::from_extractor, - routing::get, - Extension, Json, Router, -}; -use axum_sessions::extractors::ReadableSession; -use prisma_client_rust::Direction; -use serde::Deserialize; -use stump_core::{ - db::utils::PrismaCountTrait, - fs::{image, media_file}, - prisma::{ - media::{self, OrderByParam as MediaOrderByParam}, - read_progress, series, - }, - types::{ - ContentType, FindManyTrait, Media, Pageable, PagedRequestParams, QueryOrder, - Series, - }, -}; -use tracing::trace; - -use crate::{ - config::state::State, - errors::{ApiError, ApiResult}, - middleware::auth::Auth, - utils::{ - get_session_user, - http::{ImageResponse, PageableTrait}, - }, -}; - -pub(crate) fn mount() -> Router { - Router::new() - .route("/series", get(get_series)) - .nest( - "/series/:id", - Router::new() - .route("/", get(get_series_by_id)) - .route("/media", get(get_series_media)) - .route("/media/next", get(get_next_in_series)) - .route("/thumbnail", get(get_series_thumbnail)), - ) - .layer(from_extractor::()) -} - -#[derive(Deserialize)] -struct LoadMedia { - load_media: Option, -} - -/// Get all series accessible by user. This is a paginated respone, and -/// accepts various paginated request params. -async fn get_series( - load: Query, - pagination: Query, - Extension(ctx): State, - session: ReadableSession, -) -> ApiResult>>> { - let db = ctx.get_db(); - let user_id = get_session_user(&session)?.id; - - let load_media = load.load_media.unwrap_or(false); - - let action = db.series(); - let action = action.find_many(vec![]); - - let query = match load_media { - true => action.with( - series::media::fetch(vec![]) - .with(media::read_progresses::fetch(vec![ - read_progress::user_id::equals(user_id), - ])) - .order_by(media::name::order(Direction::Asc)), - ), - false => action, - }; - - let series = query - .exec() - .await? - .into_iter() - .map(|s| s.into()) - .collect::>(); - - let unpaged = pagination.unpaged.unwrap_or(false); - if unpaged { - return Ok(Json(series.into())); - } - - Ok(Json((series, pagination.page_params()).into())) -} - -/// Get a series by ID. Optional query param `load_media` that will load the media -/// relation (i.e. the media entities will be loaded and sent with the response) -// #[get("/series/?")] -async fn get_series_by_id( - Path(id): Path, - Extension(ctx): State, - load_media: Query, - session: ReadableSession, -) -> ApiResult> { - let db = ctx.get_db(); - let user_id = get_session_user(&session)?.id; - - let load_media = load_media.load_media.unwrap_or(false); - let mut query = db.series().find_unique(series::id::equals(id.clone())); - - if load_media { - query = query.with( - series::media::fetch(vec![]) - .with(media::read_progresses::fetch(vec![ - read_progress::user_id::equals(user_id), - ])) - .order_by(media::name::order(Direction::Asc)), - ); - } - - let series = query.exec().await?; - - if series.is_none() { - return Err(ApiError::NotFound(format!( - "Series with id {} not found", - id - ))); - } - - if !load_media { - // FIXME: PCR doesn't support relation counts yet! - // let media_count = db - // .media() - // .count(vec![media::series_id::equals(Some(id.clone()))]) - // .exec() - // .await?; - let series_media_count = db.media_in_series_count(id).await?; - - return Ok(Json((series.unwrap(), series_media_count).into())); - } - - Ok(Json(series.unwrap().into())) -} - -/// Returns the thumbnail image for a series -// #[get("/series//thumbnail")] -async fn get_series_thumbnail( - Path(id): Path, - Extension(ctx): State, -) -> ApiResult { - let db = ctx.get_db(); - - let media = db - .media() - .find_first(vec![media::series_id::equals(Some(id.clone()))]) - .order_by(media::name::order(Direction::Asc)) - .exec() - .await?; - - if media.is_none() { - return Err(ApiError::NotFound(format!( - "Series with id {} not found", - id - ))); - } - - let media = media.unwrap(); - - if let Some(webp_path) = image::get_thumbnail_path(&media.id) { - trace!("Found webp thumbnail for series {}", &id); - return Ok((ContentType::WEBP, image::get_image_bytes(webp_path)?).into()); - } - - Ok(media_file::get_page(media.path.as_str(), 1)?.into()) -} - -/// Returns the media in a given series. This is a paginated respone, and -/// accepts various paginated request params. -// #[get("/series//media?&")] -async fn get_series_media( - Path(id): Path, - Extension(ctx): State, - pagination: Query, - session: ReadableSession, -) -> ApiResult>>> { - let db = ctx.get_db(); - let user_id = get_session_user(&session)?.id; - - let unpaged = pagination.unpaged.unwrap_or(false); - let page_params = pagination.page_params(); - let order_by_param: MediaOrderByParam = - QueryOrder::from(page_params.clone()).try_into()?; - - let base_query = db - .media() - .find_many(vec![media::series_id::equals(Some(id.clone()))]) - .with(media::read_progresses::fetch(vec![ - read_progress::user_id::equals(user_id), - ])) - .order_by(order_by_param); - - let media = if unpaged { - base_query.exec().await? - } else { - base_query.paginated(page_params.clone()).exec().await? - }; - - let media = media.into_iter().map(|m| m.into()).collect::>(); - - if unpaged { - return Ok(Json(media.into())); - } - - // TODO: investigate this, I am getting incorrect counts here... - // FIXME: AHAHAHAHAHAHA, PCR doesn't support relation counts! I legit thought I was - // going OUTSIDE my fuckin mind - // FIXME: PCR doesn't support relation counts yet! - // let series_media_count = db - // .media() - // .count(vec![media::series_id::equals(Some(id))]) - // .exec() - // .await?; - let series_media_count = db.media_in_series_count(id).await?; - - Ok(Json((media, series_media_count, page_params).into())) -} - -// TODO: Should I support ehere too?? Not sure, I have separate routes for epub, -// but until I actually implement progress tracking for eI think think I can really -// give a hard answer on what is best... -/// Get the next media in a series, based on the read progress for the requesting user. -/// Stump will return the first book in the series without progress, or return the first -/// with partial progress. E.g. if a user has read pages 32/32 of book 3, then book 4 is -/// next. If a user has read pages 31/32 of book 4, then book 4 is still next. -// #[get("/series//media/next")] -async fn get_next_in_series( - Path(id): Path, - Extension(ctx): State, - session: ReadableSession, -) -> ApiResult>> { - let db = ctx.get_db(); - let user_id = get_session_user(&session)?.id; - - let series = db - .series() - .find_unique(series::id::equals(id.clone())) - .with( - series::media::fetch(vec![]) - .with(media::read_progresses::fetch(vec![ - read_progress::user_id::equals(user_id), - ])) - .order_by(media::name::order(Direction::Asc)), - ) - .exec() - .await?; - - if series.is_none() { - return Err(ApiError::NotFound(format!( - "Series with id {} no found.", - id - ))); - } - - let series = series.unwrap(); - - let media = series.media(); - - if media.is_err() { - return Ok(Json(None)); - } - - let media = media.unwrap(); - - Ok(Json( - media - .iter() - .find(|m| { - // I don't really know that this is valid... When I load in the - // relation, this will NEVER be None. It will default to an empty - // vector. But, for safety I guess I will leave this for now. - if m.read_progresses.is_none() { - return true; - } - - let progresses = m.read_progresses.as_ref().unwrap(); - - // No progress means it is up next (for this user)! - if progresses.is_empty() { - true - } else { - // Note: this should never really exceed len == 1, but :shrug: - let progress = progresses.get(0).unwrap(); - - progress.page < m.pages && progress.page > 0 - } - }) - .or_else(|| media.get(0)) - .map(|data| data.to_owned().into()), - )) -} - -// async fn download_series() diff --git a/apps/server/src/routers/api/tag.rs b/apps/server/src/routers/api/tag.rs deleted file mode 100644 index ffb6456bc..000000000 --- a/apps/server/src/routers/api/tag.rs +++ /dev/null @@ -1,63 +0,0 @@ -use axum::{middleware::from_extractor, routing::get, Extension, Json, Router}; -use serde::Deserialize; -use stump_core::types::Tag; - -use crate::{config::state::State, errors::ApiResult, middleware::auth::Auth}; - -pub(crate) fn mount() -> Router { - Router::new() - .route("/tags", get(get_tags).post(create_tags)) - .layer(from_extractor::()) -} - -/// Get all tags for all items in the database. Tags are returned in a flat list, -/// not grouped by the items which they belong to. -async fn get_tags(Extension(ctx): State) -> ApiResult>> { - let db = ctx.get_db(); - - Ok(Json( - db.tag() - .find_many(vec![]) - .exec() - .await? - .into_iter() - .map(|t| t.into()) - .collect(), - )) -} - -#[derive(Deserialize)] -pub struct CreateTags { - pub tags: Vec, -} - -async fn create_tags( - Json(input): Json, - Extension(ctx): State, -) -> ApiResult>> { - let db = ctx.get_db(); - - let tags = input.tags.to_owned(); - - let mut created_tags = vec![]; - - // FIXME: bulk insert not yet supported. Also transactions, as an alternative, - // not yet supported. - for tag in tags { - match db.tag().create(tag, vec![]).exec().await { - Ok(new_tag) => { - created_tags.push(new_tag.into()); - }, - Err(e) => { - // TODO: check if duplicate tag error, in which case I don't care and - // will ignore the error, otherwise throw the error. - // Alternative, I could upsert? This way an error is always an error, - // and if there's a duplicate tag it will be "updated", but really nothing - // will happen sine the name is the same? - println!("{}", e); - }, - } - } - - Ok(Json(created_tags)) -} diff --git a/apps/server/src/routers/api/user.rs b/apps/server/src/routers/api/user.rs deleted file mode 100644 index 6da6fc7ef..000000000 --- a/apps/server/src/routers/api/user.rs +++ /dev/null @@ -1,185 +0,0 @@ -use axum::{ - extract::Path, middleware::from_extractor, routing::get, Extension, Json, Router, -}; -use axum_sessions::extractors::ReadableSession; -use stump_core::{ - prisma::{user, user_preferences}, - types::{LoginOrRegisterArgs, User, UserPreferences, UserPreferencesUpdate}, -}; - -use crate::{ - config::state::State, - errors::{ApiError, ApiResult}, - middleware::auth::{AdminGuard, Auth}, - utils::{get_hash_cost, get_session_user}, -}; - -pub(crate) fn mount() -> Router { - Router::new() - .route("/users", get(get_users).post(create_user)) - .layer(from_extractor::()) - .nest( - "/users/:id", - Router::new() - .route("/", get(get_user_by_id).put(update_user)) - .route( - "/preferences", - get(get_user_preferences).put(update_user_preferences), - ), - ) - .layer(from_extractor::()) -} - -async fn get_users( - Extension(ctx): State, - session: ReadableSession, -) -> ApiResult>> { - let user = get_session_user(&session)?; - - // FIXME: admin middleware - if !user.is_admin() { - return Err(ApiError::Forbidden( - "You do not have permission to access this resource.".into(), - )); - } - - Ok(Json( - ctx.db - .user() - .find_many(vec![]) - .exec() - .await? - .into_iter() - .map(|u| u.into()) - .collect::>(), - )) -} - -async fn create_user( - Extension(ctx): State, - Json(input): Json, - session: ReadableSession, -) -> ApiResult> { - let db = ctx.get_db(); - let user = get_session_user(&session)?; - - // FIXME: admin middleware - if !user.is_admin() { - return Err(ApiError::Forbidden( - "You do not have permission to access this resource.".into(), - )); - } - let hashed_password = bcrypt::hash(&input.password, get_hash_cost())?; - - let created_user = db - .user() - .create(input.username.to_owned(), hashed_password, vec![]) - .exec() - .await?; - - // FIXME: these next two queries will be removed once nested create statements are - // supported on the prisma client. Until then, this ugly mess is necessary. - // https://github.com/Brendonovich/prisma-client-rust/issues/44 - let _user_preferences = db - .user_preferences() - .create(vec![user_preferences::user::connect(user::id::equals( - created_user.id.clone(), - ))]) - .exec() - .await?; - - // This *really* shouldn't fail, so I am using unwrap here. It also doesn't - // matter too much in the long run since this query will go away once above fixme - // is resolved. - let user = db - .user() - .find_unique(user::id::equals(created_user.id)) - .with(user::user_preferences::fetch()) - .exec() - .await? - .unwrap(); - - Ok(Json(user.into())) -} - -async fn get_user_by_id() -> ApiResult<()> { - Err(ApiError::NotImplemented) -} - -// TODO: figure out what operations are allowed here, and by whom. E.g. can a server -// owner update user details of another managed account after they've been created? -// or update another user's preferences? I don't like that last one, unsure about -// the first. In general, after creation, I think a user has sole control over their account. -// The server owner should be able to remove them, but I don't think they should be able -// to do anything else? -async fn update_user() -> ApiResult<()> { - Err(ApiError::NotImplemented) -} - -// FIXME: remove this once I resolve the below 'TODO' -async fn get_user_preferences( - Path(id): Path, - Extension(ctx): State, - // session: ReadableSession, -) -> ApiResult> { - let db = ctx.get_db(); - - Ok(Json( - db.user_preferences() - .find_unique(user_preferences::id::equals(id.clone())) - .exec() - .await? - .expect("Failed to fetch user preferences") - .into(), // .map(|p| p.into()), - // user_preferences, - )) -} - -// TODO: I load the user preferences from the session in the auth call. -// If a session didn't exist then I load it from DB. I think for now this is OK since -// all the preferences are client-side, so if the server is not in sync with -// preferences updates it is not a big deal. This will have to change somehow in the -// future potentially though, unless I just load preferences when required. -// -// Note: I don't even use the user id to load the preferences, as I pull it from -// when I got from the session. I could remove the ID requirement. I think the preferences -// structure needs to eventually change a little anyways, I don't like how I can't query -// by user id, it should be a unique where param but it isn't with how I structured it... -// FIXME: remove this 'allow' once I resolve the above 'TODO' -#[allow(unused)] -async fn update_user_preferences( - Path(id): Path, - Json(input): Json, - Extension(ctx): State, - session: ReadableSession, -) -> ApiResult> { - let db = ctx.get_db(); - - let user = get_session_user(&session)?; - let user_preferences = user.user_preferences.unwrap_or_default(); - - if user_preferences.id != input.id { - return Err(ApiError::Forbidden( - "You cannot update another user's preferences".into(), - )); - } - - Ok(Json( - db.user_preferences() - .update( - user_preferences::id::equals(user_preferences.id.clone()), - vec![ - user_preferences::locale::set(input.locale.to_owned()), - user_preferences::library_layout_mode::set( - input.library_layout_mode.to_owned(), - ), - user_preferences::series_layout_mode::set( - input.series_layout_mode.to_owned(), - ), - ], - ) - .exec() - .await? - .into(), - )) -} diff --git a/apps/server/src/routers/api/v1/auth.rs b/apps/server/src/routers/api/v1/auth.rs new file mode 100644 index 000000000..b8fe1dfa6 --- /dev/null +++ b/apps/server/src/routers/api/v1/auth.rs @@ -0,0 +1,193 @@ +use axum::{ + extract::State, + routing::{get, post}, + Json, Router, +}; +use axum_sessions::extractors::{ReadableSession, WritableSession}; +use stump_core::{ + db::models::User, + prelude::{LoginOrRegisterArgs, UserRole}, + prisma::{user, user_preferences}, +}; + +use crate::{ + config::state::AppState, + errors::{ApiError, ApiResult}, + utils::{self, verify_password}, +}; + +pub(crate) fn mount() -> Router { + Router::new().nest( + "/auth", + Router::new() + .route("/me", get(viewer)) + .route("/login", post(login)) + .route("/logout", post(logout)) + .route("/register", post(register)), + ) +} + +#[utoipa::path( + get, + path = "/api/v1/auth/me", + tag = "auth", + responses( + (status = 200, description = "Returns the currently logged in user from the session.", body = User), + (status = 401, description = "No user is logged in (unauthorized).") + ) +)] +/// Returns the currently logged in user from the session. If no user is logged in, returns an +/// unauthorized error. +async fn viewer(session: ReadableSession) -> ApiResult> { + if let Some(user) = session.get::("user") { + Ok(Json(user)) + } else { + Err(ApiError::Unauthorized) + } +} + +#[utoipa::path( + post, + path = "/api/v1/auth/login", + tag = "auth", + request_body = LoginOrRegisterArgs, + responses( + (status = 200, description = "Authenticates the user and returns the user object.", body = User), + (status = 401, description = "Invalid username or password."), + (status = 500, description = "An internal server error occurred.") + ) +)] +/// Authenticates the user and returns the user object. If the user is already logged in, returns the +/// user object from the session. +async fn login( + mut session: WritableSession, + State(state): State, + Json(input): Json, +) -> ApiResult> { + if let Some(user) = session.get::("user") { + if input.username == user.username { + return Ok(Json(user)); + } + } + + let fetched_user = state + .db + .user() + .find_unique(user::username::equals(input.username.to_owned())) + .with(user::user_preferences::fetch()) + .exec() + .await?; + + if let Some(db_user) = fetched_user { + let matches = verify_password(&db_user.hashed_password, &input.password)?; + if !matches { + return Err(ApiError::Unauthorized); + } + + let user: User = db_user.into(); + session + .insert("user", user.clone()) + .expect("Failed to write user to session"); + + return Ok(Json(user)); + } + + Err(ApiError::Unauthorized) +} + +#[utoipa::path( + post, + path = "/api/v1/auth/logout", + tag = "auth", + responses( + (status = 200, description = "Destroys the session and logs the user out."), + (status = 500, description = "Failed to destroy session.") + ) +)] +/// Destroys the session and logs the user out. +async fn logout(mut session: WritableSession) -> ApiResult<()> { + session.destroy(); + if !session.is_destroyed() { + return Err(ApiError::InternalServerError( + "Failed to destroy session".to_string(), + )); + } + Ok(()) +} + +#[utoipa::path( + post, + path = "/api/v1/auth/register", + tag = "auth", + request_body = LoginOrRegisterArgs, + responses( + (status = 200, description = "Successfully registered new user.", body = User), + (status = 403, description = "Must be server owner to register member accounts."), + (status = 500, description = "An internal server error occurred.") + ) +)] +/// Attempts to register a new user. If no users exist in the database, the user is registered as a server owner. +/// Otherwise, the registration is rejected by all users except the server owner. +pub async fn register( + session: ReadableSession, + State(ctx): State, + Json(input): Json, +) -> ApiResult> { + let db = ctx.get_db(); + + let has_users = db.user().find_first(vec![]).exec().await?.is_some(); + + let mut user_role = UserRole::default(); + + let session_user = session.get::("user"); + + // TODO: move nested if to if let once stable + if let Some(user) = session_user { + if !user.is_admin() { + return Err(ApiError::Forbidden(String::from( + "You do not have permission to access this resource.", + ))); + } + } else if session_user.is_none() && has_users { + // if users exist, a valid session is required to register a new user + return Err(ApiError::Unauthorized); + } else if !has_users { + // if no users present, the user is automatically a server owner + user_role = UserRole::ServerOwner; + } + + let hashed_password = bcrypt::hash(&input.password, utils::get_hash_cost())?; + + let created_user = db + .user() + .create( + input.username.to_owned(), + hashed_password, + vec![user::role::set(user_role.into())], + ) + .exec() + .await?; + + // FIXME: these next two queries will be removed once nested create statements are + // supported on the prisma client. Until then, this ugly mess is necessary. + let _user_preferences = db + .user_preferences() + .create(vec![user_preferences::user::connect(user::id::equals( + created_user.id.clone(), + ))]) + .exec() + .await?; + + // This *really* shouldn't fail, so I am using expect here. It also doesn't + // matter too much in the long run since this query will go away once above fixme + // is resolved. + let user = db + .user() + .find_unique(user::id::equals(created_user.id)) + .with(user::user_preferences::fetch()) + .exec() + .await? + .expect("Failed to fetch user after registration."); + + Ok(Json(user.into())) +} diff --git a/apps/server/src/routers/api/epub.rs b/apps/server/src/routers/api/v1/epub.rs similarity index 89% rename from apps/server/src/routers/api/epub.rs rename to apps/server/src/routers/api/v1/epub.rs index bb8f9fe47..ae1d141da 100644 --- a/apps/server/src/routers/api/epub.rs +++ b/apps/server/src/routers/api/v1/epub.rs @@ -1,23 +1,26 @@ use std::path::PathBuf; use axum::{ - extract::Path, middleware::from_extractor, routing::get, Extension, Json, Router, + extract::{Path, State}, + middleware::from_extractor_with_state, + routing::get, + Json, Router, }; use axum_sessions::extractors::ReadableSession; use stump_core::{ + db::models::Epub, fs::epub, prisma::{media, read_progress}, - types::Epub, }; use crate::{ - config::state::State, + config::state::AppState, errors::{ApiError, ApiResult}, middleware::auth::Auth, utils::{get_session_user, http::BufferResponse}, }; -pub(crate) fn mount() -> Router { +pub(crate) fn mount(app_state: AppState) -> Router { Router::new() .nest( "/epub/:id", @@ -26,13 +29,13 @@ pub(crate) fn mount() -> Router { .route("/chapter/:chapter", get(get_epub_chapter)) .route("/:root/:resource", get(get_epub_meta)), ) - .layer(from_extractor::()) + .layer(from_extractor_with_state::(app_state)) } /// Get an Epub by ID. The `read_progress` relation is loaded. async fn get_epub( Path(id): Path, - Extension(ctx): State, + State(ctx): State, session: ReadableSession, ) -> ApiResult> { let user_id = get_session_user(&session)?.id; @@ -66,7 +69,7 @@ async fn get_epub( /// the resource path) async fn get_epub_chapter( Path((id, chapter)): Path<(String, usize)>, - Extension(ctx): State, + State(ctx): State, ) -> ApiResult { let book = ctx .db @@ -95,7 +98,7 @@ async fn get_epub_chapter( async fn get_epub_meta( // TODO: does this work? Path((id, root, resource)): Path<(String, String, PathBuf)>, - Extension(ctx): State, + State(ctx): State, ) -> ApiResult { let book = ctx .db diff --git a/apps/server/src/routers/api/filesystem.rs b/apps/server/src/routers/api/v1/filesystem.rs similarity index 68% rename from apps/server/src/routers/api/filesystem.rs rename to apps/server/src/routers/api/v1/filesystem.rs index 057eb9251..84dfcc8e3 100644 --- a/apps/server/src/routers/api/filesystem.rs +++ b/apps/server/src/routers/api/v1/filesystem.rs @@ -1,44 +1,54 @@ -use axum::{extract::Query, middleware::from_extractor, routing::post, Json, Router}; +use axum::{ + extract::Query, + middleware::{from_extractor, from_extractor_with_state}, + routing::post, + Json, Router, +}; use axum_sessions::extractors::ReadableSession; use std::path::Path; -use stump_core::types::{ - DirectoryListing, DirectoryListingFile, DirectoryListingInput, Pageable, - PagedRequestParams, +use stump_core::prelude::{ + DirectoryListing, DirectoryListingFile, DirectoryListingInput, PageQuery, Pageable, }; use tracing::trace; use crate::{ + config::state::AppState, errors::{ApiError, ApiResult}, middleware::auth::{AdminGuard, Auth}, - utils::get_session_user, + utils::get_session_admin_user, }; -pub(crate) fn mount() -> Router { +pub(crate) fn mount(app_state: AppState) -> Router { Router::new() .route("/filesystem", post(list_directory)) .layer(from_extractor::()) - .layer(from_extractor::()) + .layer(from_extractor_with_state::(app_state)) } +#[utoipa::path( + post, + path = "/api/v1/filesystem", + tag = "filesystem", + request_body = Option, + params( + ("pagination" = Option, Query, description = "Pagination parameters for the directory listing.") + ), + responses( + (status = 200, description = "Successfully retrieved contents of directory", body = PageableDirectoryListing), + (status = 400, description = "Invalid request."), + (status = 401, description = "No user is logged in (unauthorized)."), + (status = 403, description = "User does not have permission to access this resource."), + (status = 404, description = "Directory does not exist."), + ) +)] /// List the contents of a directory on the file system at a given (optional) path. If no path /// is provided, the file system root directory contents is returned. pub async fn list_directory( - input: Json>, session: ReadableSession, - pagination: Query, + pagination: Query, + input: Json>, ) -> ApiResult>> { - let user = get_session_user(&session)?; - - // FIXME: The auth extractor middleware doesn't check admin, but I don't want to have this check - // here. I thought of making another extractor, but it would be the same code as the auth with - // one additional check, which is way too much duplication for me. I'll have to look into - // how I can extend middlewares, rather than just copy it from scratch... - if !user.is_admin() { - return Err(ApiError::Forbidden( - "You must be an admin to access this endpoint".to_string(), - )); - } - + let _ = get_session_admin_user(&session)?; let input = input.0.unwrap_or_default(); let start_path = input.path.unwrap_or_else(|| { diff --git a/apps/server/src/routers/api/v1/job.rs b/apps/server/src/routers/api/v1/job.rs new file mode 100644 index 000000000..bbfa66d4e --- /dev/null +++ b/apps/server/src/routers/api/v1/job.rs @@ -0,0 +1,109 @@ +use axum::{ + extract::{Path, State}, + middleware::{from_extractor, from_extractor_with_state}, + routing::{delete, get}, + Json, Router, +}; +use stump_core::{event::InternalCoreTask, job::JobReport}; +use tokio::sync::oneshot; +use tracing::debug; + +use crate::{ + config::state::AppState, + errors::{ApiError, ApiResult}, + middleware::auth::{AdminGuard, Auth}, +}; + +pub(crate) fn mount(app_state: AppState) -> Router { + Router::new() + .nest( + "/jobs", + Router::new() + .route("/", get(get_job_reports).delete(delete_job_reports)) + .route("/:id/cancel", delete(cancel_job)), + ) + .layer(from_extractor::()) + .layer(from_extractor_with_state::(app_state)) +} + +#[utoipa::path( + get, + path = "/api/v1/jobs", + tag = "job", + responses( + (status = 200, description = "Successfully retrieved job reports", body = [JobReport]), + (status = 401, description = "No user is logged in (unauthorized)."), + (status = 403, description = "User does not have permission to access this resource."), + (status = 500, description = "Internal server error."), + ) +)] +/// Get all running/pending jobs. +async fn get_job_reports(State(ctx): State) -> ApiResult>> { + let (task_tx, task_rx) = oneshot::channel(); + + ctx.internal_task(InternalCoreTask::GetJobReports(task_tx)) + .map_err(|e| { + ApiError::InternalServerError(format!( + "Failed to submit internal task: {}", + e + )) + })?; + + let res = task_rx.await.map_err(|e| { + ApiError::InternalServerError(format!("Failed to get job report: {}", e)) + })??; + + Ok(Json(res)) +} + +#[utoipa::path( + delete, + path = "/api/v1/jobs", + tag = "job", + responses( + (status = 200, description = "Successfully deleted job reports"), + (status = 401, description = "No user is logged in (unauthorized)."), + (status = 403, description = "User does not have permission to access this resource."), + (status = 500, description = "Internal server error."), + ) +)] +/// Delete all job reports. +async fn delete_job_reports(State(ctx): State) -> ApiResult<()> { + let result = ctx.db.job().delete_many(vec![]).exec().await?; + debug!("Deleted {} job reports", result); + Ok(()) +} + +#[utoipa::path( + delete, + path = "/api/v1/jobs/:id/cancel", + tag = "job", + params( + ("id" = String, Path, description = "The ID of the job to cancel.") + ), + responses( + (status = 200, description = "Successfully cancelled job"), + (status = 401, description = "No user is logged in (unauthorized)."), + (status = 403, description = "User does not have permission to access this resource."), + (status = 500, description = "Internal server error."), + ) +)] +/// Cancel a running job. This will not delete the job report. +async fn cancel_job( + State(ctx): State, + Path(job_id): Path, +) -> ApiResult<()> { + let (task_tx, task_rx) = oneshot::channel(); + + ctx.internal_task(InternalCoreTask::CancelJob { + job_id, + return_sender: task_tx, + }) + .map_err(|e| { + ApiError::InternalServerError(format!("Failed to submit internal task: {}", e)) + })?; + + Ok(task_rx.await.map_err(|e| { + ApiError::InternalServerError(format!("Failed to cancel job: {}", e)) + })??) +} diff --git a/apps/server/src/routers/api/v1/library.rs b/apps/server/src/routers/api/v1/library.rs new file mode 100644 index 000000000..5d523ac57 --- /dev/null +++ b/apps/server/src/routers/api/v1/library.rs @@ -0,0 +1,693 @@ +use axum::{ + extract::{Path, State}, + middleware::from_extractor_with_state, + routing::get, + Json, Router, +}; +use axum_extra::extract::Query; +use axum_sessions::extractors::ReadableSession; +use prisma_client_rust::{raw, Direction}; +use std::{path, str::FromStr}; +use tracing::{debug, error, trace}; + +use stump_core::{ + db::{ + models::{LibrariesStats, Library, LibraryScanMode, Series}, + utils::PrismaCountTrait, + }, + fs::{image, media_file}, + job::LibraryScanJob, + prelude::{ + CreateLibraryArgs, Pageable, Pagination, PaginationQuery, ScanQueryParam, + UpdateLibraryArgs, + }, + prisma::{ + library::{self, WhereParam}, + library_options, media, + series::{self, OrderByParam as SeriesOrderByParam}, + tag, + }, +}; + +use crate::{ + config::state::AppState, + errors::{ApiError, ApiResult}, + middleware::auth::Auth, + utils::{ + get_session_admin_user, http::ImageResponse, FilterableQuery, LibraryFilter, + }, +}; + +// TODO: .layer(from_extractor::()) where needed. Might need to remove some +// of the nesting +pub(crate) fn mount(app_state: AppState) -> Router { + Router::new() + .route("/libraries", get(get_libraries).post(create_library)) + .route("/libraries/stats", get(get_libraries_stats)) + .nest( + "/libraries/:id", + Router::new() + .route( + "/", + get(get_library_by_id) + .put(update_library) + .delete(delete_library), + ) + .route("/scan", get(scan_library)) + .route("/series", get(get_library_series)) + .route("/thumbnail", get(get_library_thumbnail)), + ) + .layer(from_extractor_with_state::(app_state)) +} + +pub(crate) fn apply_library_filters(filters: LibraryFilter) -> Vec { + let mut _where: Vec = vec![]; + + if !filters.id.is_empty() { + _where.push(library::id::in_vec(filters.id)) + } + if !filters.name.is_empty() { + _where.push(library::name::in_vec(filters.name)); + } + + _where +} + +#[utoipa::path( + get, + path = "/api/v1/libraries", + tag = "library", + params( + ("filter_query" = Option, Query, description = "The library filters"), + ("pagination_query" = Option, Query, description = "The pagination options") + ), + responses( + (status = 200, description = "Successfully retrieved libraries", body = PageableLibraries), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Internal server error") + ) +)] +/// Get all libraries +#[tracing::instrument(skip(ctx), err)] +async fn get_libraries( + filter_query: Query>, + pagination_query: Query, + State(ctx): State, +) -> ApiResult>>> { + let FilterableQuery { filters, ordering } = filter_query.0.get(); + let pagination = pagination_query.0.get(); + + let is_unpaged = pagination.is_unpaged(); + let where_conditions = apply_library_filters(filters); + let order_by = ordering.try_into()?; + + let mut query = ctx + .db + .library() + .find_many(where_conditions.clone()) + .with(library::tags::fetch(vec![])) + .with(library::library_options::fetch()) + .order_by(order_by); + + if !is_unpaged { + match pagination.clone() { + Pagination::Page(page_query) => { + let (skip, take) = page_query.get_skip_take(); + query = query.skip(skip).take(take); + }, + Pagination::Cursor(cursor_query) => { + if let Some(cursor) = cursor_query.cursor { + query = query.cursor(library::id::equals(cursor)).skip(1) + } + if let Some(limit) = cursor_query.limit { + query = query.take(limit) + } + }, + _ => unreachable!(), + } + } + + let libraries = query + .exec() + .await? + .into_iter() + .map(|l| l.into()) + .collect::>(); + + if is_unpaged { + return Ok(Json(libraries.into())); + } + + let count = ctx.db.library().count(where_conditions).exec().await?; + + Ok(Json((libraries, count, pagination).into())) +} + +#[utoipa::path( + get, + path = "/api/v1/libraries/stats", + tag = "library", + responses( + (status = 200, description = "Successfully fetched stats", body = LibrariesStats), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Internal server error") + ) +)] +/// Get stats for all libraries +async fn get_libraries_stats( + State(ctx): State, +) -> ApiResult> { + let db = ctx.get_db(); + + // TODO: maybe add more, like missingBooks, idk + let stats = db + ._query_raw::(raw!( + "SELECT COUNT(*) as book_count, IFNULL(SUM(media.size),0) as total_bytes, IFNULL(series_count,0) as series_count FROM media INNER JOIN (SELECT COUNT(*) as series_count FROM series)" + )) + .exec() + .await? + .into_iter() + .next(); + + if stats.is_none() { + return Err(ApiError::InternalServerError( + "Failed to compute stats for libraries".to_string(), + )); + } + + Ok(Json(stats.unwrap())) +} + +#[utoipa::path( + get, + path = "/api/v1/libraries/:id", + tag = "library", + params( + ("id" = String, Path, description = "The library ID") + ), + responses( + (status = 200, description = "Successfully retrieved library", body = Library), + (status = 401, description = "Unauthorized"), + (status = 404, description = "Library not found"), + (status = 500, description = "Internal server error") + ) +)] +/// Get a library by id, if the current user has access to it. Library `series`, `media` +/// and `tags` relations are loaded on this route. +async fn get_library_by_id( + Path(id): Path, + State(ctx): State, +) -> ApiResult> { + let db = ctx.get_db(); + + // FIXME: this query is a pain to add series->media relation counts. + // This should be much better in https://github.com/Brendonovich/prisma-client-rust/issues/24 + // but for now I kinda have to load all the media... + let library = db + .library() + .find_unique(library::id::equals(id.clone())) + .with(library::series::fetch(vec![])) + .with(library::library_options::fetch()) + .with(library::tags::fetch(vec![])) + .exec() + .await?; + + if library.is_none() { + return Err(ApiError::NotFound(format!( + "Library with id {} not found", + id + ))); + } + + let library = library.unwrap(); + + Ok(Json(library.into())) +} + +#[utoipa::path( + get, + path = "/api/v1/libraries/:id/series", + tag = "library", + params( + ("id" = String, Path, description = "The library ID"), + ("filter_query" = Option, Query, description = "The library filters"), + ("pagination_query" = Option, Query, description = "The pagination options") + ), + responses( + (status = 200, description = "Successfully retrieved series", body = PageableSeries), + (status = 401, description = "Unauthorized"), + (status = 404, description = "Library not found"), + (status = 500, description = "Internal server error") + ) +)] +// FIXME: this is absolutely atrocious... +// This should be much better once https://github.com/Brendonovich/prisma-client-rust/issues/24 is added +// but for now I will have this disgustingly gross and ugly work around... +/// Returns the series in a given library. Will *not* load the media relation. +async fn get_library_series( + filter_query: Query>, + pagination_query: Query, + Path(id): Path, + State(ctx): State, +) -> ApiResult>>> { + let FilterableQuery { ordering, .. } = filter_query.0.get(); + let pagination = pagination_query.0.get(); + let db = ctx.get_db(); + + let is_unpaged = pagination.is_unpaged(); + let order_by_param: SeriesOrderByParam = ordering.try_into()?; + + let where_conditions = vec![series::library_id::equals(Some(id.clone()))]; + let mut query = db + .series() + // TODO: add media relation count.... + .find_many(where_conditions.clone()) + .order_by(order_by_param); + + if !is_unpaged { + match pagination.clone() { + Pagination::Page(page_query) => { + let (skip, take) = page_query.get_skip_take(); + query = query.skip(skip).take(take); + }, + Pagination::Cursor(cursor_query) => { + if let Some(cursor) = cursor_query.cursor { + query = query.cursor(series::id::equals(cursor)).skip(1) + } + if let Some(limit) = cursor_query.limit { + query = query.take(limit) + } + }, + _ => unreachable!(), + } + } + + let series = query.exec().await?; + let series_ids = series.iter().map(|s| s.id.clone()).collect(); + let media_counts = db.series_media_count(series_ids).await?; + + let series = series + .iter() + .map(|s| { + let media_count = match media_counts.get(&s.id) { + Some(count) => count.to_owned(), + _ => 0, + }; + + (s.to_owned(), media_count).into() + }) + .collect::>(); + + if is_unpaged { + return Ok(Json(series.into())); + } + + let series_count = db + .series() + .count(vec![series::library_id::equals(Some(id.clone()))]) + .exec() + .await?; + + Ok(Json((series, series_count, pagination).into())) +} + +// TODO: ImageResponse for utoipa +#[utoipa::path( + get, + path = "/api/v1/libraries/:id/thumbnail", + tag = "library", + params( + ("id" = String, Path, description = "The library ID"), + ), + responses( + (status = 200, description = "Successfully retrieved library thumbnail"), + (status = 401, description = "Unauthorized"), + (status = 404, description = "Library not found"), + (status = 500, description = "Internal server error") + ) +)] +/// Get the thumbnail image for a library by id, if the current user has access to it. +async fn get_library_thumbnail( + Path(id): Path, + State(ctx): State, +) -> ApiResult { + let db = ctx.get_db(); + + let library_series = db + .series() + .find_many(vec![series::library_id::equals(Some(id.clone()))]) + .with(series::media::fetch(vec![]).order_by(media::name::order(Direction::Asc))) + .exec() + .await?; + + // TODO: error handling + let series = library_series.first().expect("No series in library"); + let media = series.media()?.first().expect("No media in series"); + + Ok(media_file::get_page(media.path.as_str(), 1)?.into()) +} + +#[utoipa::path( + post, + path = "/api/v1/libraries/:id/scan", + tag = "library", + params( + ("id" = String, Path, description = "The library ID"), + ("query" = ScanQueryParam, Query, description = "The scan options"), + ), + responses( + (status = 200, description = "Successfully queued library scan"), + (status = 401, description = "Unauthorized"), + (status = 404, description = "Library not found"), + (status = 500, description = "Internal server error") + ) +)] +/// Queue a ScannerJob to scan the library by id. The job, when started, is +/// executed in a separate thread. +async fn scan_library( + Path(id): Path, + State(ctx): State, + query: Query, + session: ReadableSession, +) -> Result<(), ApiError> { + let db = ctx.get_db(); + let _user = get_session_admin_user(&session)?; + + let lib = db + .library() + .find_unique(library::id::equals(id.clone())) + .exec() + .await?; + + if lib.is_none() { + return Err(ApiError::NotFound(format!( + "Library with id {} not found", + id + ))); + } + + let lib = lib.unwrap(); + + let scan_mode = query.scan_mode.to_owned().unwrap_or_default(); + let scan_mode = LibraryScanMode::from_str(&scan_mode) + .map_err(|e| ApiError::BadRequest(format!("Invalid scan mode: {}", e)))?; + + // TODO: should this just be an error? + if scan_mode != LibraryScanMode::None { + let job = LibraryScanJob { + path: lib.path, + scan_mode, + }; + + return Ok(ctx.spawn_job(Box::new(job))?); + } + + Ok(()) +} + +#[utoipa::path( + post, + path = "/api/v1/libraries", + tag = "library", + request_body = CreateLibraryArgs, + responses( + (status = 200, description = "Successfully created library"), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden"), + (status = 500, description = "Internal server error") + ) +)] +/// Create a new library. Will queue a ScannerJob to scan the library, and return the library +async fn create_library( + session: ReadableSession, + State(ctx): State, + Json(input): Json, +) -> ApiResult> { + get_session_admin_user(&session)?; + + let db = ctx.get_db(); + + if !path::Path::new(&input.path).exists() { + return Err(ApiError::BadRequest(format!( + "The library directory does not exist: {}", + input.path + ))); + } + + let child_libraries = db + .library() + .find_many(vec![library::path::starts_with(input.path.clone())]) + .exec() + .await?; + + if !child_libraries.is_empty() { + return Err(ApiError::BadRequest(String::from( + "You may not create a library that is a parent of another on the filesystem.", + ))); + } + + // TODO: refactor once nested create is supported + // https://github.com/Brendonovich/prisma-client-rust/issues/44 + let library_options_arg = input.library_options.unwrap_or_default(); + let transaction_result: Result = db + ._transaction() + .run(|client| async move { + let library_options = client + .library_options() + .create(vec![ + library_options::convert_rar_to_zip::set( + library_options_arg.convert_rar_to_zip, + ), + library_options::hard_delete_conversions::set( + library_options_arg.hard_delete_conversions, + ), + library_options::create_webp_thumbnails::set( + library_options_arg.create_webp_thumbnails, + ), + library_options::library_pattern::set( + library_options_arg.library_pattern.to_string(), + ), + ]) + .exec() + .await?; + + let library = client + .library() + .create( + input.name.to_owned(), + input.path.to_owned(), + library_options::id::equals(library_options.id), + vec![library::description::set(input.description.to_owned())], + ) + .exec() + .await?; + + // FIXME: try and do multiple connects again soon, batching is WAY better than + // previous solution but still... + if let Some(tags) = input.tags.to_owned() { + let library_id = library.id.clone(); + let tag_connect = tags.into_iter().map(|tag| { + client.library().update( + library::id::equals(library_id.clone()), + vec![library::tags::connect(vec![tag::id::equals(tag.id)])], + ) + }); + + client._batch(tag_connect).await?; + } + + Ok(Library::from(library)) + }) + .await; + + let library = transaction_result?; + let scan_mode = input.scan_mode.unwrap_or_default(); + // `scan` is not a required field, however it will default to BATCHED if not provided + if scan_mode != LibraryScanMode::None { + ctx.spawn_job(Box::new(LibraryScanJob { + path: library.path.clone(), + scan_mode, + }))?; + } + + Ok(Json(library)) +} + +#[utoipa::path( + put, + path = "/api/v1/libraries/:id", + tag = "library", + request_body = UpdateLibraryArgs, + params( + ("id" = String, Path, description = "The id of the library to update") + ), + responses( + (status = 200, description = "Successfully updated library"), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden"), + (status = 404, description = "Not found"), + (status = 500, description = "Internal server error") + ) +)] +/// Update a library by id, if the current user is a SERVER_OWNER. +async fn update_library( + session: ReadableSession, + State(ctx): State, + Path(id): Path, + Json(input): Json, +) -> ApiResult> { + get_session_admin_user(&session)?; + let db = ctx.get_db(); + + if !path::Path::new(&input.path).exists() { + return Err(ApiError::BadRequest(format!( + "Updated path does not exist: {}", + input.path + ))); + } + + let library_options = input.library_options.to_owned(); + + db.library_options() + .update( + library_options::id::equals(library_options.id.unwrap_or_default()), + vec![ + library_options::convert_rar_to_zip::set( + library_options.convert_rar_to_zip, + ), + library_options::hard_delete_conversions::set( + library_options.hard_delete_conversions, + ), + library_options::create_webp_thumbnails::set( + library_options.create_webp_thumbnails, + ), + ], + ) + .exec() + .await?; + + let mut batches = vec![]; + + // FIXME: this is disgusting. I don't understand why the library::tag::connect doesn't + // work with multiple tags, nor why providing multiple library::tag::connect params + // doesn't work. Regardless, absolutely do NOT keep this. Correction required, + // highly inefficient queries. + + if let Some(tags) = input.tags.to_owned() { + for tag in tags { + batches.push(db.library().update( + library::id::equals(id.clone()), + vec![library::tags::connect(vec![tag::id::equals( + tag.id.to_owned(), + )])], + )); + } + } + + if let Some(removed_tags) = input.removed_tags.to_owned() { + for tag in removed_tags { + batches.push(db.library().update( + library::id::equals(id.clone()), + vec![library::tags::disconnect(vec![tag::id::equals( + tag.id.to_owned(), + )])], + )); + } + } + + if !batches.is_empty() { + db._batch(batches).await?; + } + + let updated = db + .library() + .update( + library::id::equals(id), + vec![ + library::name::set(input.name.to_owned()), + library::path::set(input.path.to_owned()), + library::description::set(input.description.to_owned()), + ], + ) + .with(library::tags::fetch(vec![])) + .exec() + .await?; + + let scan_mode = input.scan_mode.unwrap_or_default(); + + // `scan` is not a required field, however it will default to BATCHED if not provided + if scan_mode != LibraryScanMode::None { + ctx.spawn_job(Box::new(LibraryScanJob { + path: updated.path.clone(), + scan_mode, + }))?; + } + + Ok(Json(updated.into())) +} + +#[utoipa::path( + delete, + path = "/api/v1/libraries/:id", + tag = "library", + params( + ("id" = String, Path, description = "The id of the library to delete") + ), + responses( + (status = 200, description = "Successfully deleted library"), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden"), + (status = 404, description = "Not found"), + (status = 500, description = "Internal server error") + ) +)] +/// Delete a library by id +async fn delete_library( + session: ReadableSession, + Path(id): Path, + State(ctx): State, +) -> ApiResult> { + get_session_admin_user(&session)?; + let db = ctx.get_db(); + + trace!(?id, "Attempting to delete library"); + + let deleted = db + .library() + .delete(library::id::equals(id.clone())) + .include(library::include!({ + series: include { + media: select { + id + } + } + })) + .exec() + .await?; + + let media_ids = deleted + .series + .into_iter() + .flat_map(|series| series.media) + .map(|media| media.id) + .collect::>(); + + if !media_ids.is_empty() { + trace!(?media_ids, "Deleted media"); + debug!( + "Attempting to delete {} media thumbnails (if present)", + media_ids.len() + ); + + if let Err(err) = image::remove_thumbnails(&media_ids) { + error!("Failed to remove thumbnails for library media: {:?}", err); + } else { + debug!("Removed thumbnails for library media (if present)"); + } + } + + Ok(Json(deleted.id)) +} diff --git a/apps/server/src/routers/api/log.rs b/apps/server/src/routers/api/v1/log.rs similarity index 50% rename from apps/server/src/routers/api/log.rs rename to apps/server/src/routers/api/v1/log.rs index 8fd3c1b3a..c516e2a0b 100644 --- a/apps/server/src/routers/api/log.rs +++ b/apps/server/src/routers/api/v1/log.rs @@ -1,38 +1,57 @@ -use axum::{middleware::from_extractor, routing::get, Json, Router}; +use axum::{middleware::from_extractor_with_state, routing::get, Json, Router}; use axum_sessions::extractors::ReadableSession; use prisma_client_rust::chrono::{DateTime, Utc}; use std::fs::File; -use stump_core::{config::logging::get_log_file, types::LogMetadata}; +use stump_core::{config::logging::get_log_file, db::models::LogMetadata}; use crate::{ + config::state::AppState, errors::{ApiError, ApiResult}, middleware::auth::Auth, - utils::get_session_user, + utils::get_session_admin_user, }; -pub(crate) fn mount() -> Router { +pub(crate) fn mount(app_state: AppState) -> Router { Router::new() - .route("/logs", get(get_log_info).delete(clear_logs)) + .route("/logs", get(get_logs).delete(clear_logs)) + .route("/logs/info", get(get_logfile_info)) // FIXME: admin middleware - .layer(from_extractor::()) + .layer(from_extractor_with_state::(app_state)) } // TODO: there is a database entity Log. If that stays, I should differenciate between // the stump.log file operations and the database operations better in this file. For now, // I'm just going to leave it as is. +#[utoipa::path( + get, + path = "/api/v1/logs", + tag = "log", + responses( + (status = 500, description = "Internal server error."), + ) +)] +/// Get all logs from the database. +async fn get_logs() -> ApiResult<()> { + // TODO: implement + Err(ApiError::NotImplemented) +} + +#[utoipa::path( + get, + path = "/api/v1/logs/info", + tag = "log", + responses( + (status = 200, description = "Successfully retrieved log info", body = LogMetadata), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 500, description = "Internal server error."), + ) +)] /// Get information about the Stump log file, located at STUMP_CONFIG_DIR/Stump.log, or /// ~/.stump/Stump.log by default. Information such as the file size, last modified date, etc. -// #[get("/logs")] -async fn get_log_info(session: ReadableSession) -> ApiResult> { - let user = get_session_user(&session)?; - - if !user.is_admin() { - return Err(ApiError::Forbidden( - "You must be an admin to access this resource.".into(), - )); - } - +async fn get_logfile_info(session: ReadableSession) -> ApiResult> { + get_session_admin_user(&session)?; let log_file_path = get_log_file(); let file = File::open(log_file_path.as_path())?; @@ -47,6 +66,17 @@ async fn get_log_info(session: ReadableSession) -> ApiResult> })) } +#[utoipa::path( + delete, + path = "/api/v1/logs", + tag = "log", + responses( + (status = 200, description = "Successfully cleared logs."), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 500, description = "Internal server error."), + ) +)] /// Clear the Stump log file, located at STUMP_CONFIG_DIR/Stump.log, or /// ~/.stump/Stump.log by default. // Note: I think it is important to point out that this `delete` actually creates @@ -54,14 +84,7 @@ async fn get_log_info(session: ReadableSession) -> ApiResult> // this route *WILL* delete all of the file contents. // #[delete("/logs")] async fn clear_logs(session: ReadableSession) -> ApiResult<()> { - let user = get_session_user(&session)?; - - if !user.is_admin() { - return Err(ApiError::Forbidden( - "You must be an admin to access this resource.".into(), - )); - } - + get_session_admin_user(&session)?; let log_file_path = get_log_file(); File::create(log_file_path.as_path())?; diff --git a/apps/server/src/routers/api/v1/media.rs b/apps/server/src/routers/api/v1/media.rs new file mode 100644 index 000000000..d1d85c000 --- /dev/null +++ b/apps/server/src/routers/api/v1/media.rs @@ -0,0 +1,591 @@ +use axum::{ + extract::{Path, State}, + middleware::from_extractor_with_state, + routing::{get, put}, + Json, Router, +}; +use axum_extra::extract::Query; +use axum_sessions::extractors::ReadableSession; +use prisma_client_rust::Direction; +use serde::Deserialize; +use stump_core::{ + config::get_config_dir, + db::{ + models::{Media, ReadProgress}, + utils::PrismaCountTrait, + Dao, MediaDao, MediaDaoImpl, + }, + fs::{image, media_file}, + prelude::{ContentType, PageQuery, Pageable, Pagination, PaginationQuery}, + prisma::{ + media::{self, OrderByParam as MediaOrderByParam, WhereParam}, + read_progress, user, + }, +}; +use tracing::{debug, trace}; + +use crate::{ + config::state::AppState, + errors::{ApiError, ApiResult}, + middleware::auth::Auth, + utils::{ + get_session_user, + http::{ImageResponse, NamedFile}, + FilterableQuery, MediaFilter, + }, +}; + +use super::series::apply_series_filters; + +pub(crate) fn mount(app_state: AppState) -> Router { + Router::new() + .route("/media", get(get_media)) + .route("/media/duplicates", get(get_duplicate_media)) + .route("/media/keep-reading", get(get_in_progress_media)) + .route("/media/recently-added", get(get_recently_added_media)) + .nest( + "/media/:id", + Router::new() + .route("/", get(get_media_by_id)) + .route("/file", get(get_media_file)) + .route("/convert", get(convert_media)) + .route("/thumbnail", get(get_media_thumbnail)) + .route("/page/:page", get(get_media_page)) + .route("/progress/:page", put(update_media_progress)), + ) + .layer(from_extractor_with_state::(app_state)) +} + +pub(crate) fn apply_media_filters(filters: MediaFilter) -> Vec { + let mut _where: Vec = vec![]; + + if !filters.id.is_empty() { + _where.push(media::id::in_vec(filters.id)); + } + if !filters.name.is_empty() { + _where.push(media::name::in_vec(filters.name)); + } + if !filters.extension.is_empty() { + _where.push(media::extension::in_vec(filters.extension)); + } + + if let Some(series_filters) = filters.series { + _where.push(media::series::is(apply_series_filters(series_filters))); + } + + _where +} + +#[utoipa::path( + get, + path = "/api/v1/media", + tag = "media", + params( + ("filter_query" = Option, Query, description = "The optional media filters"), + ("pagination_query" = Option, Query, description = "The pagination options"), + ), + responses( + (status = 200, description = "Successfully fetched media", body = PageableMedia), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 500, description = "Internal server error."), + ) +)] +/// Get all media accessible to the requester. This is a paginated request, and +/// has various pagination params available. +#[tracing::instrument(skip(ctx, session))] +async fn get_media( + filter_query: Query>, + pagination_query: Query, + State(ctx): State, + session: ReadableSession, +) -> ApiResult>>> { + let FilterableQuery { filters, ordering } = filter_query.0.get(); + let pagination = pagination_query.0.get(); + + trace!(?filters, ?ordering, ?pagination, "get_media"); + + let db = ctx.get_db(); + let user_id = get_session_user(&session)?.id; + + let is_unpaged = pagination.is_unpaged(); + let order_by_param: MediaOrderByParam = ordering.try_into()?; + + let pagination_cloned = pagination.clone(); + let where_conditions = apply_media_filters(filters); + + let (media, count) = db + ._transaction() + .run(|client| async move { + let mut query = client + .media() + .find_many(where_conditions.clone()) + .with(media::read_progresses::fetch(vec![ + read_progress::user_id::equals(user_id), + ])) + .order_by(order_by_param); + + if !is_unpaged { + match pagination_cloned { + Pagination::Page(page_query) => { + let (skip, take) = page_query.get_skip_take(); + query = query.skip(skip).take(take); + }, + Pagination::Cursor(cursor_query) => { + if let Some(cursor) = cursor_query.cursor { + query = query.cursor(media::id::equals(cursor)).skip(1) + } + if let Some(limit) = cursor_query.limit { + query = query.take(limit) + } + }, + _ => unreachable!(), + } + } + + let media = query + .exec() + .await? + .into_iter() + .map(|m| m.into()) + .collect::>(); + + if is_unpaged { + return Ok((media, None)); + } + + client + .media() + .count(where_conditions) + .exec() + .await + .map(|count| (media, Some(count))) + }) + .await?; + + if let Some(count) = count { + return Ok(Json(Pageable::from((media, count, pagination)))); + } + + Ok(Json(Pageable::from(media))) +} + +#[utoipa::path( + get, + path = "/api/v1/media/duplicates", + tag = "media", + params( + ("pagination" = Option, Query, description = "Pagination options") + ), + responses( + (status = 200, description = "Successfully fetched duplicate media", body = PageableMedia), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 500, description = "Internal server error."), + ) +)] +/// Get all media with identical checksums. This heavily implies duplicate files, +/// however it is not a guarantee (checksums are generated from the first chunk of +/// the file, so if a 2 comic books had say the same first 6 pages it might return a +/// false positive). This is a paginated request, and has various pagination +/// params available, but hopefully you won't have that many duplicates ;D +async fn get_duplicate_media( + pagination: Query, + State(ctx): State, + _session: ReadableSession, +) -> ApiResult>>> { + let media_dao = MediaDaoImpl::new(ctx.db.clone()); + + if pagination.page.is_none() { + return Ok(Json(Pageable::from(media_dao.get_duplicate_media().await?))); + } + + Ok(Json( + media_dao + .get_duplicate_media_page(pagination.0.page_params()) + .await?, + )) +} + +#[utoipa::path( + get, + path = "/api/v1/media/in-progress", + tag = "media", + params( + ("pagination" = Option, Query, description = "Pagination options") + ), + responses( + (status = 200, description = "Successfully fetched in progress media", body = PageableMedia), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 500, description = "Internal server error."), + ) +)] +/// Get all media which the requester has progress for that is less than the +/// total number of pages available (i.e not completed). +async fn get_in_progress_media( + State(ctx): State, + session: ReadableSession, + pagination: Query, +) -> ApiResult>>> { + let user_id = get_session_user(&session)?.id; + let media_dao = MediaDaoImpl::new(ctx.db.clone()); + let page_params = pagination.0.page_params(); + + Ok(Json( + media_dao + .get_in_progress_media(&user_id, page_params) + .await?, + )) +} + +#[utoipa::path( + get, + path = "/api/v1/media/recently-added", + tag = "media", + params( + ("pagination" = Option, Query, description = "Pagination options") + ), + responses( + (status = 200, description = "Successfully fetched recently added media", body = PageableMedia), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 500, description = "Internal server error."), + ) +)] +/// Get all media which was added to the library in descending order of when it +/// was added. +async fn get_recently_added_media( + State(ctx): State, + pagination: Query, +) -> ApiResult>>> { + let db = ctx.get_db(); + + let unpaged = pagination.page.is_none(); + let page_params = pagination.0.page_params(); + + let mut query = db + .media() + .find_many(vec![]) + .order_by(media::created_at::order(Direction::Desc)); + + if !unpaged { + let (skip, take) = page_params.get_skip_take(); + query = query.skip(skip).take(take); + } + + let media = query + .exec() + .await? + .into_iter() + .map(|m| m.into()) + .collect::>(); + + if unpaged { + return Ok(Json(Pageable::from(media))); + } + + let count = db.media_count().await?; + + Ok(Json(Pageable::from((media, count, page_params)))) +} + +#[derive(Deserialize)] +struct LoadSeries { + load_series: Option, +} + +#[utoipa::path( + get, + path = "/api/v1/media/:id", + tag = "media", + params( + ("id" = String, Path, description = "The ID of the media to get"), + ("load_series" = Option, Query, description = "Whether to load the series relation for the media") + ), + responses( + (status = 200, description = "Successfully fetched media", body = Media), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 404, description = "Media not found."), + (status = 500, description = "Internal server error."), + ) +)] +/// Get a media by its ID. If provided, the `load_series` query param will load +/// the series relation for the media. +async fn get_media_by_id( + Path(id): Path, + params: Query, + State(ctx): State, + session: ReadableSession, +) -> ApiResult> { + let db = ctx.get_db(); + let user_id = get_session_user(&session)?.id; + + let mut query = db.media().find_unique(media::id::equals(id.clone())).with( + media::read_progresses::fetch(vec![read_progress::user_id::equals(user_id)]), + ); + + if params.load_series.unwrap_or_default() { + trace!(media_id = id, "Loading series relation for media"); + query = query.with(media::series::fetch()); + } + + let result = query.exec().await?; + debug!(media_id = id, ?result, "Get media by id"); + + if result.is_none() { + return Err(ApiError::NotFound(format!( + "Media with id {} not found", + id + ))); + } + + Ok(Json(Media::from(result.unwrap()))) +} + +// TODO: type a body +#[utoipa::path( + get, + path = "/api/v1/media/:id/file", + tag = "media", + params( + ("id" = String, Path, description = "The ID of the media") + ), + responses( + (status = 200, description = "Successfully fetched media file"), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 404, description = "Media not found."), + (status = 500, description = "Internal server error."), + ) +)] +/// Download the file associated with the media. +async fn get_media_file( + Path(id): Path, + State(ctx): State, +) -> ApiResult { + let db = ctx.get_db(); + + let media = db + .media() + .find_unique(media::id::equals(id.clone())) + .exec() + .await?; + + if media.is_none() { + return Err(ApiError::NotFound(format!( + "Media with id {} not found", + id + ))); + } + + let media = media.unwrap(); + + Ok(NamedFile::open(media.path.clone()).await?) +} + +#[utoipa::path( + get, + path = "/api/v1/media/:id/convert", + tag = "media", + params( + ("id" = String, Path, description = "The ID of the media") + ), + responses( + (status = 200, description = "Successfully converted media"), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 404, description = "Media not found."), + (status = 500, description = "Internal server error."), + ) +)] +// TODO: remove this, implement it? maybe? +/// Converts a media file to a different format. Currently UNIMPLEMENTED. +async fn convert_media( + Path(id): Path, + State(ctx): State, +) -> Result<(), ApiError> { + let db = ctx.get_db(); + + let media = db + .media() + .find_unique(media::id::equals(id.clone())) + .exec() + .await?; + + if media.is_none() { + return Err(ApiError::NotFound(format!( + "Media with id {} not found", + id + ))); + } + + let media = media.unwrap(); + + if media.extension != "cbr" || media.extension != "rar" { + return Err(ApiError::BadRequest(format!( + "Media with id {} is not a rar file. Stump only supports converting rar/cbr files to zip/cbz.", + id + ))); + } + + Err(ApiError::NotImplemented) +} + +// TODO: ImageResponse as body type +#[utoipa::path( + get, + path = "/api/v1/media/:id/page/:page", + tag = "media", + params( + ("id" = String, Path, description = "The ID of the media to get"), + ("page" = i32, Path, description = "The page to get") + ), + responses( + (status = 200, description = "Successfully fetched media"), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 404, description = "Media not found."), + (status = 500, description = "Internal server error."), + ) +)] +/// Get a page of a media +async fn get_media_page( + Path((id, page)): Path<(String, i32)>, + State(ctx): State, + session: ReadableSession, +) -> ApiResult { + let db = ctx.get_db(); + let user_id = get_session_user(&session)?.id; + + let book = db + .media() + .find_unique(media::id::equals(id.clone())) + .with(media::read_progresses::fetch(vec![ + read_progress::user_id::equals(user_id), + ])) + .exec() + .await?; + + match book { + Some(book) => { + if page > book.pages { + // FIXME: probably won't work lol + Err(ApiError::Redirect(format!( + "/book/{}/read?page={}", + id, book.pages + ))) + } else { + Ok(media_file::get_page(&book.path, page)?.into()) + } + }, + None => Err(ApiError::NotFound(format!( + "Media with id {} not found", + id + ))), + } +} + +// TODO: ImageResponse as body type +#[utoipa::path( + get, + path = "/api/v1/media/:id/thumbnail", + tag = "media", + params( + ("id" = String, Path, description = "The ID of the media to get") + ), + responses( + (status = 200, description = "Successfully fetched media thumbnail"), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 404, description = "Media not found."), + (status = 500, description = "Internal server error."), + ) +)] +/// Get the thumbnail image of a media +async fn get_media_thumbnail( + Path(id): Path, + State(ctx): State, + session: ReadableSession, +) -> ApiResult { + let db = ctx.get_db(); + let user_id = get_session_user(&session)?.id; + + let webp_path = get_config_dir() + .join("thumbnails") + .join(format!("{}.webp", id)); + + if webp_path.exists() { + trace!("Found webp thumbnail for media {}", id); + return Ok((ContentType::WEBP, image::get_bytes(webp_path)?).into()); + } + + let book = db + .media() + .find_unique(media::id::equals(id.clone())) + .with(media::read_progresses::fetch(vec![ + read_progress::user_id::equals(user_id), + ])) + .exec() + .await?; + + if book.is_none() { + return Err(ApiError::NotFound(format!( + "Media with id {} not found", + id + ))); + } + + let book = book.unwrap(); + + Ok(media_file::get_page(book.path.as_str(), 1)?.into()) +} + +#[utoipa::path( + put, + path = "/api/v1/media/:id/read-progress", + tag = "media", + params( + ("id" = String, Path, description = "The ID of the media to get"), + ("page" = i32, Path, description = "The page to update the read progress to") + ), + responses( + (status = 200, description = "Successfully fetched media read progress"), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 404, description = "Media not found."), + (status = 500, description = "Internal server error."), + ) +)] +// FIXME: this doesn't really handle certain errors correctly, e.g. media/user not found +/// Update the read progress of a media. If the progress doesn't exist, it will be created. +async fn update_media_progress( + Path((id, page)): Path<(String, i32)>, + State(ctx): State, + session: ReadableSession, +) -> ApiResult> { + let db = ctx.get_db(); + let user_id = get_session_user(&session)?.id; + + // update the progress, otherwise create it + Ok(Json( + db.read_progress() + .upsert( + read_progress::UniqueWhereParam::UserIdMediaIdEquals( + user_id.clone(), + id.clone(), + ), + ( + page, + media::id::equals(id.clone()), + user::id::equals(user_id.clone()), + vec![], + ), + vec![read_progress::page::set(page)], + ) + .exec() + .await? + .into(), + )) +} diff --git a/apps/server/src/routers/api/v1/mod.rs b/apps/server/src/routers/api/v1/mod.rs new file mode 100644 index 000000000..14127efb2 --- /dev/null +++ b/apps/server/src/routers/api/v1/mod.rs @@ -0,0 +1,82 @@ +use axum::{ + extract::State, + routing::{get, post}, + Json, Router, +}; +use stump_core::prelude::{ClaimResponse, StumpVersion}; + +use crate::{config::state::AppState, errors::ApiResult}; + +pub(crate) mod auth; +pub(crate) mod epub; +pub(crate) mod filesystem; +pub(crate) mod job; +pub(crate) mod library; +pub(crate) mod log; +pub(crate) mod media; +pub(crate) mod reading_list; +pub(crate) mod series; +pub(crate) mod tag; +pub(crate) mod user; + +pub(crate) fn mount(app_state: AppState) -> Router { + Router::new() + .merge(auth::mount()) + .merge(epub::mount(app_state.clone())) + .merge(library::mount(app_state.clone())) + .merge(media::mount(app_state.clone())) + .merge(filesystem::mount(app_state.clone())) + .merge(job::mount(app_state.clone())) + .merge(log::mount(app_state.clone())) + .merge(series::mount(app_state.clone())) + .merge(tag::mount(app_state.clone())) + .merge(user::mount(app_state)) + .merge(reading_list::mount()) + .route("/claim", get(claim)) + .route("/ping", get(ping)) + .route("/version", post(version)) +} + +#[utoipa::path( + get, + path = "/api/v1/claim", + tag = "util", + responses( + (status = 200, description = "Claim status successfully determined", body = ClaimResponse) + ) +)] +async fn claim(State(ctx): State) -> ApiResult> { + let db = ctx.get_db(); + + Ok(Json(ClaimResponse { + is_claimed: db.user().find_first(vec![]).exec().await?.is_some(), + })) +} + +#[utoipa::path( + get, + path = "/api/v1/ping", + tag = "util", + responses( + (status = 200, description = "Always responds with 'pong'", body = String) + ) +)] +async fn ping() -> ApiResult { + Ok("pong".to_string()) +} + +#[utoipa::path( + post, + path = "/api/v1/version", + tag = "util", + responses( + (status = 200, description = "Version information for the Stump server instance", body = StumpVersion) + ) +)] +async fn version() -> ApiResult> { + Ok(Json(StumpVersion { + semver: env!("CARGO_PKG_VERSION").to_string(), + rev: std::env::var("GIT_REV").ok(), + compile_time: env!("STATIC_BUILD_DATE").to_string(), + })) +} diff --git a/apps/server/src/routers/api/v1/reading_list.rs b/apps/server/src/routers/api/v1/reading_list.rs new file mode 100644 index 000000000..81d09bbb6 --- /dev/null +++ b/apps/server/src/routers/api/v1/reading_list.rs @@ -0,0 +1,258 @@ +use crate::{ + config::state::AppState, + errors::{ApiError, ApiResult}, + utils::get_session_user, +}; +use axum::{ + extract::{Path, State}, + routing::get, + Json, Router, +}; +use axum_sessions::extractors::ReadableSession; +use stump_core::{ + db::models::ReadingList, + prelude::CreateReadingList, + prisma::{media, reading_list, user}, +}; +use tracing::log::trace; + +pub(crate) fn mount() -> Router { + Router::new() + .route( + "/reading-list", + get(get_reading_list).post(create_reading_list), + ) + .nest( + "/reading-list/:id", + Router::new().route( + "/", + get(get_reading_list_by_id) + .put(update_reading_list) + .delete(delete_reading_list_by_id), + ), + ) +} + +#[utoipa::path( + get, + path = "/api/v1/reading-list", + tag = "reading-list", + responses( + (status = 200, description = "Successfully fetched reading lists.", body = [ReadingList]), + (status = 401, description = "Unauthorized."), + (status = 500, description = "Internal server error."), + ) +)] +// TODO: pagination +/// Fetches all reading lists for the current user. +async fn get_reading_list( + State(ctx): State, + session: ReadableSession, +) -> ApiResult>> { + let user_id = get_session_user(&session)?.id; + + Ok(Json( + ctx.db + .reading_list() + .find_many(vec![reading_list::creating_user_id::equals(user_id)]) + .exec() + .await? + .into_iter() + .map(|u| u.into()) + .collect::>(), + )) +} + +#[utoipa::path( + post, + path = "/api/v1/reading-list", + tag = "reading-list", + request_body = CreateReadingList, + responses( + (status = 200, description = "Successfully created reading list.", body = ReadingList), + (status = 401, description = "Unauthorized."), + (status = 500, description = "Internal server error."), + ) +)] +async fn create_reading_list( + State(ctx): State, + session: ReadableSession, + Json(input): Json, +) -> ApiResult> { + let db = ctx.get_db(); + let user_id = get_session_user(&session)?.id; + + let created_reading_list = db + .reading_list() + .create( + input.id.to_owned(), + user::id::equals(user_id.clone()), + vec![reading_list::media::connect( + input + .media_ids + .iter() + .map(|id| media::id::equals(id.to_string())) + .collect(), + )], + ) + .exec() + .await?; + + Ok(Json(created_reading_list.into())) +} + +#[utoipa::path( + get, + path = "/api/v1/reading-list/:id", + tag = "reading-list", + params( + ("id" = String, Path, description = "The ID of the reading list to fetch.") + ), + responses( + (status = 200, description = "Successfully fetched reading list.", body = ReadingList), + (status = 401, description = "Unauthorized."), + (status = 404, description = "Reading list not found."), + (status = 500, description = "Internal server error."), + ) +)] +async fn get_reading_list_by_id( + Path(id): Path, + State(ctx): State, + session: ReadableSession, +) -> ApiResult> { + let _user_id = get_session_user(&session)?.id; + let db = ctx.get_db(); + + let reading_list = db + .reading_list() + .find_unique(reading_list::id::equals(id.clone())) + .exec() + .await?; + + if reading_list.is_none() { + return Err(ApiError::NotFound(format!( + "Reading List with id {} not found", + id + ))); + } + + // TODO: access control for reading lists... + Ok(Json(reading_list.unwrap().into())) +} + +// TODO: fix this endpoint, way too naive of an update... +#[utoipa::path( + put, + path = "/api/v1/reading-list/:id", + tag = "reading-list", + params( + ("id" = String, Path, description = "The ID of the reading list to update.") + ), + request_body = CreateReadingList, + responses( + (status = 200, description = "Successfully updated reading list.", body = ReadingList), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 404, description = "Reading list not found."), + (status = 500, description = "Internal server error."), + ) +)] +async fn update_reading_list( + session: ReadableSession, + Path(id): Path, + State(ctx): State, + Json(input): Json, +) -> ApiResult> { + let user = get_session_user(&session)?; + let db = ctx.get_db(); + + let reading_list = db + .reading_list() + .find_unique(reading_list::id::equals(id.clone())) + .exec() + .await?; + + if reading_list.is_none() { + return Err(ApiError::NotFound(format!( + "Reading List with id {} not found", + id + ))); + } + + let reading_list = reading_list.unwrap(); + if reading_list.creating_user_id != user.id { + // TODO: log bad access attempt to DB + return Err(ApiError::Forbidden(String::from( + "You do not have permission to access this resource.", + ))); + } + + let created_reading_list = db + .reading_list() + .update( + reading_list::id::equals(id.clone()), + vec![reading_list::media::connect( + input + .media_ids + .iter() + .map(|id| media::id::equals(id.to_string())) + .collect(), + )], + ) + .exec() + .await?; + + Ok(Json(created_reading_list.into())) +} + +#[utoipa::path( + delete, + path = "/api/v1/reading-list/:id", + tag = "reading-list", + params( + ("id" = String, Path, description = "The ID of the reading list to delete.") + ), + responses( + (status = 200, description = "Successfully deleted reading list.", body = ReadingList), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 404, description = "Reading list not found."), + (status = 500, description = "Internal server error."), + ) +)] +async fn delete_reading_list_by_id( + session: ReadableSession, + Path(id): Path, + State(ctx): State, +) -> ApiResult> { + let user = get_session_user(&session)?; + let db = ctx.get_db(); + + let reading_list = db + .reading_list() + .find_unique(reading_list::id::equals(id.clone())) + .exec() + .await?; + + if reading_list.is_none() { + return Err(ApiError::NotFound(format!( + "Reading List with id {} not found", + id + ))); + } + + let reading_list = reading_list.unwrap(); + if reading_list.creating_user_id != user.id { + // TODO: log bad access attempt to DB + return Err(ApiError::forbidden_discreet()); + } + + trace!("Attempting to delete reading list with ID {}", &id); + let deleted = db + .reading_list() + .delete(reading_list::id::equals(id.clone())) + .exec() + .await?; + + Ok(Json(deleted.into())) +} diff --git a/apps/server/src/routers/api/v1/series.rs b/apps/server/src/routers/api/v1/series.rs new file mode 100644 index 000000000..e9a47fb76 --- /dev/null +++ b/apps/server/src/routers/api/v1/series.rs @@ -0,0 +1,489 @@ +use axum::{ + extract::{Path, State}, + middleware::from_extractor_with_state, + routing::get, + Json, Router, +}; +use axum_extra::extract::Query; +use axum_sessions::extractors::ReadableSession; +use prisma_client_rust::Direction; +use stump_core::{ + db::{ + models::{Media, Series}, + utils::PrismaCountTrait, + Dao, SeriesDao, SeriesDaoImpl, + }, + fs::{image, media_file}, + prelude::{ + ContentType, PageQuery, Pageable, Pagination, PaginationQuery, QueryOrder, + }, + prisma::{ + media::{self, OrderByParam as MediaOrderByParam}, + read_progress, + series::{self, WhereParam}, + }, +}; +use tracing::{error, trace}; + +use crate::{ + config::state::AppState, + errors::{ApiError, ApiResult}, + middleware::auth::Auth, + utils::{ + get_session_user, http::ImageResponse, FilterableQuery, SeriesFilter, + SeriesRelation, + }, +}; + +use super::library::apply_library_filters; + +pub(crate) fn mount(app_state: AppState) -> Router { + Router::new() + .route("/series", get(get_series)) + .route("/series/recently-added", get(get_recently_added_series)) + .nest( + "/series/:id", + Router::new() + .route("/", get(get_series_by_id)) + .route("/media", get(get_series_media)) + .route("/media/next", get(get_next_in_series)) + .route("/thumbnail", get(get_series_thumbnail)), + ) + .layer(from_extractor_with_state::(app_state)) +} + +pub(crate) fn apply_series_filters(filters: SeriesFilter) -> Vec { + let mut _where: Vec = vec![]; + + if !filters.id.is_empty() { + _where.push(series::id::in_vec(filters.id)) + } + if !filters.name.is_empty() { + _where.push(series::name::in_vec(filters.name)); + } + + if let Some(library_filters) = filters.library { + _where.push(series::library::is(apply_library_filters(library_filters))); + } + + _where +} + +#[utoipa::path( + get, + path = "/api/v1/series", + tag = "series", + params( + ("filter_query" = Option, Query, description = "The filter options"), + ("pagination_query" = Option, Query, description = "The pagination options"), + ("relation_query" = Option, Query, description = "The relations to include"), + ), + responses( + (status = 200, description = "Successfully fetched series.", body = PageableSeries), + (status = 401, description = "Unauthorized."), + (status = 500, description = "Internal server error."), + ) +)] +/// Get all series accessible by user. +async fn get_series( + filter_query: Query>, + pagination_query: Query, + relation_query: Query, + State(ctx): State, + session: ReadableSession, +) -> ApiResult>>> { + let FilterableQuery { ordering, filters } = filter_query.0.get(); + let pagination = pagination_query.0.get(); + + let db = ctx.get_db(); + let user_id = get_session_user(&session)?.id; + + let is_unpaged = pagination.is_unpaged(); + let load_media = relation_query.load_media.unwrap_or(false); + let order_by = ordering.try_into()?; + + let where_conditions = apply_series_filters(filters); + let action = db.series(); + let action = action.find_many(where_conditions.clone()); + let mut query = if load_media { + action.with( + series::media::fetch(vec![]) + .with(media::read_progresses::fetch(vec![ + read_progress::user_id::equals(user_id), + ])) + .order_by(order_by), + ) + } else { + action + }; + + if !is_unpaged { + match pagination.clone() { + Pagination::Page(page_query) => { + let (skip, take) = page_query.get_skip_take(); + query = query.skip(skip).take(take); + }, + Pagination::Cursor(cursor_query) => { + if let Some(cursor) = cursor_query.cursor { + query = query.cursor(series::id::equals(cursor)).skip(1) + } + if let Some(limit) = cursor_query.limit { + query = query.take(limit) + } + }, + _ => unreachable!(), + } + } + + let series = query + .exec() + .await? + .into_iter() + .map(|s| s.into()) + .collect::>(); + + if is_unpaged { + return Ok(Json(series.into())); + } + + let series_count = db.series().count(where_conditions).exec().await?; + + Ok(Json((series, series_count, pagination).into())) +} + +#[utoipa::path( + get, + path = "/api/v1/series/:id", + tag = "series", + params( + ("id" = String, Path, description = "The ID of the series to fetch"), + ("relation_query" = Option, Query, description = "The relations to include"), + ), + responses( + (status = 200, description = "Successfully fetched series.", body = Series), + (status = 401, description = "Unauthorized."), + (status = 404, description = "Series not found."), + (status = 500, description = "Internal server error."), + ) +)] +/// Get a series by ID. Optional query param `load_media` that will load the media +/// relation (i.e. the media entities will be loaded and sent with the response) +async fn get_series_by_id( + query: Query, + Path(id): Path, + State(ctx): State, + session: ReadableSession, +) -> ApiResult> { + let db = ctx.get_db(); + let user_id = get_session_user(&session)?.id; + + let load_media = query.load_media.unwrap_or(false); + let mut query = db.series().find_unique(series::id::equals(id.clone())); + + if load_media { + query = query.with( + series::media::fetch(vec![]) + .with(media::read_progresses::fetch(vec![ + read_progress::user_id::equals(user_id), + ])) + .order_by(media::name::order(Direction::Asc)), + ); + } + + let series = query.exec().await?; + + if series.is_none() { + return Err(ApiError::NotFound(format!( + "Series with id {} not found", + id + ))); + } + + if !load_media { + // FIXME: PCR doesn't support relation counts yet! + // let media_count = db + // .media() + // .count(vec![media::series_id::equals(Some(id.clone()))]) + // .exec() + // .await?; + let series_media_count = db.media_in_series_count(id).await?; + + return Ok(Json((series.unwrap(), series_media_count).into())); + } + + Ok(Json(series.unwrap().into())) +} + +#[utoipa::path( + get, + path = "/api/v1/series/recently-added", + tag = "series", + params( + ("pagination" = PageQuery, Query, description = "The pagination params"), + ), + responses( + (status = 200, description = "Successfully fetched recently added series.", body = PageableSeries), + (status = 400, description = "Bad request. Unpaged request not supported for this endpoint."), + (status = 401, description = "Unauthorized."), + (status = 500, description = "Internal server error."), + ) +)] +async fn get_recently_added_series( + State(ctx): State, + pagination: Query, + session: ReadableSession, +) -> ApiResult>>> { + if pagination.page.is_none() { + return Err(ApiError::BadRequest( + "Unpaged request not supported for this endpoint".to_string(), + )); + } + + let viewer_id = get_session_user(&session)?.id; + let page_params = pagination.0.page_params(); + let series_dao = SeriesDaoImpl::new(ctx.db.clone()); + + let recently_added_series = series_dao + .get_recently_added_series_page(&viewer_id, page_params) + .await?; + + Ok(Json(recently_added_series)) +} + +// TODO: ImageResponse type for body +#[utoipa::path( + get, + path = "/api/v1/series/:id/thumbnail", + tag = "series", + params( + ("id" = String, Path, description = "The ID of the series to fetch the thumbnail for"), + ), + responses( + (status = 200, description = "Successfully fetched series thumbnail."), + (status = 401, description = "Unauthorized."), + (status = 404, description = "Series not found."), + (status = 500, description = "Internal server error."), + ) +)] +/// Returns the thumbnail image for a series +async fn get_series_thumbnail( + Path(id): Path, + State(ctx): State, +) -> ApiResult { + let db = ctx.get_db(); + + let media = db + .media() + .find_first(vec![media::series_id::equals(Some(id.clone()))]) + .order_by(media::name::order(Direction::Asc)) + .exec() + .await?; + + if media.is_none() { + return Err(ApiError::NotFound(format!( + "Series with id {} not found", + id + ))); + } + + let media = media.unwrap(); + if let Some(webp_path) = image::get_thumbnail_path(&media.id) { + trace!("Found webp thumbnail for series {}", &id); + return Ok((ContentType::WEBP, image::get_bytes(webp_path)?).into()); + } + + Ok(media_file::get_page(media.path.as_str(), 1)?.into()) +} + +#[utoipa::path( + get, + path = "/api/v1/series/:id/media", + tag = "series", + params( + ("id" = String, Path, description = "The ID of the series to fetch the media for"), + ("pagination" = Option, Query, description = "The pagination params"), + ("ordering" = Option, Query, description = "The ordering params"), + ), + responses( + (status = 200, description = "Successfully fetched series media.", body = PageableMedia), + (status = 401, description = "Unauthorized."), + (status = 404, description = "Series not found."), + (status = 500, description = "Internal server error."), + ) +)] +/// Returns the media in a given series. +async fn get_series_media( + pagination_query: Query, + ordering: Query, + session: ReadableSession, + Path(id): Path, + State(ctx): State, +) -> ApiResult>>> { + let db = ctx.get_db(); + let user_id = get_session_user(&session)?.id; + + let series_exists = db + .series() + .find_first(vec![series::id::equals(id.clone())]) + .exec() + .await? + .is_some(); + + if !series_exists { + return Err(ApiError::NotFound(format!( + "Series with id {} not found", + id + ))); + } + + let pagination = pagination_query.0.get(); + let is_unpaged = pagination.is_unpaged(); + let order_by_param: MediaOrderByParam = ordering.0.try_into()?; + + let pagination_cloned = pagination.clone(); + let (media, count) = db + ._transaction() + .run(|client| async move { + let mut query = client + .media() + .find_many(vec![media::series_id::equals(Some(id.clone()))]) + .with(media::read_progresses::fetch(vec![ + read_progress::user_id::equals(user_id), + ])) + .order_by(order_by_param); + + if !is_unpaged { + match pagination_cloned { + Pagination::Page(page_query) => { + let (skip, take) = page_query.get_skip_take(); + query = query.skip(skip).take(take); + }, + Pagination::Cursor(cursor_query) => { + if let Some(cursor) = cursor_query.cursor { + query = query.cursor(media::id::equals(cursor)).skip(1) + } + if let Some(limit) = cursor_query.limit { + query = query.take(limit) + } + }, + _ => unreachable!(), + } + } + + let media = query + .exec() + .await? + .into_iter() + .map(|m| m.into()) + .collect::>(); + + if is_unpaged { + return Ok((media, None)); + } + + // FIXME: PCR doesn't support relation counts yet! + // client + // .media() + // .count(where_conditions) + // .exec() + // .await + // .map(|count| (media, Some(count))) + client + .media_in_series_count(id) + .await + .map(|count| (media, Some(count))) + }) + .await?; + + if let Some(count) = count { + return Ok(Json(Pageable::from((media, count, pagination)))); + } + + Ok(Json(Pageable::from(media))) +} + +#[utoipa::path( + get, + path = "/api/v1/series/:id/media/next", + tag = "series", + params( + ("id" = String, Path, description = "The ID of the series to fetch the up-next media for"), + ), + responses( + (status = 200, description = "Successfully fetched media up-next in series", body = Option), + (status = 401, description = "Unauthorized."), + (status = 404, description = "Series not found."), + (status = 500, description = "Internal server error."), + ) +)] +// TODO: Should I support epub here too?? Not sure, I have separate routes for epub, +// but until I actually implement progress tracking for epub I think think I can really +// give a hard answer on what is best... +/// Get the next media in a series, based on the read progress for the requesting user. +/// Stump will return the first book in the series without progress, or return the first +/// with partial progress. E.g. if a user has read pages 32/32 of book 3, then book 4 is +/// next. If a user has read pages 31/32 of book 4, then book 4 is still next. +async fn get_next_in_series( + Path(id): Path, + State(ctx): State, + session: ReadableSession, +) -> ApiResult>> { + let db = ctx.get_db(); + let user_id = get_session_user(&session)?.id; + + let series = db + .series() + .find_unique(series::id::equals(id.clone())) + .with( + series::media::fetch(vec![]) + .with(media::read_progresses::fetch(vec![ + read_progress::user_id::equals(user_id), + ])) + .order_by(media::name::order(Direction::Asc)), + ) + .exec() + .await?; + + if series.is_none() { + return Err(ApiError::NotFound(format!( + "Series with id {} no found.", + id + ))); + } + + let series = series.unwrap(); + + let media = series.media().map_err(|e| { + error!(error = ?e, "Failed to load media for series"); + e + })?; + + Ok(Json( + media + .iter() + .find(|m| { + // I don't really know that this is valid... When I load in the + // relation, this will NEVER be None. It will default to an empty + // vector. But, for safety I guess I will leave this for now. + if m.read_progresses.is_none() { + return true; + } + + let progresses = m.read_progresses.as_ref().unwrap(); + + // No progress means it is up next (for this user)! + if progresses.is_empty() { + true + } else { + // Note: this should never really exceed len == 1, but :shrug: + let progress = progresses.get(0).unwrap(); + + progress.page < m.pages && progress.page > 0 + } + }) + .or_else(|| media.get(0)) + .map(|data| data.to_owned().into()), + )) +} + +// async fn download_series() diff --git a/apps/server/src/routers/api/v1/tag.rs b/apps/server/src/routers/api/v1/tag.rs new file mode 100644 index 000000000..f80c87d75 --- /dev/null +++ b/apps/server/src/routers/api/v1/tag.rs @@ -0,0 +1,92 @@ +use axum::{ + extract::State, middleware::from_extractor_with_state, routing::get, Json, Router, +}; +use stump_core::{db::models::Tag, prelude::CreateTags, prisma::tag}; +use tracing::error; + +use crate::{ + config::state::AppState, + errors::{ApiError, ApiResult}, + middleware::auth::Auth, +}; + +pub(crate) fn mount(app_state: AppState) -> Router { + Router::new() + .route("/tags", get(get_tags).post(create_tags)) + .layer(from_extractor_with_state::(app_state)) +} + +#[utoipa::path( + get, + path = "/api/v1/tags", + tag = "tag", + responses( + (status = 200, description = "Successfully fetched tags.", body = [Tag]), + (status = 401, description = "Unauthorized."), + (status = 500, description = "Internal server error."), + ) +)] +/// Get all tags for all items in the database. Tags are returned in a flat list, +/// not grouped by the items which they belong to. +async fn get_tags(State(ctx): State) -> ApiResult>> { + let db = ctx.get_db(); + + Ok(Json( + db.tag() + .find_many(vec![]) + .exec() + .await? + .into_iter() + .map(Tag::from) + .collect(), + )) +} + +#[utoipa::path( + post, + path = "/api/v1/tags", + tag = "tag", + request_body = CreateTags, + responses( + (status = 200, description = "Successfully created tags.", body = [Tag]), + (status = 401, description = "Unauthorized."), + (status = 500, description = "Internal server error."), + ) +)] +/// Create new tags. If any of the tags already exist, an error is returned. +async fn create_tags( + State(ctx): State, + Json(input): Json, +) -> ApiResult>> { + let db = ctx.get_db(); + + let already_existing_tags = db + .tag() + .find_many(vec![tag::name::in_vec(input.tags.clone())]) + .exec() + .await?; + + if !already_existing_tags.is_empty() { + error!(existing_tags = ?already_existing_tags, "Tags already exist"); + + let existing_names = already_existing_tags + .into_iter() + .map(|t| t.name) + .collect::>() + .join(", "); + + return Err(ApiError::BadRequest(format!( + "Attempted to create tags which already exist: {}", + existing_names + ))); + } + + let create_tags = input + .tags + .into_iter() + .map(|value| db.tag().create(value, vec![])) + .collect::>(); + let created_tags = db._batch(create_tags).await?; + + Ok(Json(created_tags.into_iter().map(Tag::from).collect())) +} diff --git a/apps/server/src/routers/api/v1/user.rs b/apps/server/src/routers/api/v1/user.rs new file mode 100644 index 000000000..a56b0ae8a --- /dev/null +++ b/apps/server/src/routers/api/v1/user.rs @@ -0,0 +1,368 @@ +use axum::{ + extract::{Path, State}, + middleware::from_extractor_with_state, + routing::get, + Json, Router, +}; +use axum_sessions::extractors::{ReadableSession, WritableSession}; +use stump_core::{ + db::models::{User, UserPreferences}, + prelude::{LoginOrRegisterArgs, UpdateUserArgs, UserPreferencesUpdate}, + prisma::{user, user_preferences}, +}; +use tracing::{debug, trace}; + +use crate::{ + config::state::AppState, + errors::{ApiError, ApiResult}, + middleware::auth::Auth, + utils::{ + get_hash_cost, get_session_admin_user, get_session_user, + get_writable_session_user, + }, +}; + +// TODO: move some of these user operations to the UserDao... + +pub(crate) fn mount(app_state: AppState) -> Router { + Router::new() + // TODO: adminguard these first two routes + .route("/users", get(get_users).post(create_user)) + .nest( + "/users/:id", + Router::new() + .route( + "/", + get(get_user_by_id) + .put(update_user) + .delete(delete_user_by_id), + ) + .route( + "/preferences", + get(get_user_preferences).put(update_user_preferences), + ), + ) + .layer(from_extractor_with_state::(app_state)) +} + +#[utoipa::path( + get, + path = "/api/v1/users", + tag = "user", + responses( + (status = 200, description = "Successfully fetched users.", body = [User]), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 500, description = "Internal server error."), + ) +)] +async fn get_users( + State(ctx): State, + session: ReadableSession, +) -> ApiResult>> { + get_session_admin_user(&session)?; + Ok(Json( + ctx.db + .user() + .find_many(vec![]) + .exec() + .await? + .into_iter() + .map(User::from) + .collect::>(), + )) +} + +#[utoipa::path( + post, + path = "/api/v1/users", + tag = "user", + request_body = LoginOrRegisterArgs, + responses( + (status = 200, description = "Successfully created user.", body = User), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 500, description = "Internal server error."), + ) +)] +/// Creates a new user. +async fn create_user( + session: ReadableSession, + State(ctx): State, + Json(input): Json, +) -> ApiResult> { + get_session_admin_user(&session)?; + let db = ctx.get_db(); + let hashed_password = bcrypt::hash(input.password, get_hash_cost())?; + let created_user = db + .user() + .create(input.username.to_owned(), hashed_password, vec![]) + .exec() + .await?; + + // FIXME: these next two queries will be removed once nested create statements are + // supported on the prisma client. Until then, this ugly mess is necessary. + // https://github.com/Brendonovich/prisma-client-rust/issues/44 + let _user_preferences = db + .user_preferences() + .create(vec![user_preferences::user::connect(user::id::equals( + created_user.id.clone(), + ))]) + .exec() + .await?; + + // This *really* shouldn't fail, so I am using unwrap here. It also doesn't + // matter too much in the long run since this query will go away once above fixme + // is resolved. + let user = db + .user() + .find_unique(user::id::equals(created_user.id)) + .with(user::user_preferences::fetch()) + .exec() + .await? + .unwrap(); + + Ok(Json(user.into())) +} + +#[utoipa::path( + delete, + path = "/api/v1/users/:id", + tag = "user", + params( + ("id" = String, Path, description = "The user's id.", example = "1ab2c3d4") + ), + responses( + (status = 200, description = "Successfully deleted user.", body = String), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 404, description = "User not found."), + (status = 500, description = "Internal server error."), + ) +)] +/// Deletes a user by ID. +async fn delete_user_by_id( + Path(id): Path, + State(ctx): State, + session: ReadableSession, +) -> ApiResult> { + let db = ctx.get_db(); + let user = get_session_admin_user(&session)?; + + if user.id == id { + return Err(ApiError::BadRequest( + "You cannot delete your own account.".into(), + )); + } + + let deleted_user = db + .user() + .delete(user::id::equals(id.clone())) + .exec() + .await?; + + debug!(?deleted_user, "Deleted user"); + + Ok(Json(deleted_user.id)) +} + +#[utoipa::path( + get, + path = "/api/v1/users/:id", + tag = "user", + params( + ("id" = String, Path, description = "The user's ID.", example = "1ab2c3d4") + ), + responses( + (status = 200, description = "Successfully fetched user.", body = User), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 404, description = "User not found."), + (status = 500, description = "Internal server error."), + ) +)] +/// Gets a user by ID. +async fn get_user_by_id( + Path(id): Path, + State(ctx): State, + session: ReadableSession, +) -> ApiResult> { + get_session_admin_user(&session)?; + let db = ctx.get_db(); + let user_by_id = db + .user() + .find_unique(user::id::equals(id.clone())) + .exec() + .await?; + debug!(id, ?user_by_id, "Result of fetching user by id"); + + if user_by_id.is_none() { + return Err(ApiError::NotFound(format!("User with id {} not found", id))); + } + + Ok(Json(User::from(user_by_id.unwrap()))) +} + +#[utoipa::path( + put, + path = "/api/v1/users/:id", + tag = "user", + params( + ("id" = String, Path, description = "The user's ID.", example = "1ab2c3d4") + ), + request_body = UpdateUserArgs, + responses( + (status = 200, description = "Successfully updated user.", body = User), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 500, description = "Internal server error."), + ) +)] +/// Updates a user by ID. +async fn update_user( + mut writable_session: WritableSession, + State(ctx): State, + Path(id): Path, + Json(input): Json, +) -> ApiResult> { + let db = ctx.get_db(); + let user = get_writable_session_user(&writable_session)?; + + if user.id != id { + return Err(ApiError::forbidden_discreet()); + } + + let mut update_params = vec![user::username::set(input.username)]; + if let Some(password) = input.password { + let hashed_password = bcrypt::hash(password, get_hash_cost())?; + update_params.push(user::hashed_password::set(hashed_password)); + } + + let updated_user_data = db + .user() + .update(user::id::equals(user.id.clone()), update_params) + .exec() + .await?; + let updated_user = User::from(updated_user_data); + debug!(?updated_user, "Updated user"); + + writable_session + .insert("user", updated_user.clone()) + .map_err(|e| { + ApiError::InternalServerError(format!("Failed to update session: {}", e)) + })?; + + Ok(Json(updated_user)) +} + +#[utoipa::path( + get, + path = "/api/v1/users/:id/preferences", + tag = "user", + params( + ("id" = String, Path, description = "The user's ID.", example = "1ab2c3d4") + ), + responses( + (status = 200, description = "Successfully fetched user preferences.", body = UserPreferences), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 404, description = "User preferences not found."), + (status = 500, description = "Internal server error."), + ) +)] +/// Gets the user's preferences. +async fn get_user_preferences( + Path(id): Path, + State(ctx): State, + session: ReadableSession, +) -> ApiResult> { + let db = ctx.get_db(); + let user = get_session_user(&session)?; + + if id != user.id { + return Err(ApiError::forbidden_discreet()); + } + + let user_preferences = db + .user_preferences() + .find_unique(user_preferences::id::equals(id.clone())) + .exec() + .await?; + debug!(id, ?user_preferences, "Fetched user preferences"); + + if user_preferences.is_none() { + return Err(ApiError::NotFound(format!( + "User preferences with id {} not found", + id + ))); + } + + Ok(Json(UserPreferences::from(user_preferences.unwrap()))) +} + +#[utoipa::path( + put, + path = "/api/v1/users/:id/preferences", + tag = "user", + params( + ("id" = String, Path, description = "The user's ID.", example = "1ab2c3d4") + ), + request_body = UserPreferencesUpdate, + responses( + (status = 200, description = "Successfully updated user preferences.", body = UserPreferences), + (status = 401, description = "Unauthorized."), + (status = 403, description = "Forbidden."), + (status = 500, description = "Internal server error."), + ) +)] +/// Updates a user's preferences. +async fn update_user_preferences( + mut writable_session: WritableSession, + State(ctx): State, + Path(id): Path, + Json(input): Json, +) -> ApiResult> { + trace!(?id, ?input, "Updating user preferences"); + let db = ctx.get_db(); + + let user = get_writable_session_user(&writable_session)?; + let user_preferences = user.user_preferences.clone().unwrap_or_default(); + + if user_preferences.id != input.id { + return Err(ApiError::forbidden_discreet()); + } + + let updated_preferences = db + .user_preferences() + .update( + user_preferences::id::equals(user_preferences.id.clone()), + vec![ + user_preferences::locale::set(input.locale.to_owned()), + user_preferences::library_layout_mode::set( + input.library_layout_mode.to_owned(), + ), + user_preferences::series_layout_mode::set( + input.series_layout_mode.to_owned(), + ), + ], + ) + .exec() + .await?; + debug!(?updated_preferences, "Updated user preferences"); + + writable_session + .insert( + "user", + User { + user_preferences: Some(UserPreferences::from( + updated_preferences.clone(), + )), + ..user + }, + ) + .map_err(|e| { + ApiError::InternalServerError(format!("Failed to update session: {}", e)) + })?; + + Ok(Json(UserPreferences::from(updated_preferences))) +} diff --git a/apps/server/src/routers/mod.rs b/apps/server/src/routers/mod.rs index cc3b3077e..c1bc0f7bd 100644 --- a/apps/server/src/routers/mod.rs +++ b/apps/server/src/routers/mod.rs @@ -1,16 +1,29 @@ +use std::env; + use axum::Router; +use crate::config::{state::AppState, utils::is_debug}; + mod api; mod opds; mod spa; mod sse; +mod utoipa; mod ws; -pub(crate) fn mount() -> Router { - Router::new() +pub(crate) fn mount(app_state: AppState) -> Router { + let mut app_router = Router::new(); + + let enable_swagger = + env::var("ENABLE_SWAGGER_UI").unwrap_or_else(|_| String::from("true")); + if enable_swagger != "false" || is_debug() { + app_router = app_router.merge(utoipa::swagger_ui()); + } + + app_router .merge(spa::mount()) .merge(ws::mount()) .merge(sse::mount()) - .merge(api::mount()) - .merge(opds::mount()) + .merge(api::mount(app_state.clone())) + .merge(opds::mount(app_state)) } diff --git a/apps/server/src/routers/opds.rs b/apps/server/src/routers/opds.rs index 35e4d0247..2703e0016 100644 --- a/apps/server/src/routers/opds.rs +++ b/apps/server/src/routers/opds.rs @@ -1,8 +1,8 @@ use axum::{ - extract::{Path, Query}, - middleware::from_extractor, + extract::{Path, Query, State}, + middleware::from_extractor_with_state, routing::get, - Extension, Router, + Router, }; use axum_sessions::extractors::ReadableSession; use prisma_client_rust::{chrono, Direction}; @@ -14,12 +14,12 @@ use stump_core::{ feed::OpdsFeed, link::{OpdsLink, OpdsLinkRel, OpdsLinkType}, }, + prelude::PageQuery, prisma::{library, media, read_progress, series}, - types::PagedRequestParams, }; use crate::{ - config::state::State, + config::state::AppState, errors::{ApiError, ApiResult}, middleware::auth::Auth, utils::{ @@ -28,7 +28,7 @@ use crate::{ }, }; -pub(crate) fn mount() -> Router { +pub(crate) fn mount(app_state: AppState) -> Router { Router::new() .nest( "/opds/v1.2", @@ -55,7 +55,7 @@ pub(crate) fn mount() -> Router { .route("/pages/:page", get(get_book_page)), ), ) - .layer(from_extractor::()) + .layer(from_extractor_with_state::(app_state)) } fn pagination_bounds(page: i64, page_size: i64) -> (i64, i64) { @@ -179,7 +179,10 @@ async fn catalog() -> ApiResult { Ok(Xml(feed.build()?)) } -async fn keep_reading(Extension(ctx): State, session: ReadableSession) -> ApiResult { +async fn keep_reading( + State(ctx): State, + session: ReadableSession, +) -> ApiResult { let db = ctx.get_db(); let user_id = get_session_user(&session)?.id; @@ -246,7 +249,7 @@ async fn keep_reading(Extension(ctx): State, session: ReadableSession) -> ApiRes Ok(Xml(feed.build()?)) } -async fn get_libraries(Extension(ctx): State) -> ApiResult { +async fn get_libraries(State(ctx): State) -> ApiResult { let db = ctx.get_db(); let libraries = db.library().find_many(vec![]).exec().await?; @@ -275,9 +278,9 @@ async fn get_libraries(Extension(ctx): State) -> ApiResult { } async fn get_library_by_id( - Extension(ctx): State, + State(ctx): State, Path(id): Path, - pagination: Query, + pagination: Query, ) -> ApiResult { let db = ctx.get_db(); @@ -318,8 +321,8 @@ async fn get_library_by_id( // /// A handler for GET /opds/v1.2/series, accepts a `page` URL param. Note: OPDS // /// pagination is zero-indexed. async fn get_series( - pagination: Query, - Extension(ctx): State, + pagination: Query, + State(ctx): State, ) -> ApiResult { let db = ctx.get_db(); @@ -352,8 +355,8 @@ async fn get_series( } async fn get_latest_series( - pagination: Query, - Extension(ctx): State, + pagination: Query, + State(ctx): State, ) -> ApiResult { let db = ctx.get_db(); @@ -384,8 +387,8 @@ async fn get_latest_series( async fn get_series_by_id( Path(id): Path, - pagination: Query, - Extension(ctx): State, + pagination: Query, + State(ctx): State, ) -> ApiResult { let db = ctx.get_db(); @@ -432,7 +435,7 @@ async fn get_series_by_id( async fn get_book_thumbnail( Path(id): Path, - Extension(ctx): State, + State(ctx): State, ) -> ApiResult { let db = ctx.get_db(); @@ -453,8 +456,8 @@ async fn get_book_thumbnail( async fn get_book_page( Path((id, page)): Path<(String, i32)>, - Extension(ctx): State, - pagination: Query, + State(ctx): State, + pagination: Query, ) -> ApiResult { let db = ctx.get_db(); @@ -479,7 +482,7 @@ async fn get_book_page( let book = book.unwrap(); if book.path.ends_with(".epub") && correct_page == 1 { - return Ok(epub::get_epub_cover(&book.path)?.into()); + return Ok(epub::get_cover(&book.path)?.into()); } Ok(media_file::get_page(book.path.as_str(), correct_page)?.into()) diff --git a/apps/server/src/routers/spa.rs b/apps/server/src/routers/spa.rs index 26d34a4d2..9241f2119 100644 --- a/apps/server/src/routers/spa.rs +++ b/apps/server/src/routers/spa.rs @@ -2,11 +2,11 @@ use std::path::Path; use axum_extra::routing::SpaRouter; -use crate::config::utils::get_client_dir; +use crate::config::{state::AppState, utils::get_client_dir}; // FIXME: I am not picking up the favicon.ico file in docker, but can't seem // to replicate it locally... -pub(crate) fn mount() -> SpaRouter { +pub(crate) fn mount() -> SpaRouter { let dist = get_client_dir(); let dist_path = Path::new(&dist); diff --git a/apps/server/src/routers/sse.rs b/apps/server/src/routers/sse.rs index 210c4db1d..eb872884e 100644 --- a/apps/server/src/routers/sse.rs +++ b/apps/server/src/routers/sse.rs @@ -1,22 +1,23 @@ use std::convert::Infallible; use axum::{ + extract::State, response::sse::{Event, Sse}, routing::get, - Extension, Router, + Router, }; use futures_util::{stream::Stream, StreamExt}; -use crate::{config::state::State, utils::shutdown_signal}; +use crate::{config::state::AppState, utils::shutdown_signal}; // TODO: do I need auth middleware here? I think so. -pub(crate) fn mount() -> Router { +pub(crate) fn mount() -> Router { Router::new().route("/sse", get(sse_handler)) - // .layer(from_extractor::()) + // .layer(from_extractor_with_state::(app_state)) } async fn sse_handler( - Extension(ctx): State, + State(ctx): State, ) -> Sse>> { let mut rx = ctx.get_client_receiver(); diff --git a/apps/server/src/routers/utoipa.rs b/apps/server/src/routers/utoipa.rs new file mode 100644 index 000000000..406a14440 --- /dev/null +++ b/apps/server/src/routers/utoipa.rs @@ -0,0 +1,115 @@ +use stump_core::db::models::{ + LibrariesStats, Library, LibraryOptions, LibraryPattern, LibraryScanMode, LogLevel, + Media, ReadProgress, ReadingList, Series, Tag, User, UserPreferences, +}; +use stump_core::job::{JobReport, JobStatus}; +use stump_core::prelude::{ + ClaimResponse, CreateLibraryArgs, CreateReadingList, CreateTags, CursorInfo, + Direction, DirectoryListing, DirectoryListingFile, DirectoryListingInput, FileStatus, + LoginOrRegisterArgs, PageInfo, PageQuery, PageableDirectoryListing, + PageableLibraries, PageableMedia, PageableSeries, PaginationQuery, QueryOrder, + ScanQueryParam, StumpVersion, UpdateLibraryArgs, UpdateUserArgs, + UserPreferencesUpdate, +}; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +use crate::errors::ApiError; +use crate::utils::{ + FilterableLibraryQuery, FilterableMediaQuery, FilterableSeriesQuery, LibraryFilter, + MediaFilter, SeriesFilter, SeriesRelation, +}; + +use super::api; + +// NOTE: it is very easy to indirectly cause fmt failures by not adhering to the +// rustfmt rules, since cargo fmt will not format the code in the macro. +#[derive(OpenApi)] +#[openapi( + paths( + api::v1::claim, + api::v1::ping, + api::v1::version, + api::v1::auth::viewer, + api::v1::auth::login, + api::v1::auth::logout, + api::v1::auth::register, + // TODO: epub here + api::v1::filesystem::list_directory, + api::v1::job::get_job_reports, + api::v1::job::delete_job_reports, + api::v1::job::cancel_job, + api::v1::library::get_libraries, + api::v1::library::get_libraries_stats, + api::v1::library::get_library_by_id, + api::v1::library::get_library_series, + api::v1::library::get_library_thumbnail, + api::v1::library::scan_library, + api::v1::library::create_library, + api::v1::library::update_library, + api::v1::library::delete_library, + api::v1::media::get_media, + api::v1::media::get_duplicate_media, + api::v1::media::get_in_progress_media, + api::v1::media::get_recently_added_media, + api::v1::media::get_media_by_id, + api::v1::media::get_media_file, + api::v1::media::convert_media, + api::v1::media::get_media_page, + api::v1::media::get_media_thumbnail, + api::v1::media::update_media_progress, + api::v1::reading_list::get_reading_list, + api::v1::reading_list::create_reading_list, + api::v1::reading_list::get_reading_list_by_id, + api::v1::reading_list::update_reading_list, + api::v1::reading_list::delete_reading_list_by_id, + api::v1::series::get_series, + api::v1::series::get_series_by_id, + api::v1::series::get_recently_added_series, + api::v1::series::get_series_thumbnail, + api::v1::series::get_series_media, + api::v1::tag::get_tags, + api::v1::tag::create_tags, + api::v1::series::get_next_in_series, + api::v1::user::get_users, + api::v1::user::create_user, + api::v1::user::delete_user_by_id, + api::v1::user::get_user_by_id, + api::v1::user::update_user, + api::v1::user::get_user_preferences, + api::v1::user::update_user_preferences, + ), + components( + schemas( + Library, LibraryOptions, Media, ReadingList, ReadProgress, Series, Tag, User, + UserPreferences, LibraryPattern, LibraryScanMode, LogLevel, ClaimResponse, + StumpVersion, FileStatus, PageableDirectoryListing, DirectoryListing, + DirectoryListingFile, CursorInfo, PageInfo, PageableLibraries, + PageableMedia, PageableSeries, LoginOrRegisterArgs, DirectoryListingInput, + PageQuery, FilterableLibraryQuery, PaginationQuery, QueryOrder, LibraryFilter, + Direction, CreateLibraryArgs, UpdateLibraryArgs, ApiError, MediaFilter, SeriesFilter, + FilterableMediaQuery, FilterableSeriesQuery, JobReport, LibrariesStats, ScanQueryParam, + JobStatus, SeriesRelation, CreateReadingList, UserPreferencesUpdate, UpdateUserArgs, + CreateTags + ) + ), + tags( + (name = "util", description = "Utility API"), + (name = "auth", description = "Authentication API"), + (name = "epub", description = "EPUB API"), + (name = "filesystem", description = "Filesystem API"), + (name = "job", description = "Job API"), + (name = "library", description = "Library API"), + (name = "media", description = "Media API"), + (name = "series", description = "Series API"), + (name = "tag", description = "Tag API"), + (name = "reading-list", description = "Reading List API"), + (name = "user", description = "User API"), + (name = "opds", description = "OPDS API"), + ) +)] +struct ApiDoc; + +pub(crate) fn swagger_ui() -> SwaggerUi { + SwaggerUi::new("/swagger-ui").url("/api-doc/openapi.json", ApiDoc::openapi()) +} diff --git a/apps/server/src/routers/ws.rs b/apps/server/src/routers/ws.rs index 91ae52175..401a848ec 100644 --- a/apps/server/src/routers/ws.rs +++ b/apps/server/src/routers/ws.rs @@ -1,26 +1,32 @@ use std::sync::Arc; use axum::{ - extract::ws::{Message, WebSocket, WebSocketUpgrade}, + extract::{ + ws::{Message, WebSocket, WebSocketUpgrade}, + State, + }, response::IntoResponse, routing::get, - Extension, Router, + Router, }; // use axum_typed_websockets::{Message, WebSocket, WebSocketUpgrade}; use futures_util::{sink::SinkExt, stream::StreamExt}; -use stump_core::config::Ctx; +use stump_core::prelude::Ctx; use tracing::error; -use crate::config::state::State; +use crate::config::state::AppState; // TODO: do I need auth middleware here? I think so, but I think the ws:// is // throwing if off and making it not think there is a session when there is. -pub(crate) fn mount() -> Router { +pub(crate) fn mount() -> Router { Router::new().route("/ws", get(ws_handler)) - // .layer(from_extractor::()) + // .layer(from_extractor_with_state::(app_state)) } -async fn ws_handler(ws: WebSocketUpgrade, Extension(ctx): State) -> impl IntoResponse { +async fn ws_handler( + ws: WebSocketUpgrade, + State(ctx): State, +) -> impl IntoResponse { ws.on_upgrade(|socket| handle_socket(socket, ctx)) } diff --git a/apps/server/src/utils/auth.rs b/apps/server/src/utils/auth.rs index f40847116..ff8c1f264 100644 --- a/apps/server/src/utils/auth.rs +++ b/apps/server/src/utils/auth.rs @@ -1,5 +1,5 @@ -use axum_sessions::extractors::ReadableSession; -use stump_core::types::{DecodedCredentials, User}; +use axum_sessions::extractors::{ReadableSession, WritableSession}; +use stump_core::{db::models::User, prelude::DecodedCredentials}; use crate::errors::{ApiError, ApiResult, AuthError}; @@ -37,6 +37,26 @@ pub fn get_session_user(session: &ReadableSession) -> ApiResult { } } +pub fn get_writable_session_user(session: &WritableSession) -> ApiResult { + if let Some(user) = session.get::("user") { + Ok(user) + } else { + Err(ApiError::Unauthorized) + } +} + +// pub fn get_writable_session_admin_user(session: &WritableSession) -> ApiResult { +// let user = get_writable_session_user(session)?; + +// if user.is_admin() { +// Ok(user) +// } else { +// Err(ApiError::Forbidden( +// "You do not have permission to access this resource.".to_string(), +// )) +// } +// } + pub fn get_session_admin_user(session: &ReadableSession) -> ApiResult { let user = get_session_user(session)?; diff --git a/apps/server/src/utils/filter.rs b/apps/server/src/utils/filter.rs new file mode 100644 index 000000000..d1d410880 --- /dev/null +++ b/apps/server/src/utils/filter.rs @@ -0,0 +1,105 @@ +use std::marker::PhantomData; + +use serde::{de, Deserialize, Deserializer, Serialize}; +use serde_with::with_prefix; +use std::fmt; +use stump_core::prelude::QueryOrder; +use utoipa::ToSchema; + +#[derive(Debug, Default, Deserialize, Serialize, ToSchema)] +#[aliases(FilterableLibraryQuery = FilterableQuery, FilterableSeriesQuery = FilterableQuery, FilterableMediaQuery = FilterableQuery)] +pub struct FilterableQuery +where + T: Sized + Default, +{ + #[serde(flatten, default)] + pub filters: T, + // #[serde(flatten)] + // pub pagination: PaginationQuery, + #[serde(flatten)] + pub ordering: QueryOrder, +} + +impl FilterableQuery +where + T: Sized + Default, +{ + pub fn get(self) -> Self { + self + } +} + +fn string_or_seq_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + struct StringOrVec(PhantomData>); + + impl<'de> de::Visitor<'de> for StringOrVec { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string or list of strings") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(vec![value.to_owned()]) + } + + fn visit_seq(self, visitor: S) -> Result + where + S: de::SeqAccess<'de>, + { + Deserialize::deserialize(de::value::SeqAccessDeserializer::new(visitor)) + } + } + + deserializer.deserialize_any(StringOrVec(PhantomData)) +} + +// TODO: tags +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct LibraryFilter { + #[serde(default, deserialize_with = "string_or_seq_string")] + pub id: Vec, + #[serde(default, deserialize_with = "string_or_seq_string")] + pub name: Vec, +} + +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct SeriesRelation { + pub load_media: Option, +} + +// TODO: I don't like this convention and I'd rather figure out a way around it. +// I would prefer /series?library[field]=value, but could not get that to work. +with_prefix!(library_prefix "library_"); + +#[derive(Default, Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct SeriesFilter { + #[serde(default, deserialize_with = "string_or_seq_string")] + pub id: Vec, + #[serde(default, deserialize_with = "string_or_seq_string")] + pub name: Vec, + + #[serde(flatten, with = "library_prefix")] + pub library: Option, +} + +// TODO: I don't like this convention and I'd rather figure out a way around it. +// I would prefer /media?series[field]=value, but could not get that to work. +with_prefix!(series_prefix "series_"); +#[derive(Default, Debug, Deserialize, Serialize, ToSchema)] +pub struct MediaFilter { + #[serde(default)] + pub id: Vec, + #[serde(default)] + pub name: Vec, + #[serde(default)] + pub extension: Vec, + #[serde(flatten, with = "series_prefix")] + pub series: Option, +} diff --git a/apps/server/src/utils/http.rs b/apps/server/src/utils/http.rs index 11b8064d1..f9caff21a 100644 --- a/apps/server/src/utils/http.rs +++ b/apps/server/src/utils/http.rs @@ -1,6 +1,5 @@ use axum::{ body::{BoxBody, StreamBody}, - extract::Query, http::{header, HeaderValue, StatusCode}, response::{IntoResponse, Response}, }; @@ -8,7 +7,7 @@ use std::{ io, path::{Path, PathBuf}, }; -use stump_core::types::{ContentType, PageParams, PagedRequestParams}; +use stump_core::prelude::ContentType; use tokio::fs::File; use tokio_util::io::ReaderStream; @@ -123,26 +122,6 @@ impl IntoResponse for UnknownBufferResponse { } } -pub trait PageableTrait { - fn page_params(self) -> PageParams; -} - -impl PageableTrait for Query { - fn page_params(self) -> PageParams { - let params = self.0; - - let zero_based = params.zero_based.unwrap_or(false); - - PageParams { - zero_based, - page: params.page.unwrap_or(if zero_based { 0 } else { 1 }), - page_size: params.page_size.unwrap_or(20), - order_by: params.order_by.unwrap_or_else(|| "name".to_string()), - direction: params.direction.unwrap_or_default(), - } - } -} - // TODO: I think it would be cool to support some variant of a named file with // range request support. I'm not sure how to do that yet, but it would be cool. // maybe something here -> https://docs.rs/tower-http/latest/tower_http/services/fs/index.html @@ -176,7 +155,7 @@ impl IntoResponse for NamedFile { Response::builder() .header( header::CONTENT_TYPE, - ContentType::from_infer(&self.path_buf).to_string(), + ContentType::from_path(&self.path_buf).to_string(), ) .header( header::CONTENT_DISPOSITION, diff --git a/apps/server/src/utils/mod.rs b/apps/server/src/utils/mod.rs index 5d4cf84f7..2422cf0b1 100644 --- a/apps/server/src/utils/mod.rs +++ b/apps/server/src/utils/mod.rs @@ -1,6 +1,8 @@ mod auth; +mod filter; pub mod http; mod signal; pub(crate) use auth::*; +pub(crate) use filter::*; pub(crate) use signal::*; diff --git a/apps/tui/Cargo.toml b/apps/tui/Cargo.toml deleted file mode 100644 index 4c1248a89..000000000 --- a/apps/tui/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "stump_cli" -version = "0.0.0" -edition = "2021" - -[dependencies] -clap = { version = "3.2.22", features = ["derive"] } -tui = "0.19" -crossterm = "0.25" - -### ASYNC ### -tokio = { version = "1.17.0", features = ["macros", "rt-multi-thread"] } -reqwest = "0.11.11" -reqwest-eventsource = "0.4.0" -futures-util = "0.3.24" - -### UTILS ### -thiserror = "1.0.30" -tokio-graceful-shutdown = "0.11.1" diff --git a/apps/tui/package.json b/apps/tui/package.json deleted file mode 100644 index 32d728fd2..000000000 --- a/apps/tui/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "@stump/tui", - "private": true, - "version": "0.0.0", - "scripts": { - "dev": "cargo watch -x run", - "build": "cargo build --release" - } -} \ No newline at end of file diff --git a/apps/tui/src/api/mod.rs b/apps/tui/src/api/mod.rs deleted file mode 100644 index 70b786d12..000000000 --- a/apps/tui/src/api/mod.rs +++ /dev/null @@ -1 +0,0 @@ -// TODO diff --git a/apps/tui/src/cli/config.rs b/apps/tui/src/cli/config.rs deleted file mode 100644 index daef723c6..000000000 --- a/apps/tui/src/cli/config.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::error::CliError; - -// TODO: remove this -#[allow(unused)] -pub async fn run_command( - base_url: Option, - username: Option, - password: Option, -) -> Result<(), CliError> { - // Ok(()) - unimplemented!() -} diff --git a/apps/tui/src/cli/mod.rs b/apps/tui/src/cli/mod.rs deleted file mode 100644 index 6a9d02568..000000000 --- a/apps/tui/src/cli/mod.rs +++ /dev/null @@ -1,51 +0,0 @@ -use clap::{Parser, Subcommand}; - -use crate::{error::CliError, event::CliEvent}; - -pub mod config; - -#[derive(Parser, Debug)] -#[clap(author, version, about, long_about = None)] -#[clap(propagate_version = true)] -struct Cli { - #[clap(subcommand)] - command: Option, -} - -#[derive(Subcommand, Debug)] -enum Commands { - /// Configures the CLI to connect to the Stump server - Config { - // #[clap()] - base_url: Option, - username: Option, - password: Option, - }, - /// Pings the Stump server - Ping, -} - -pub async fn parse_and_run() -> Result { - let args = Cli::parse(); - match args.command { - Some(Commands::Config { - base_url, - username, - password, - }) => { - config::run_command(base_url, username, password).await?; - }, - Some(Commands::Ping) => { - unimplemented!("Ping command not implemented yet"); - }, - None => { - // When no subcommand is provided, launch the TUI - return Ok(CliEvent::StartTui); - }, - }; - - // If we get here, we've successfully run a command. We should exit the CLI. - Ok(CliEvent::GracefulShutdown(Some( - "Command completed".to_string(), - ))) -} diff --git a/apps/tui/src/error.rs b/apps/tui/src/error.rs deleted file mode 100644 index 351cbfbfb..000000000 --- a/apps/tui/src/error.rs +++ /dev/null @@ -1,21 +0,0 @@ -use thiserror::Error; -use tokio_graceful_shutdown::errors::GracefulShutdownError; - -#[derive(Debug, Error)] -pub enum CliError {} - -#[derive(Debug, Error)] -pub enum TuiError { - #[error("An IO error occurred: {0}")] - IoError(#[from] std::io::Error), - #[error("A graceful shutdown error occurred: {0}")] - ShutdownError(#[from] GracefulShutdownError), - #[error("An error occurred: {0}")] - Unknown(String), -} - -impl From for TuiError { - fn from(_error: CliError) -> Self { - unimplemented!() - } -} diff --git a/apps/tui/src/event/mod.rs b/apps/tui/src/event/mod.rs deleted file mode 100644 index fa3632cda..000000000 --- a/apps/tui/src/event/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod sse; - -pub enum CliEvent { - StartTui, - GracefulShutdown(Option), -} - -// TODO: remove this -#[allow(unused)] -pub enum TuiEvent { - RerenderScreen, - GracefulShutdown(Option), -} diff --git a/apps/tui/src/event/sse.rs b/apps/tui/src/event/sse.rs deleted file mode 100644 index a431b5b4b..000000000 --- a/apps/tui/src/event/sse.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::sync::Arc; - -use futures_util::stream::StreamExt; -use reqwest_eventsource::{Event, EventSource}; -use tokio::sync::mpsc::UnboundedSender; - -use super::TuiEvent; - -// TODO: remove this -#[allow(unused)] -pub struct SSEHandler { - base_url: String, - internal_sender: UnboundedSender, -} - -// FIXME: server is now websockets -impl SSEHandler { - pub fn new(base_url: &str, internal_sender: UnboundedSender) -> Arc { - let this = Arc::new(Self { - base_url: base_url.to_string(), - internal_sender, - }); - - let this_cpy = this.clone(); - tokio::spawn(async move { - // https://docs.rs/reqwest-eventsource/latest/reqwest_eventsource/ - // FIXME: this panics if cannot connect. What a bad implementation... - let mut source = - EventSource::get(&format!("{}/api/jobs/listen", this_cpy.base_url)); - while let Some(event) = source.next().await { - match event { - Ok(Event::Open) => { - // println!("SSE connection opened"); - }, - Ok(Event::Message(message_event)) => { - this_cpy.handle_message(message_event.data); - }, - Err(err) => { - println!("Error: {}", err); - source.close(); - }, - } - } - }); - - this - } - - fn handle_message(&self, data: String) { - println!("SSE message: {}", data); - // deserialize as JobUpdate, will be {key: String, data: ... }, can match - // on that enum accordingly... - // requires some heavy restructure again to access core types lol - // core -> just the core library - // apps/server -> rocket will need to be moved here - } -} diff --git a/apps/tui/src/main.rs b/apps/tui/src/main.rs deleted file mode 100644 index 0fac6da8c..000000000 --- a/apps/tui/src/main.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crossterm::{ - event::{DisableMouseCapture, EnableMouseCapture}, - execute, - terminal::{ - disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, - }, -}; -use error::TuiError; -use event::{sse::SSEHandler, CliEvent, TuiEvent}; -use std::{ - io::{self, Stdout}, - sync::Arc, - thread, - time::Duration, -}; -use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; -use tokio_graceful_shutdown::{SubsystemHandle, Toplevel}; -use tui::{ - backend::CrosstermBackend, - widgets::{Block, Borders}, - Terminal, -}; - -pub(crate) mod api; -pub(crate) mod cli; -pub(crate) mod error; -pub(crate) mod event; - -// TODO: remove this -#[allow(unused)] -struct StumpTui { - base_url: String, - sse_handler: Arc, - internal_sender: UnboundedSender, - internal_receiver: UnboundedReceiver, - term: Terminal>, -} - -impl StumpTui { - pub fn new(base_url: &str, term: Terminal>) -> Self { - let internal_channel = unbounded_channel::(); - - let sender_cpy = internal_channel.0.clone(); - thread::spawn(move || { - thread::sleep(Duration::from_secs(5)); - let _ = sender_cpy.send(TuiEvent::GracefulShutdown(None)); - }); - - Self { - base_url: base_url.to_string(), - sse_handler: SSEHandler::new(base_url, internal_channel.0.clone()), - internal_sender: internal_channel.0, - internal_receiver: internal_channel.1, - term, - } - } - - fn render(&mut self) -> Result<(), TuiError> { - self.term.draw(|f| { - let size = f.size(); - let block = Block::default().title("Stump").borders(Borders::ALL); - f.render_widget(block, size); - })?; - - Ok(()) - } - - pub fn shutdown(&mut self) -> Result<(), TuiError> { - let _ = self.internal_sender.send(TuiEvent::GracefulShutdown(None)); - - Ok(()) - } - - pub async fn run(&mut self) -> Result<(), TuiError> { - self.term.clear()?; - - self.render()?; - - #[allow(clippy::never_loop)] - while let Some(event) = self.internal_receiver.recv().await { - match event { - TuiEvent::GracefulShutdown(message) => { - if let Some(message) = message { - println!("Graceful shutdown: {}", message); - } - break; - }, - _ => { - self.render()?; - - let err = tokio::spawn(async move { - thread::sleep(Duration::from_secs(5)); - TuiError::Unknown("test".to_string()) - }) - .await - .unwrap(); - - return Err(err); - }, - } - } - - disable_raw_mode()?; - execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?; - Ok(()) - } -} - -async fn start_tui(_subsys: SubsystemHandle) -> Result<(), TuiError> { - enable_raw_mode()?; - - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let terminal = Terminal::new(backend)?; - - let mut stump_tui = StumpTui::new("http://localhost:10801", terminal); - - stump_tui.run().await?; - stump_tui.shutdown()?; - - Ok(()) -} - -#[tokio::main] -async fn main() -> Result<(), TuiError> { - if let CliEvent::GracefulShutdown(message) = cli::parse_and_run().await? { - if let Some(message) = message { - println!("{}", message); - } - - return Ok(()); - } - - Ok(Toplevel::new() - .start("start_tui", start_tui) - .catch_signals() - .handle_shutdown_requests(Duration::from_millis(1000)) - .await?) -} diff --git a/apps/web/dist/.placeholder b/apps/web/dist/.placeholder new file mode 100644 index 000000000..32f95c0d1 --- /dev/null +++ b/apps/web/dist/.placeholder @@ -0,0 +1 @@ +hi \ No newline at end of file diff --git a/apps/web/moon.yml b/apps/web/moon.yml new file mode 100644 index 000000000..aa46eea7a --- /dev/null +++ b/apps/web/moon.yml @@ -0,0 +1,24 @@ +type: 'application' + +workspace: + inheritedTasks: + exclude: ['buildPackage'] + +fileGroups: + app: + - 'src/**/*' + +language: 'typescript' + +tasks: + dev: + command: 'vite --host' + local: true + + start: + command: 'vite preview' + local: true + + build: + command: 'vite build' + local: true diff --git a/apps/web/package.json b/apps/web/package.json index f8d0fecd7..0fd5c4e8a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -4,28 +4,29 @@ "description": "", "license": "MIT", "scripts": { - "start": "vite preview", - "dev": "vite --host", "build": "vite build" }, "dependencies": { "@stump/client": "workspace:*", "@stump/interface": "workspace:*", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router": "^6.8.1", + "react-router-dom": "^6.8.1" }, "devDependencies": { - "@tailwindcss/typography": "^0.5.7", - "@types/react": "^18.0.21", - "@types/react-dom": "^18.0.6", - "@vitejs/plugin-react": "^2.0.0", - "autoprefixer": "^10.4.12", - "postcss": "^8.4.17", + "@tailwindcss/typography": "^0.5.9", + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.11", + "@types/react-router-dom": "^5.3.3", + "@vitejs/plugin-react": "^2.2.0", + "autoprefixer": "^10.4.13", + "postcss": "^8.4.21", "tailwind": "^4.0.0", "tailwind-scrollbar-hide": "^1.1.7", - "tailwindcss": "^3.1.8", - "typescript": "^4.8.4", - "vite": "^3.1.6", + "tailwindcss": "^3.2.7", + "typescript": "^4.9.5", + "vite": "^3.2.5", "vite-plugin-tsconfig-paths": "^1.1.0" } -} \ No newline at end of file +} diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js index e873f1a4f..65994d328 100644 --- a/apps/web/postcss.config.js +++ b/apps/web/postcss.config.js @@ -1,6 +1,6 @@ module.exports = { plugins: { - tailwindcss: {}, autoprefixer: {}, + tailwindcss: {}, }, -}; +} diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 07ca9cac0..636178b5b 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,14 +1,12 @@ -import StumpInterface from '@stump/interface'; -import { StumpQueryProvider } from '@stump/client'; +import StumpInterface from '@stump/interface' -import '@stump/interface/styles'; +const getDebugUrl = () => { + const { hostname } = window.location + return `http://${hostname}:10801` +} -export const baseUrl = import.meta.env.PROD ? window.location.href : 'http://localhost:10801'; +export const baseUrl = import.meta.env.PROD ? window.location.href : getDebugUrl() export default function App() { - return ( - - - - ); + return } diff --git a/apps/web/src/env.d.ts b/apps/web/src/env.d.ts index 84bbce675..23e0df365 100644 --- a/apps/web/src/env.d.ts +++ b/apps/web/src/env.d.ts @@ -1,9 +1,9 @@ /// interface ImportMetaEnv { - readonly VITE_STUMP_SERVER_BASE_URL: string; + readonly VITE_STUMP_SERVER_BASE_URL: string } interface ImportMeta { - readonly env: ImportMetaEnv; + readonly env: ImportMetaEnv } diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx index 8f7495734..3ae5bf509 100644 --- a/apps/web/src/index.tsx +++ b/apps/web/src/index.tsx @@ -1,17 +1,17 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; +import React from 'react' +import { createRoot } from 'react-dom/client' -import App from './App'; +import App from './App' -const rootElement = document.getElementById('root'); +const rootElement = document.getElementById('root') if (!rootElement) { - throw new Error('Root element not found'); + throw new Error('Root element not found') } -const root = createRoot(rootElement); +const root = createRoot(rootElement) root.render( , -); +) diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index ac8a93a3a..86fbc0e4c 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -1 +1 @@ -module.exports = require('../../common/config/tailwind.js')('web'); +module.exports = require('../../packages/components/tailwind.js')('web') diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index a7354a4e4..000f17f22 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,7 +1,34 @@ { - "extends": "../../common/config/base.tsconfig.json", - "compilerOptions": { - "types": ["vite/client"] - }, - "include": ["src"] + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": [ + "vite/client" + ], + "outDir": "../../.moon/cache/types/apps/web", + "paths": { + "@stump/client": [ + "../../packages/client/src/index.ts" + ], + "@stump/client/*": [ + "../../packages/client/src/*" + ], + "@stump/interface": [ + "../../packages/interface/src/index.ts" + ], + "@stump/interface/*": [ + "../../packages/interface/src/*" + ] + } + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../packages/client" + }, + { + "path": "../../packages/interface" + } + ] } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 13564d991..7d14ed070 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,24 +1,24 @@ -import { defineConfig } from 'vite'; -import tsconfigPaths from 'vite-plugin-tsconfig-paths'; +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' +import tsconfigPaths from 'vite-plugin-tsconfig-paths' -import react from '@vitejs/plugin-react'; - -import { name, version } from './package.json'; +import { name, version } from './package.json' // https://vitejs.dev/config/ export default defineConfig({ - server: { - port: 3000, + build: { + assetsDir: './assets', + manifest: true, + outDir: '../dist', }, - plugins: [react(), tsconfigPaths()], - root: 'src', - publicDir: '../../../common/interface/public', + clearScreen: false, define: { pkgJson: { name, version }, }, - build: { - outDir: '../dist', - assetsDir: './assets', - manifest: true, + plugins: [react(), tsconfigPaths()], + publicDir: '../../../packages/interface/public', + root: 'src', + server: { + port: 3000, }, -}); +}) diff --git a/common/client/package.json b/common/client/package.json deleted file mode 100644 index cbcc27fbb..000000000 --- a/common/client/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "@stump/client", - "version": "0.0.0", - "private": true, - "main": "src/index.ts", - "exports": { - ".": "./src/index.ts", - "./api": "./src/api/index.ts", - "./types": "./src/types/index.ts" - }, - "scripts": { - "dev": "tsc -w", - "build": "tsc", - "check": "tsc --noEmit" - }, - "dependencies": { - "@stump/config": "workspace:*", - "@tanstack/react-query": "^4.10.3", - "axios": "^1.1.2", - "immer": "^9.0.15", - "react-use-websocket": "^4.2.0", - "zustand": "^4.1.1" - }, - "devDependencies": { - "@types/axios": "^0.14.0", - "@types/node": "^18.8.3", - "@types/react": "^18.0.21", - "tsconfig": "*", - "typescript": "^4.8.4" - }, - "peerDependencies": { - "react": "^18.2.0" - } -} \ No newline at end of file diff --git a/common/client/src/Provider.tsx b/common/client/src/Provider.tsx deleted file mode 100644 index 7a43fe986..000000000 --- a/common/client/src/Provider.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { QueryClientProvider } from '@tanstack/react-query'; -import { ReactElement, useState } from 'react'; -import { queryClient } from './client'; -import { ActiveJobContext, StumpQueryContext } from './context'; -import { JobUpdate } from './types'; - -export function StumpQueryProvider({ children }: { children: ReactElement }) { - return ( - - {children} - - ); -} - -export function JobContextProvider({ children }: { children: ReactElement }) { - const [jobs, setJobs] = useState>({}); - - function addJob(newJob: JobUpdate) { - let job = jobs[newJob.runner_id]; - - if (job) { - updateJob(newJob); - } else { - setJobs((jobs) => ({ - ...jobs, - [newJob.runner_id]: newJob, - })); - } - } - - function updateJob(jobUpdate: JobUpdate) { - let job = jobs[jobUpdate.runner_id]; - - if (!job || !Object.keys(jobs).length) { - addJob(jobUpdate); - return; - } - - const { current_task, message, task_count } = jobUpdate; - job = { - ...job, - current_task, - message, - task_count, - }; - - setJobs((jobs) => ({ - ...jobs, - [jobUpdate.runner_id]: job, - })); - } - - function removeJob(jobId: string) { - setJobs((jobs) => { - const newJobs = { ...jobs }; - delete newJobs[jobId]; - return newJobs; - }); - } - - return ( - - {children} - - ); -} diff --git a/common/client/src/api/auth.ts b/common/client/src/api/auth.ts deleted file mode 100644 index 75a306602..000000000 --- a/common/client/src/api/auth.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ApiResult, LoginOrRegisterArgs, User } from '../types'; -import { API } from '.'; - -// TODO: types - -export function me(): Promise> { - return API.get('/auth/me'); -} - -export function login(input: LoginOrRegisterArgs): Promise> { - return API.post('/auth/login', input); -} - -export function register(payload: LoginOrRegisterArgs) { - return API.post('/auth/register', payload); -} - -export function logout(): Promise> { - return API.post('/auth/logout'); -} diff --git a/common/client/src/api/config.ts b/common/client/src/api/config.ts deleted file mode 100644 index eb9cdb78c..000000000 --- a/common/client/src/api/config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ApiResult, ClaimResponse } from '../types'; -import { API } from '.'; - -export function ping() {} - -export async function checkIsClaimed(): Promise> { - return API.get('/claim'); -} diff --git a/common/client/src/api/filesystem.ts b/common/client/src/api/filesystem.ts deleted file mode 100644 index 901bfaf07..000000000 --- a/common/client/src/api/filesystem.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { ApiResult, DirectoryListing, DirectoryListingInput, Pageable } from '../types'; -import { API } from '.'; - -interface ListDirectoryFnInput extends DirectoryListingInput { - page?: number; -} - -export function listDirectory( - input?: ListDirectoryFnInput, -): Promise>> { - if (input?.page != null) { - return API.post(`/filesystem?page=${input.page}`, input); - } - - return API.post('/filesystem', input); -} diff --git a/common/client/src/api/index.ts b/common/client/src/api/index.ts deleted file mode 100644 index 6a8711d1a..000000000 --- a/common/client/src/api/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import axios, { AxiosInstance } from 'axios'; - -export * from './auth'; -export * from './config'; -export * from './epub'; -export * from './series'; -export * from './media'; -export * from './job'; - -export let API: AxiosInstance; - -/** - * Creates an axios instance with the given base URL and assigns it to the global - * `API` variable. - */ -export function initializeApi(baseUrl: string) { - let correctedUrl = baseUrl; - - // remove trailing slash - if (correctedUrl.endsWith('/')) { - // correctedUrl = correctedUrl.slice(0, -1); - } - - // add api to end of URL, don't allow double slashes - if (!correctedUrl.endsWith('/api')) { - correctedUrl += '/api'; - } - - // remove all double slashes AFTER the initial http:// or https:// or whatever - correctedUrl = correctedUrl.replace(/([^:]\/)\/+/g, '$1'); - - API = axios.create({ - baseURL: correctedUrl, - withCredentials: true, - }); -} - -// TODO: be better lol -export function isUrl(url: string) { - return url.startsWith('http://') || url.startsWith('https://'); -} - -export async function checkUrl(url: string) { - if (!isUrl(url)) { - return false; - } - - const res = await fetch(`${url}/api/ping`).catch((err) => err); - - return res.status === 200; -} diff --git a/common/client/src/api/job.ts b/common/client/src/api/job.ts deleted file mode 100644 index ecf4dbcac..000000000 --- a/common/client/src/api/job.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ApiResult, JobReport } from '../types'; -import { API } from '.'; - -export function getJobs(): Promise> { - return API.get('/jobs'); -} - -// TODO: type this -export function cancelJob(id: string): Promise> { - return API.delete(`/jobs/${id}/cancel`); -} diff --git a/common/client/src/api/log.ts b/common/client/src/api/log.ts deleted file mode 100644 index dee9c6cdc..000000000 --- a/common/client/src/api/log.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ApiResult, LogMetadata } from '../types'; -import { API } from '.'; - -export function getLogFileMeta(): Promise> { - return API.get('/logs'); -} - -export function clearLogFile() { - return API.delete('/logs'); -} diff --git a/common/client/src/api/media.ts b/common/client/src/api/media.ts deleted file mode 100644 index 437c26123..000000000 --- a/common/client/src/api/media.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { ApiResult, Media, PageableApiResult, ReadProgress } from '../types'; -import { API } from '.'; - -type GetMediaById = ApiResult; - -export function getMedia(): Promise> { - return API.get('/media?unpaged=true'); -} - -export function getPaginatedMedia(page: number): Promise> { - return API.get(`/media?page=${page}`); -} - -export function getMediaById(id: string): Promise { - return API.get(`/media/${id}`); -} - -export function getMediaThumbnail(id: string): string { - return `${API.getUri()}/media/${id}/thumbnail`; -} - -export function getMediaPage(id: string, page: number): string { - return `${API.getUri()}/media/${id}/page/${page}`; -} - -export function updateMediaProgress(id: string, page: number): Promise { - return API.put(`/media/${id}/progress/${page}`); -} diff --git a/common/client/src/api/series.ts b/common/client/src/api/series.ts deleted file mode 100644 index 14fe8f873..000000000 --- a/common/client/src/api/series.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ApiResult, Media, PageableApiResult, Series } from '../types'; -import { API } from '.'; - -export function getSeriesById(id: string): Promise> { - return API.get(`/series/${id}`); -} - -export function getSeriesMedia( - id: string, - page: number, - params?: string, -): Promise> { - if (params) { - return API.get(`/series/${id}/media?page=${page}&${params}`); - } - - return API.get(`/series/${id}/media?page=${page}`); -} - -export function getNextInSeries(id: string): Promise> { - return API.get(`/series/${id}/media/next`); -} - -export function getSeriesThumbnail(id: string): string { - return `${API.getUri()}/series/${id}/thumbnail`; -} diff --git a/common/client/src/api/server.ts b/common/client/src/api/server.ts deleted file mode 100644 index a999d6c15..000000000 --- a/common/client/src/api/server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { ApiResult, StumpVersion } from '../types'; -import { API } from '.'; - -export function getStumpVersion(): Promise> { - return API.post('/version'); -} diff --git a/common/client/src/api/tag.ts b/common/client/src/api/tag.ts deleted file mode 100644 index 6d7535918..000000000 --- a/common/client/src/api/tag.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ApiResult, Tag } from '../types'; -import { API } from '.'; - -export function getAllTags(): Promise> { - return API.get('/tags'); -} - -export function createTags(tags: string[]): Promise> { - return API.post('/tags', { tags }); -} diff --git a/common/client/src/api/user.ts b/common/client/src/api/user.ts deleted file mode 100644 index 9f8297060..000000000 --- a/common/client/src/api/user.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ApiResult, UserPreferences } from '../types'; -import { API } from '.'; - -export function getUserPreferences(userId: string): Promise> { - return API.get(`/users/${userId}/preferences`); -} - -export function updateUserPreferences( - userId: string, - preferences: UserPreferences, -): Promise> { - return API.put(`/users/${userId}/preferences`, preferences); -} diff --git a/common/client/src/client.ts b/common/client/src/client.ts deleted file mode 100644 index 6892a654f..000000000 --- a/common/client/src/client.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { QueryClient, useQuery as _useQuery } from '@tanstack/react-query'; - -export * from './queries'; - -export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - refetchOnWindowFocus: false, - suspense: true, - }, - }, -}); diff --git a/common/client/src/context.ts b/common/client/src/context.ts deleted file mode 100644 index fbb0c20c4..000000000 --- a/common/client/src/context.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { QueryClient } from '@tanstack/react-query'; -import { createContext, useContext } from 'react'; -import { JobUpdate } from './types'; - -export const AppPropsContext = createContext(null); -export const StumpQueryContext = createContext(undefined); - -export type Platform = 'browser' | 'macOS' | 'windows' | 'linux' | 'unknown'; - -export interface AppProps { - platform: Platform; - baseUrl?: string; - demoMode?: boolean; - - setBaseUrl?: (baseUrl: string) => void; - setUseDiscordPresence?: (connect: boolean) => void; - setDiscordPresence?: (status?: string, details?: string) => void; -} - -export interface JobContext { - activeJobs: Record; - - addJob(job: JobUpdate): void; - updateJob(job: JobUpdate): void; - removeJob(runnerId: string): void; -} -export const ActiveJobContext = createContext(null); - -export const useAppProps = () => useContext(AppPropsContext); -export const useJobContext = () => useContext(ActiveJobContext); -export const useQueryContext = () => useContext(StumpQueryContext); diff --git a/common/client/src/hooks/index.ts b/common/client/src/hooks/index.ts deleted file mode 100644 index 1bfbcc687..000000000 --- a/common/client/src/hooks/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './useCoreEvent'; -export * from './useStumpWs'; -export * from './useStumpSse'; -export * from './useLayoutMode'; diff --git a/common/client/src/hooks/useCoreEvent.ts b/common/client/src/hooks/useCoreEvent.ts deleted file mode 100644 index c0ea3c582..000000000 --- a/common/client/src/hooks/useCoreEvent.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { CoreEvent } from '../types'; -import { queryClient } from '../client'; -import { useStumpSse } from './useStumpSse'; -import { useJobContext } from '../context'; - -interface UseCoreEventHandlerParams { - onJobComplete?: (jobId: string) => void; - onJobFailed?: (err: { runner_id: string; message: string }) => void; -} - -export function useCoreEventHandler({ - onJobComplete, - onJobFailed, -}: UseCoreEventHandlerParams = {}) { - const context = useJobContext(); - - if (!context) { - throw new Error('useCoreEventHandler must be used within a JobContext'); - } - - const { addJob, updateJob, removeJob } = context; - - function handleCoreEvent(event: CoreEvent) { - const { key, data } = event; - - switch (key) { - case 'JobStarted': - addJob(data); - break; - case 'JobProgress': - // FIXME: Testing with a test library containing over 10k cbz files, there are so - // many updates that around 2000k it just dies. I have implemented a check to - // in this store function where if the task_count is greater than 1000, it will - // only update the store every 50 tasks. This is a temporary fix. The UI is still pretty - // slow when this happens, but is usable. A better solution needs to be found. - updateJob(data); - break; - case 'JobComplete': - setTimeout(() => { - removeJob(data); - - queryClient.invalidateQueries(['getLibrary']); - queryClient.invalidateQueries(['getLibrariesStats']); - queryClient.invalidateQueries(['getSeries']); - queryClient.invalidateQueries(['getJobReports']); - - // toast.success(`Job ${data} complete.`); - onJobComplete?.(data); - }, 750); - break; - case 'JobFailed': - onJobFailed?.(data); - removeJob(data.runner_id); - queryClient.invalidateQueries(['getJobReports']); - - break; - case 'CreatedMedia': - case 'CreatedMediaBatch': - case 'CreatedSeries': - // I set a timeout here to give the backend a little time to analyze at least - // one of the books in a new series before triggering a refetch. This is to - // prevent the series/media cards from being displayed before there is an image ready. - setTimeout(() => { - // TODO: I must misunderstand how this function works. Giving multiple keys - // does not work, not a huge deal but would rather a one-liner for these. - queryClient.invalidateQueries(['getLibrary']); - queryClient.invalidateQueries(['getLibrariesStats']); - queryClient.invalidateQueries(['getSeries']); - }, 250); - break; - default: - console.warn('Unknown JobEvent', data); - console.debug(data); - break; - } - } - - useStumpSse({ onEvent: handleCoreEvent }); -} diff --git a/common/client/src/hooks/useStumpSse.ts b/common/client/src/hooks/useStumpSse.ts deleted file mode 100644 index 4908c371e..000000000 --- a/common/client/src/hooks/useStumpSse.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { CoreEvent } from '../types'; -import { useEffect, useMemo } from 'react'; - -import { API } from '../api'; -import { useStumpStore } from '../stores'; - -interface SseOptions { - onOpen?: (event: Event) => void; - onClose?: (event?: Event) => void; - onMessage?: (event: MessageEvent) => void; - onError?: (event: Event) => void; -} - -let sse: EventSource; - -// this is a little meh -function useSse(url: string, sseOptions: SseOptions = {}) { - const { onOpen, onClose, onMessage } = sseOptions; - - function initEventSource() { - sse = new EventSource(url, { - withCredentials: true, - }); - - sse.onmessage = (e) => { - // console.log('EVENT', e); - onMessage?.(e); - }; - - sse.onerror = (event) => { - console.error('EventSource error event:', event); - - sse?.close(); - - setTimeout(() => { - initEventSource(); - - if (sse?.readyState !== EventSource.OPEN) { - onClose?.(event); - return; - } - }, 5000); - }; - - sse.onopen = (e) => { - onOpen?.(e); - }; - } - - useEffect(() => { - initEventSource(); - - return () => { - sse?.close(); - }; - }, [url]); - - return { - readyState: sse?.readyState, - }; -} - -interface Props { - onEvent(event: CoreEvent): void; -} - -export function useStumpSse({ onEvent }: Props) { - const { setConnected } = useStumpStore(); - - const eventSourceUrl = useMemo(() => { - let url = API.getUri(); - // remove /api(/) from end of url - url = url.replace(/\/api\/?$/, ''); - - return `${url}/sse`; - }, [API?.getUri()]); - - function handleMessage(e: MessageEvent) { - try { - const event = JSON.parse(e.data); - onEvent(event); - } catch (err) { - console.error(err); - } - } - - const { readyState } = useSse(eventSourceUrl, { - onMessage: handleMessage, - onOpen: () => { - setConnected(true); - }, - onClose: () => { - setConnected(false); - }, - }); - - return { - readyState, - }; -} diff --git a/common/client/src/hooks/useStumpWs.ts b/common/client/src/hooks/useStumpWs.ts deleted file mode 100644 index d88fde3fa..000000000 --- a/common/client/src/hooks/useStumpWs.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { CoreEvent } from '../types'; -import { useMemo } from 'react'; -import useWebSocket, { ReadyState } from 'react-use-websocket'; -import { API } from '../api'; -import { useStumpStore } from '../stores'; - -interface Props { - onEvent(event: CoreEvent): void; -} - -export function useStumpWs({ onEvent }: Props) { - const { setConnected } = useStumpStore(); - - const socketUrl = useMemo(() => { - let url = API.getUri(); - // remove http(s):// from url, and replace with ws(s):// - url = url.replace(/^http(s?):\/\//, 'ws$1://'); - // remove /api(/) from end of url - url = url.replace(/\/api\/?$/, ''); - - return `${url}/ws`; - }, [API?.getUri()]); - - function handleWsMessage(event: MessageEvent) { - try { - const data = JSON.parse(event.data); - onEvent(data); - } catch (err) { - console.error(err); - } - } - - function handleOpen() { - setConnected(true); - } - - function handleClose() { - setConnected(false); - } - - const { readyState } = useWebSocket(socketUrl, { - onMessage: handleWsMessage, - onOpen: handleOpen, - onClose: handleClose, - }); - - return { readyState }; -} - -// Re-export the ready state enum so consumer of client can use it if needed -export { ReadyState }; diff --git a/common/client/src/index.ts b/common/client/src/index.ts deleted file mode 100644 index 2958dac47..000000000 --- a/common/client/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './stores'; -export * from './context'; -export * from './client'; -export * from './hooks'; -export * from './Provider'; -export * from './types'; diff --git a/common/client/src/queries/auth.ts b/common/client/src/queries/auth.ts deleted file mode 100644 index 920519afe..000000000 --- a/common/client/src/queries/auth.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { useMutation, useQuery } from '@tanstack/react-query'; - -import { checkIsClaimed } from '../api'; -import { login, me, register } from '../api/auth'; -import { queryClient } from '../client'; -import { ClientQueryParams, QueryCallbacks } from '.'; -import { StumpQueryContext } from '../context'; - -import type { User } from '../types'; -export interface AuthQueryOptions extends QueryCallbacks { - disabled?: boolean; - // onSuccess?: (user: User | null) => void; - enabled?: boolean; -} - -export function useAuthQuery(options: AuthQueryOptions = {}) { - const { data, error, isLoading, isFetching, isRefetching } = useQuery(['getViewer'], me, { - onSuccess(res) { - options.onSuccess?.(res.data); - }, - onError(err) { - options.onError?.(err); - }, - useErrorBoundary: false, - enabled: options?.enabled, - context: StumpQueryContext, - }); - - return { - user: data, - error, - isLoading: isLoading || isFetching || isRefetching, - }; -} - -export function useLoginOrRegister({ onSuccess, onError }: ClientQueryParams) { - const [isClaimed, setIsClaimed] = useState(true); - - const { data: claimCheck, isLoading: isCheckingClaimed } = useQuery(['checkIsClaimed'], { - queryFn: checkIsClaimed, - context: StumpQueryContext, - }); - - useEffect(() => { - if (claimCheck?.data && !claimCheck.data.is_claimed) { - setIsClaimed(false); - } - }, [claimCheck]); - - const { isLoading: isLoggingIn, mutateAsync: loginUser } = useMutation(['loginUser'], { - mutationFn: login, - onSuccess: (res) => { - if (!res.data) { - onError?.(res); - } else { - queryClient.invalidateQueries(['getLibraries']); - - onSuccess?.(res.data); - } - }, - onError: (err) => { - onError?.(err); - }, - context: StumpQueryContext, - }); - - const { isLoading: isRegistering, mutateAsync: registerUser } = useMutation(['registerUser'], { - mutationFn: register, - // onError(err) { - // onError?.(err); - // }, - context: StumpQueryContext, - }); - - return { - isClaimed, - isCheckingClaimed, - isLoggingIn, - isRegistering, - loginUser, - registerUser, - }; -} diff --git a/common/client/src/queries/index.ts b/common/client/src/queries/index.ts deleted file mode 100644 index a52e8e9ae..000000000 --- a/common/client/src/queries/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { ApiResult } from '../types'; - -export * from './auth'; -export * from './epub'; -export * from './filesystem'; -export * from './job'; -export * from './library'; -export * from './media'; -export * from './series'; -export * from './tag'; -export * from './server'; -export * from './user'; - -export interface QueryCallbacks { - onSuccess?: (data?: T | null) => void; - onError?: (data: unknown) => void; -} - -export interface CreateCallbacks { - onCreated?: (data: T) => void; - onCreateFailed?: (res: ApiResult) => void; - onError?: (data: unknown) => void; -} - -export interface UpdateCallbacks { - onUpdated?: (data: T) => void; - onUpdateFailed?: (res: ApiResult) => void; - onError?: (data: unknown) => void; -} - -export interface DeleteCallbacks { - onDeleted?: (data: T) => void; - onDeleteFailed?: (res: ApiResult) => void; - onError?: (data: unknown) => void; -} - -export type MutationCallbacks = CreateCallbacks & UpdateCallbacks & DeleteCallbacks; - -export type ClientQueryParams = QueryCallbacks & MutationCallbacks; - -// TODO: I think it would be better to split up some of my mutations into updates -// and creates. I think that would make it easier to handle errors and loading states. diff --git a/common/client/src/queries/job.ts b/common/client/src/queries/job.ts deleted file mode 100644 index 015e5d4f7..000000000 --- a/common/client/src/queries/job.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { JobReport } from '../types'; -import { useQuery } from '@tanstack/react-query'; -import { QueryCallbacks } from '.'; -import { getJobs } from '../api/job'; -import { StumpQueryContext } from '../context'; - -export function useJobReport({ onSuccess, onError }: QueryCallbacks = {}) { - const { - data: jobReports, - isLoading, - isRefetching, - isFetching, - } = useQuery(['getJobReports'], () => getJobs().then((res) => res.data), { - onSuccess, - onError, - context: StumpQueryContext, - }); - - return { jobReports, isLoading: isLoading || isRefetching || isFetching }; -} diff --git a/common/client/src/queries/library.ts b/common/client/src/queries/library.ts deleted file mode 100644 index f05f0a7a2..000000000 --- a/common/client/src/queries/library.ts +++ /dev/null @@ -1,183 +0,0 @@ -import type { Library, PageInfo } from '../types'; -import type { ClientQueryParams, QueryCallbacks } from '.'; -import { AxiosError } from 'axios'; -import { useMemo } from 'react'; -// import { useSearchParams } from 'react-router-dom'; - -import { useMutation, useQuery } from '@tanstack/react-query'; - -import { - createLibrary, - deleteLibrary, - editLibrary, - getLibraries, - getLibrariesStats, - getLibraryById, - getLibrarySeries, - scanLibary, -} from '../api/library'; -import { queryClient } from '../client'; -import { StumpQueryContext } from '../context'; -import { useQueryParamStore } from '../stores'; - -export function useLibrary(id: string, options: QueryCallbacks = {}) { - const { isLoading, data: library } = useQuery(['getLibrary', id], { - queryFn: async () => getLibraryById(id).then((res) => res.data), - onError(err) { - options.onError?.(err); - }, - context: StumpQueryContext, - }); - - return { isLoading, library }; -} - -export interface UseLibrariesReturn { - libraries: Library[]; - pageData?: PageInfo; -} - -export function useLibraries() { - const { data, ...rest } = useQuery(['getLibraries'], getLibraries, { - // Send all non-401 errors to the error page - useErrorBoundary: (err: AxiosError) => !err || (err.response?.status ?? 500) !== 401, - context: StumpQueryContext, - }); - - const { libraries, pageData } = useMemo(() => { - if (data?.data) { - return { - libraries: data.data.data, - pageData: data.data._page, - }; - } - - return { libraries: [] }; - }, [data]); - - return { - libraries, - pageData, - ...rest, - }; -} - -export function useLibrarySeries(libraryId: string, page: number = 1) { - const { getQueryString, ...paramsStore } = useQueryParamStore(); - - const { isLoading, isFetching, isPreviousData, data } = useQuery( - ['getLibrarySeries', page, libraryId, paramsStore], - () => - getLibrarySeries(libraryId, page, getQueryString()).then(({ data }) => ({ - series: data.data, - pageData: data._page, - })), - { - keepPreviousData: true, - context: StumpQueryContext, - }, - ); - - const { series, pageData } = data ?? {}; - - return { isLoading, isFetching, isPreviousData, series, pageData }; -} - -export function useLibraryStats() { - const { - data: libraryStats, - isLoading, - isRefetching, - isFetching, - } = useQuery(['getLibraryStats'], () => getLibrariesStats().then((data) => data.data), { - context: StumpQueryContext, - }); - - return { libraryStats, isLoading: isLoading || isRefetching || isFetching }; -} - -export function useScanLibrary({ onError }: ClientQueryParams = {}) { - const { mutate: scan, mutateAsync: scanAsync } = useMutation(['scanLibary'], { - mutationFn: scanLibary, - onError, - context: StumpQueryContext, - }); - - return { scan, scanAsync }; -} - -export function useLibraryMutation({ - onCreated, - onUpdated, - onDeleted, - onCreateFailed, - onUpdateFailed, - onError, -}: ClientQueryParams = {}) { - const { isLoading: createIsLoading, mutateAsync: createLibraryAsync } = useMutation( - ['createLibrary'], - { - mutationFn: createLibrary, - onSuccess: (res) => { - if (!res.data) { - onCreateFailed?.(res); - } else { - queryClient.invalidateQueries(['getLibraries']); - queryClient.invalidateQueries(['getJobReports']); - queryClient.invalidateQueries(['getLibraryStats']); - onCreated?.(res.data); - // onClose(); - } - }, - onError: (err) => { - // toast.error('Login failed. Please try again.'); - onError?.(err); - }, - context: StumpQueryContext, - }, - ); - - const { isLoading: editIsLoading, mutateAsync: editLibraryAsync } = useMutation(['editLibrary'], { - mutationFn: editLibrary, - onSuccess: (res) => { - if (!res.data) { - // throw new Error('Something went wrong.'); - // TODO: log? - onUpdateFailed?.(res); - } else { - queryClient.invalidateQueries(['getLibraries']); - queryClient.invalidateQueries(['getJobReports']); - queryClient.invalidateQueries(['getLibraryStats']); - // onClose(); - onUpdated?.(res.data); - } - }, - onError: (err) => { - onError?.(err); - // TODO: handle this error - // toast.error('Login failed. Please try again.'); - console.error(err); - }, - context: StumpQueryContext, - }); - - const { mutateAsync: deleteLibraryAsync } = useMutation(['deleteLibrary'], { - mutationFn: deleteLibrary, - async onSuccess(res) { - // FIXME: just realized invalidateQueries is async... I need to check all my usages of it... - await queryClient.invalidateQueries(['getLibraries']); - await queryClient.invalidateQueries(['getLibraryStats']); - - onDeleted?.(res.data); - }, - context: StumpQueryContext, - }); - - return { - createIsLoading, - editIsLoading, - createLibraryAsync, - editLibraryAsync, - deleteLibraryAsync, - }; -} diff --git a/common/client/src/queries/media.ts b/common/client/src/queries/media.ts deleted file mode 100644 index 1a9f5150f..000000000 --- a/common/client/src/queries/media.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Media, ReadProgress } from '../types'; -import type { MutationCallbacks, QueryCallbacks } from '.'; - -import { useMutation, useQuery } from '@tanstack/react-query'; - -import { getMediaById, updateMediaProgress } from '../api'; -import { queryClient } from '../client'; -import { StumpQueryContext } from '../context'; - -export const prefetchMedia = async (id: string) => { - await queryClient.prefetchQuery(['getMediaById', id], () => getMediaById(id), { - staleTime: 10 * 1000, - }); -}; - -export function useMedia(id: string, options: QueryCallbacks = {}) { - const { - data: media, - isLoading, - isFetching, - isRefetching, - } = useQuery(['getMediaById'], { - queryFn: async () => getMediaById(id).then((res) => res.data), - onSuccess(data) { - options.onSuccess?.(data); - }, - onError(err) { - options.onError?.(err); - }, - context: StumpQueryContext, - }); - - return { isLoading: isLoading || isFetching || isRefetching, media }; -} - -export function useMediaMutation(id: string, options: MutationCallbacks = {}) { - const { - mutate: updateReadProgress, - mutateAsync: updateReadProgressAsync, - isLoading, - } = useMutation(['updateReadProgress'], (page: number) => updateMediaProgress(id, page), { - onSuccess(data) { - options.onUpdated?.(data); - }, - onError(err) { - options.onError?.(err); - }, - context: StumpQueryContext, - }); - - return { updateReadProgress, updateReadProgressAsync, isLoading }; -} diff --git a/common/client/src/queries/series.ts b/common/client/src/queries/series.ts deleted file mode 100644 index a90fbea88..000000000 --- a/common/client/src/queries/series.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { Media, Series } from '../types'; -import type { QueryCallbacks } from '.'; -import { useMemo } from 'react'; - -import { useQuery } from '@tanstack/react-query'; - -import { getNextInSeries, getSeriesById, getSeriesMedia } from '../api'; -import { queryClient } from '../client'; -import { StumpQueryContext } from '../context'; -import { useQueryParamStore } from '../stores'; - -export const prefetchSeries = async (id: string) => { - await queryClient.prefetchQuery(['getSeries', id], () => getSeriesById(id), { - staleTime: 10 * 1000, - }); -}; - -export function useSeries(id: string, options: QueryCallbacks = {}) { - const { - isLoading, - isFetching, - isRefetching, - data: series, - } = useQuery(['getSeries'], { - queryFn: async () => getSeriesById(id).then((res) => res.data), - onSuccess(data) { - options.onSuccess?.(data); - }, - onError(err) { - options.onError?.(err); - }, - context: StumpQueryContext, - }); - - return { isLoading: isLoading || isFetching || isRefetching, series }; -} - -export function useSeriesMedia(seriesId: string, page: number = 1) { - const { getQueryString, ...paramsStore } = useQueryParamStore(); - - const { isLoading, isFetching, isRefetching, isPreviousData, data } = useQuery( - ['getSeriesMedia', page, seriesId, paramsStore], - () => - getSeriesMedia(seriesId, page, getQueryString()).then(({ data }) => ({ - media: data.data, - pageData: data._page, - })), - { - keepPreviousData: true, - context: StumpQueryContext, - }, - ); - - const { media, pageData } = data ?? {}; - - return { - isLoading: isLoading || isFetching || isRefetching, - isPreviousData, - media, - pageData, - }; -} - -export function useUpNextInSeries(id: string, options: QueryCallbacks = {}) { - const { - data: media, - isLoading, - isFetching, - isRefetching, - } = useQuery(['getNextInSeries', id], () => getNextInSeries(id).then((res) => res.data), { - onSuccess(data) { - options.onSuccess?.(data); - }, - onError(err) { - options.onError?.(err); - }, - context: StumpQueryContext, - }); - - return { isLoading: isLoading || isFetching || isRefetching, media }; -} diff --git a/common/client/src/queries/server.ts b/common/client/src/queries/server.ts deleted file mode 100644 index 70ba1b101..000000000 --- a/common/client/src/queries/server.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { getStumpVersion } from '../api/server'; -import { StumpQueryContext } from '../context'; - -export function useStumpVersion() { - const { data: version } = useQuery( - ['stumpVersion'], - () => getStumpVersion().then((res) => res.data), - { - onError(err) { - console.error('Failed to fetch Stump API version:', err); - }, - context: StumpQueryContext, - }, - ); - - return version; -} diff --git a/common/client/src/queries/tag.ts b/common/client/src/queries/tag.ts deleted file mode 100644 index b931e0c10..000000000 --- a/common/client/src/queries/tag.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { ApiResult, Tag } from '../types'; -import { AxiosError } from 'axios'; -import { useMemo } from 'react'; - -import { useMutation, useQuery } from '@tanstack/react-query'; - -import { createTags, getAllTags } from '../api/tag'; -import { queryClient } from '../client'; -import { StumpQueryContext } from '../context'; - -export interface UseTagsConfig { - onQuerySuccess?: (res: ApiResult) => void; - onQueryError?: (err: AxiosError) => void; - onCreateSuccess?: (res: ApiResult) => void; - onCreateError?: (err: AxiosError) => void; -} - -export interface TagOption { - label: string; - value: string; -} - -export function useTags({ - onQuerySuccess, - onQueryError, - onCreateSuccess, - onCreateError, -}: UseTagsConfig = {}) { - const { data, isLoading, refetch } = useQuery(['getAllTags'], { - queryFn: getAllTags, - onSuccess: onQuerySuccess, - onError: onQueryError, - suspense: false, - context: StumpQueryContext, - }); - - const { - mutate, - mutateAsync, - isLoading: isCreating, - } = useMutation(['createTags'], { - mutationFn: createTags, - onSuccess(res) { - onCreateSuccess?.(res); - - queryClient.refetchQueries(['getAllTags']); - }, - onError: onCreateError, - context: StumpQueryContext, - }); - - const { tags, options } = useMemo(() => { - if (data && data.data) { - const tagOptions = data.data?.map( - (tag) => - ({ - label: tag.name, - value: tag.name, - } as TagOption), - ); - - return { tags: data.data, options: tagOptions }; - } - - return { tags: [], options: [] }; - }, [data]); - - return { - tags, - options, - isLoading, - refetch, - createTags: mutate, - createTagsAsync: mutateAsync, - isCreating, - }; -} diff --git a/common/client/src/stores/index.ts b/common/client/src/stores/index.ts deleted file mode 100644 index c41dc566f..000000000 --- a/common/client/src/stores/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './useStumpStore'; -export * from './useUserStore'; -export * from './useJobStore'; -export * from './useQueryParamStore'; -export * from './useTopBarStore'; - -export interface StoreBase> { - reset(): void; - set(changes: Partial): void; -} diff --git a/common/client/src/stores/useLayoutStore.ts b/common/client/src/stores/useLayoutStore.ts deleted file mode 100644 index 79aa1a291..000000000 --- a/common/client/src/stores/useLayoutStore.ts +++ /dev/null @@ -1,21 +0,0 @@ -import create from 'zustand'; -import { devtools, persist } from 'zustand/middleware'; -import { StoreBase } from '.'; - -interface LayoutStore extends StoreBase {} - -export const useLayoutStore = create()( - devtools( - persist( - (set) => ({ - reset() { - set(() => ({})); - }, - set(changes) { - set((state) => ({ ...state, ...changes })); - }, - }), - { name: 'stump-layout-store' }, - ), - ), -); diff --git a/common/client/src/stores/useQueryParamStore.ts b/common/client/src/stores/useQueryParamStore.ts deleted file mode 100644 index 82243f1ea..000000000 --- a/common/client/src/stores/useQueryParamStore.ts +++ /dev/null @@ -1,83 +0,0 @@ -import create from 'zustand'; -import { devtools, persist } from 'zustand/middleware'; - -import type { Direction, PageParams } from '../types'; - -import { StoreBase } from './'; - -export const DEFAULT_ORDER_BY = 'name'; -export const DEFAULT_ORDER_DIRECTION = 'asc'; -export const DEFAULT_PAGE_SIZE = 20; - -// TODO: search? -export interface QueryParamStore extends Partial, StoreBase { - setZeroBased: (zeroBased?: boolean) => void; - setPageSize: (pageSize?: number) => void; - setOrderBy: (orderBy?: string) => void; - setDirection: (direction?: Direction) => void; - - getQueryString: () => string; -} - -const defaultValues = { - // zeroBased: false, - // pageSize: 20, - order_by: 'name', - direction: 'asc', -} as Partial; - -export const useQueryParamStore = create()( - devtools( - persist( - (set, get) => ({ - ...defaultValues, - - setZeroBased(zeroBased) { - set((store) => ({ ...store, zero_based: zeroBased })); - }, - setPageSize(pageSize) { - set((store) => ({ ...store, page_zize: pageSize })); - }, - setOrderBy(orderBy) { - set((store) => ({ ...store, order_by: orderBy })); - }, - setDirection(direction) { - set((store) => ({ ...store, direction })); - }, - - getQueryString() { - let params = ''; - - for (const [key, value] of Object.entries(get())) { - if (value != undefined && typeof value !== 'function' && typeof value !== 'object') { - params += `${key}=${value}&`; - } - } - - // remote trailing & if present - if (params.endsWith('&')) { - return params.slice(0, -1); - } - - return params; - }, - - reset() { - set(() => ({})); - }, - set(changes) { - set((state) => ({ ...state, ...changes })); - }, - }), - { - name: 'stump-query-param-store', - getStorage: () => sessionStorage, - partialize(store) { - return { - direction: store.direction, - }; - }, - }, - ), - ), -); diff --git a/common/client/src/stores/useTopBarStore.ts b/common/client/src/stores/useTopBarStore.ts deleted file mode 100644 index ee1076629..000000000 --- a/common/client/src/stores/useTopBarStore.ts +++ /dev/null @@ -1,33 +0,0 @@ -import create from 'zustand'; -import { devtools } from 'zustand/middleware'; -import { StoreBase } from '.'; - -export interface TopBarStore extends StoreBase { - title?: string; - backwardsUrl?: string | number; - forwardsUrl?: string | number; - - setTitle(title?: string): void; - setBackwardsUrl(backwardsUrl?: string | number): void; - setForwardsUrl(forwardsUrl?: string | number): void; -} - -export const useTopBarStore = create()( - devtools((set) => ({ - setTitle(title) { - set((store) => ({ ...store, title })); - }, - setBackwardsUrl(backwardsUrl) { - set((store) => ({ ...store, backwardsUrl })); - }, - setForwardsUrl(forwardsUrl) { - set((store) => ({ ...store, forwardsUrl })); - }, - reset() { - set(() => ({})); - }, - set(changes) { - set((state) => ({ ...state, ...changes })); - }, - })), -); diff --git a/common/client/src/types/core.ts b/common/client/src/types/core.ts deleted file mode 100644 index c4dad4ad1..000000000 --- a/common/client/src/types/core.ts +++ /dev/null @@ -1,259 +0,0 @@ -// DO NOT MODIFY THIS FILE, IT IS AUTOGENERATED - -export interface StumpVersion { - semver: string; - rev: string | null; - compile_time: string; -} - -export interface User { - id: string; - username: string; - role: string; - user_preferences: UserPreferences | null; -} - -export type UserRole = 'SERVER_OWNER' | 'MEMBER'; - -export interface UserPreferences { - id: string; - locale: string; - library_layout_mode: string; - series_layout_mode: string; - collection_layout_mode: string; -} - -export interface UserPreferencesUpdate { - id: string; - locale: string; - library_layout_mode: string; - series_layout_mode: string; - collection_layout_mode: string; -} - -export interface LoginOrRegisterArgs { - username: string; - password: string; -} - -export interface ClaimResponse { - is_claimed: boolean; -} - -export type FileStatus = 'UNKNOWN' | 'READY' | 'UNSUPPORTED' | 'ERROR' | 'MISSING'; - -export interface Library { - id: string; - name: string; - description: string | null; - path: string; - status: string; - updated_at: string; - series: Array | null; - tags: Array | null; - library_options: LibraryOptions; -} - -export type LibraryPattern = 'SERIES_BASED' | 'COLLECTION_BASED'; - -export type LibraryScanMode = 'SYNC' | 'BATCHED' | 'NONE'; - -export interface LibraryOptions { - id: string | null; - convert_rar_to_zip: boolean; - hard_delete_conversions: boolean; - create_webp_thumbnails: boolean; - library_pattern: LibraryPattern; - library_id: string | null; -} - -export interface CreateLibraryArgs { - name: string; - path: string; - description: string | null; - tags: Array | null; - scan_mode: LibraryScanMode | null; - library_options: LibraryOptions | null; -} - -export interface UpdateLibraryArgs { - id: string; - name: string; - path: string; - description: string | null; - tags: Array | null; - removed_tags: Array | null; - library_options: LibraryOptions; - scan_mode: LibraryScanMode | null; -} - -export interface LibrariesStats { - series_count: bigint; - book_count: bigint; - total_bytes: bigint; -} - -export interface Series { - id: string; - name: string; - path: string; - description: string | null; - status: FileStatus; - updated_at: string; - library_id: string; - library: Library | null; - media: Array | null; - media_count: bigint | null; - tags: Array | null; -} - -export interface Media { - id: string; - name: string; - description: string | null; - size: number; - extension: string; - pages: number; - updated_at: string; - checksum: string | null; - path: string; - status: FileStatus; - series_id: string; - series: Series | null; - read_progresses: Array | null; - current_page: number | null; - tags: Array | null; -} - -export interface MediaMetadata { - Series: string | null; - Number: number | null; - Web: string | null; - Summary: string | null; - Publisher: string | null; - Genre: string | null; - PageCount: number | null; -} - -export interface ReadProgress { - id: string; - page: number; - media_id: string; - media: Media | null; - user_id: string; - user: User | null; -} - -export interface Tag { - id: string; - name: string; -} - -export type LayoutMode = 'GRID' | 'LIST'; - -export interface Epub { - media_entity: Media; - spine: Array; - resources: Record; - toc: Array; - metadata: Record>; - root_base: string; - root_file: string; - extra_css: Array; -} - -export interface EpubContent { - label: string; - content: string; - play_order: number; -} - -export type JobStatus = 'RUNNING' | 'QUEUED' | 'COMPLETED' | 'CANCELLED' | 'FAILED'; - -export interface JobUpdate { - runner_id: string; - current_task: bigint | null; - task_count: bigint; - message: string | null; - status: JobStatus | null; -} - -export interface JobReport { - id: string | null; - kind: string; - details: string | null; - status: JobStatus; - task_count: number | null; - completed_task_count: number | null; - ms_elapsed: bigint | null; - completed_at: string | null; -} - -export type CoreEvent = - | { key: 'JobStarted'; data: JobUpdate } - | { key: 'JobProgress'; data: JobUpdate } - | { key: 'JobComplete'; data: string } - | { key: 'JobFailed'; data: { runner_id: string; message: string } } - | { key: 'CreateEntityFailed'; data: { runner_id: string | null; path: string; message: string } } - | { key: 'CreatedMedia'; data: Media } - | { key: 'CreatedMediaBatch'; data: bigint } - | { key: 'CreatedSeries'; data: Series } - | { key: 'CreatedSeriesBatch'; data: bigint }; - -export interface DirectoryListing { - parent: string | null; - files: Array; -} - -export interface DirectoryListingFile { - is_directory: boolean; - name: string; - path: string; -} - -export interface DirectoryListingInput { - path: string | null; -} - -export interface Log { - id: string; - level: LogLevel; - message: string; - created_at: string; - job_id: string | null; -} - -export interface LogMetadata { - path: string; - size: bigint; - modified: string; -} - -export type LogLevel = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG'; - -export type Direction = 'asc' | 'desc'; - -export interface PageParams { - zero_based: boolean; - page: number; - page_size: number; - order_by: string; - direction: Direction; -} - -export interface PagedRequestParams { - unpaged: boolean | null; - zero_based: boolean | null; - page: number | null; - page_size: number | null; - order_by: string | null; - direction: Direction | null; -} - -export interface PageInfo { - total_pages: number; - current_page: number; - page_size: number; - page_offset: number; - zero_based: boolean; -} diff --git a/common/client/tsconfig.json b/common/client/tsconfig.json deleted file mode 100644 index 09622e0cc..000000000 --- a/common/client/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../config/base.tsconfig.json", - "compilerOptions": { - "skipLibCheck": true - }, - "include": ["src"], - "exclude": ["node_modules"] -} diff --git a/common/config/base.tsconfig.json b/common/config/base.tsconfig.json deleted file mode 100644 index 693a5eeca..000000000 --- a/common/config/base.tsconfig.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "display": "Default", - "compilerOptions": { - "lib": [ - "DOM", - "DOM.Iterable", - "ESNext" - ], - "declaration": false, - "noEmit": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "inlineSources": false, - "isolatedModules": true, - "module": "ESNext", - "target": "ESNext", - "moduleResolution": "node", - "noUnusedLocals": false, - "noUnusedParameters": false, - "preserveWatchOutput": true, - "skipLibCheck": false, - "strict": true, - "allowSyntheticDefaultImports": true, - "resolveJsonModule": true, - "jsx": "react-jsx", - "paths": { - "@stump/client": [ - "../../common/client" - ], - "@stump/client/api": [ - "../../common/client/src/api" - ], - "@stump/interface": [ - "../../common/interface" - ] - } - }, - "exclude": [ - "node_modules" - ] -} \ No newline at end of file diff --git a/common/config/package.json b/common/config/package.json deleted file mode 100644 index f3ff5ae52..000000000 --- a/common/config/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@stump/config", - "version": "0.0.0", - "devDependencies": { - "tailwind-scrollbar-hide": "^1.1.7" - } -} \ No newline at end of file diff --git a/common/interface/package.json b/common/interface/package.json deleted file mode 100644 index 94941e48f..000000000 --- a/common/interface/package.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "name": "@stump/interface", - "version": "0.0.0", - "description": "", - "license": "MIT", - "private": true, - "main": "src/index.ts", - "exports": { - ".": "./src/index.ts", - "./assets/*": "./src/assets/*", - "./styles": "./src/styles/index.css", - "./components/*": "./src/components/*" - }, - "scripts": { - "check": "tsc --noEmit" - }, - "dependencies": { - "@chakra-ui/react": "^2.3.5", - "@emotion/react": "^11.10.4", - "@emotion/styled": "^11.10.4", - "@hookform/resolvers": "^2.9.8", - "@stump/client": "workspace:*", - "@tanstack/react-query": "^4.10.3", - "@tanstack/react-query-devtools": "^4.10.4", - "@tanstack/react-table": "^8.5.15", - "chakra-react-select": "^4.2.5", - "clsx": "^1.2.1", - "epubjs": "^0.3.93", - "framer-motion": "^7.5.3", - "i18next": "^21.10.0", - "immer": "^9.0.15", - "nprogress": "^0.2.0", - "phosphor-react": "^1.4.1", - "pluralize": "^8.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-error-boundary": "^3.1.4", - "react-helmet": "^6.1.0", - "react-hook-form": "^7.37.0", - "react-hot-toast": "^2.4.0", - "react-hotkeys-hook": "^3.4.7", - "react-i18next": "^11.18.6", - "react-router": "^6.4.2", - "react-router-dom": "^6.4.2", - "react-swipeable": "^7.0.0", - "rooks": "^7.4.0", - "use-count-up": "^3.0.1", - "zod": "^3.19.1", - "zustand": "^4.1.1" - }, - "devDependencies": { - "@types/node": "^18.8.3", - "@types/nprogress": "^0.2.0", - "@types/pluralize": "^0.0.29", - "@types/react": "^18.0.21", - "@types/react-dom": "^18.0.6", - "@types/react-helmet": "^6.1.5", - "@types/react-router-dom": "^5.3.3", - "@vitejs/plugin-react": "^2.1.0", - "typescript": "^4.8.4", - "vite": "^3.1.6" - }, - "pnpm": { - "peerDependencyRules": { - "ignoreMissing": [ - "@babel/core" - ] - } - } -} \ No newline at end of file diff --git a/common/interface/src/App.tsx b/common/interface/src/App.tsx deleted file mode 100644 index a2bf072e6..000000000 --- a/common/interface/src/App.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useEffect, useState } from 'react'; -import { ErrorBoundary } from 'react-error-boundary'; -import { Helmet } from 'react-helmet'; -import { BrowserRouter } from 'react-router-dom'; - -import { ChakraProvider } from '@chakra-ui/react'; -import { - AppProps, - AppPropsContext, - JobContextProvider, - queryClient, - useStumpStore, - useTopBarStore, -} from '@stump/client'; -import { initializeApi } from '@stump/client/api'; -import { defaultContext, QueryClientProvider } from '@tanstack/react-query'; -import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; - -import { AppRouter } from './AppRouter'; -import { chakraTheme } from './chakra'; -import { ErrorFallback } from './components/ErrorFallback'; - -import Notifications from './components/Notifications'; - -function RouterContainer(props: { appProps: AppProps }) { - const [mounted, setMounted] = useState(false); - const [appProps, setAppProps] = useState(props.appProps); - - const { baseUrl, setBaseUrl } = useStumpStore(); - const { setTitle } = useTopBarStore(); - - useEffect(() => { - if (!baseUrl && appProps.baseUrl) { - setBaseUrl(appProps.baseUrl); - } else if (baseUrl) { - initializeApi(baseUrl); - - setAppProps((appProps) => ({ - ...appProps, - baseUrl, - })); - } - - setMounted(true); - }, [baseUrl]); - - function handleHelmetChange(newState: any, _: any, __: any) { - if (Array.isArray(newState?.title) && newState.title.length > 0) { - if (newState.title.length > 1) { - setTitle(newState.title[newState.title.length - 1]); - } else { - setTitle(newState.title[0]); - } - } else if (typeof newState?.title === 'string') { - if (newState.title === 'Stump') { - setTitle(''); - } else { - setTitle(newState.title); - } - } - } - - if (!mounted) { - // TODO: suspend - return null; - } - - return ( - - - Stump - - - - - - - - ); -} - -export default function StumpInterface(props: AppProps) { - return ( - - - - {import.meta.env.MODE === 'development' && ( - - )} - - - - - - - ); -} diff --git a/common/interface/src/AppLayout.tsx b/common/interface/src/AppLayout.tsx deleted file mode 100644 index 1450aedf4..000000000 --- a/common/interface/src/AppLayout.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useMemo } from 'react'; -import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom'; - -import { Box, Flex, useColorModeValue } from '@chakra-ui/react'; -import { useAppProps, useAuthQuery, useCoreEventHandler, useUserStore } from '@stump/client'; - -import Lazy from './components/Lazy'; -import Sidebar from './components/sidebar/Sidebar'; -import JobOverlay from './components/jobs/JobOverlay'; -import TopBar from './components/topbar/TopBar'; -import CommandPalette from './components/CommandPalette'; -import { useHotkeys } from 'react-hotkeys-hook'; -import ServerStatusOverlay from './components/ServerStatusOverlay'; - -export function AppLayout() { - const appProps = useAppProps(); - - const navigate = useNavigate(); - const location = useLocation(); - - const hideSidebar = useMemo(() => { - // hide sidebar when on /books/:id/pages/:page or /epub/ - // TODO: replace with single regex, I am lazy rn - return ( - location.pathname.match(/\/books\/.+\/pages\/.+/) || location.pathname.match(/\/epub\/.+/) - ); - }, [location]); - - useCoreEventHandler(); - - const { user: storeUser, setUser } = useUserStore(); - - // TODO: platform specific hotkeys - // TODO: cmd+shift+h for home - useHotkeys('ctrl+,, cmd+,', (e) => { - e.preventDefault(); - navigate('/settings/general'); - }); - - // TODO: This logic needs to be moved, pretty much every request in Stump should have this - // functionality. I have no idea how to do this in a clean way right now though. - // On network error, if on desktop app, navigate to a screen to troubleshoot - // the connection to the server - // FIXME: after switching to SSE again, this seems to break desktop app... kinda annoying bug. - const { user, isLoading, error } = useAuthQuery({ - onSuccess: setUser, - enabled: !storeUser, - }); - - // @ts-ignore: FIXME: type error no good >:( - if (error?.code === 'ERR_NETWORK' && appProps?.platform !== 'browser') { - return ; - } - - const hasUser = !!user || !!storeUser; - - if (!hasUser && !isLoading) { - return ; - } - - return ( - Loading...}> - - { - // TODO: uncomment once I add custom menu on Tauri side - // if (appProps?.platform != 'browser') { - // e.preventDefault(); - // return false; - // } - - return true; - }} - > - {!hideSidebar && } - - {!hideSidebar && } - }> - - - - - - {appProps?.platform !== 'browser' && } - {!location.pathname.match(/\/settings\/jobs/) && } - - ); -} diff --git a/common/interface/src/AppRouter.tsx b/common/interface/src/AppRouter.tsx deleted file mode 100644 index 1121e54c4..000000000 --- a/common/interface/src/AppRouter.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import { Navigate } from 'react-router'; -import { Route, Routes } from 'react-router-dom'; - -import { useAppProps } from '@stump/client'; - -import { AppLayout } from './AppLayout'; - -const Home = React.lazy(() => import('./pages/Home')); -const LibraryOverview = React.lazy(() => import('./pages/library/LibraryOverview')); -const LibraryFileExplorer = React.lazy(() => import('./pages/library/LibraryFileExplorer')); -const SeriesOverview = React.lazy(() => import('./pages/SeriesOverview')); -const BookOverview = React.lazy(() => import('./pages/book/BookOverview')); -const ReadBook = React.lazy(() => import('./pages/book/ReadBook')); -const ReadEpub = React.lazy(() => import('./pages/book/ReadEpub')); -const SettingsLayout = React.lazy(() => import('./components/settings/SettingsLayout')); -const GeneralSettings = React.lazy(() => import('./pages/settings/GeneralSettings')); -const JobSettings = React.lazy(() => import('./pages/settings/JobSettings')); -const ServerSettings = React.lazy(() => import('./pages/settings/ServerSettings')); -const UserSettings = React.lazy(() => import('./pages/settings/UserSettings')); -const FourOhFour = React.lazy(() => import('./pages/FourOhFour')); -const ServerConnectionError = React.lazy(() => import('./pages/ServerConnectionError')); -const LoginOrClaim = React.lazy(() => import('./pages/LoginOrClaim')); -const OnBoarding = React.lazy(() => import('./pages/OnBoarding')); - -function OnBoardingRouter() { - return ( - - - } /> - - - ); -} - -export function AppRouter() { - const appProps = useAppProps(); - - if (!appProps?.baseUrl) { - if (appProps?.platform === 'browser') { - throw new Error('Base URL is not set'); - } - - return ; - } - - return ( - - }> - } /> - - } /> - } /> - - } /> - - } /> - } /> - } /> - - }> - } /> - } /> - } /> - } /> - } /> - {appProps?.platform !== 'browser' && Desktop!} />} - - - - } /> - {appProps?.platform !== 'browser' && ( - } /> - )} - } /> - - ); -} diff --git a/common/interface/src/components/Card.tsx b/common/interface/src/components/Card.tsx deleted file mode 100644 index 4ab2ccf89..000000000 --- a/common/interface/src/components/Card.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import clsx from 'clsx'; -import { FileX } from 'phosphor-react'; -import { useMemo } from 'react'; -import { Link } from 'react-router-dom'; - -import { Box, Spacer, Text, useBoolean, useColorModeValue } from '@chakra-ui/react'; - -export interface CardProps { - to: string; - imageAlt: string; - imageSrc: string; - imageFallback?: string; - title: string; - subtitle?: string; - variant?: 'default' | 'large'; - showMissingOverlay?: boolean; - onMouseEnter?: () => void; -} - -// FIXME: onError should behave differently to accomodate new cards that get rendered when new Series/Media -// are created during a scan. When a Series is created, there won't be any Media to render a thumbnail for at first. -// So, I think maybe there should be some retry logic in here? retry once every few ms for like 9ms before showing a -// fallback image? -export default function Card({ - to, - imageAlt, - imageSrc, - imageFallback, - title, - subtitle, - variant = 'default', - showMissingOverlay, - onMouseEnter, -}: CardProps) { - const [isFallback, { on }] = useBoolean(false); - - const src = useMemo(() => { - if (isFallback || showMissingOverlay) { - return imageFallback ?? '/fallbacks/image-file.svg'; - } - - return imageSrc; - }, [isFallback, showMissingOverlay]); - - return ( - - {showMissingOverlay && ( - // FIXME: this has terrible UX, not very readable. very ugly lmfao - - - - Missing! - - - )} - - {imageAlt} { - on(); - }} - /> - - - {variant === 'default' && ( - - - {title} - - - - - - {subtitle} - - - )} - - ); -} diff --git a/common/interface/src/components/Lazy.tsx b/common/interface/src/components/Lazy.tsx deleted file mode 100644 index 0b0a42a80..000000000 --- a/common/interface/src/components/Lazy.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import nprogress from 'nprogress'; -import { useEffect } from 'react'; - -export default function Lazy() { - useEffect(() => { - // @ts-ignore: FIXME: - let timeout: NodeJS.Timeout; - // loader doesn't need to start immediately, if it only takes 100ms to load i'd rather - // not show it at all than a quick flash - timeout = setTimeout(() => nprogress.start(), 100); - - return () => { - clearTimeout(timeout); - nprogress.done(); - }; - }); - - return null; -} diff --git a/common/interface/src/components/jobs/JobsTable.tsx b/common/interface/src/components/jobs/JobsTable.tsx deleted file mode 100644 index 09a908f92..000000000 --- a/common/interface/src/components/jobs/JobsTable.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { ColumnDef, getCoreRowModel, getPaginationRowModel } from '@tanstack/react-table'; -import { useMemo } from 'react'; -import { JobReport, JobStatus, useJobReport } from '@stump/client'; -import { formatJobStatus, readableKind } from './utils'; -import Table from '../../ui/table/Table'; - -// kind: string; -// details: string | null; -// status: JobStatus; -// task_count: number | null; -// completed_task_count: number | null; -// ms_elapsed: bigint | null; -// completed_at: string | null; - -export default function JobsTable() { - const { isLoading, jobReports } = useJobReport(); - - // TODO: mobile columns less? or maybe scroll? idk what would be best UX - const columns = useMemo[]>( - () => [ - { - id: 'jobHistory', - columns: [ - { - accessorKey: 'id', - header: 'Job ID', - cell: (info) => info.getValue(), - footer: (props) => props.column.id, - }, - { - accessorKey: 'kind', - header: 'Type', - cell: (info) => readableKind(info.getValue()), - footer: (props) => props.column.id, - }, - { - accessorKey: 'status', - header: 'Status', - // change value to all lowercase except for first letter - cell: (info) => formatJobStatus(info.getValue()), - footer: (props) => props.column.id, - }, - ], - }, - ], - [], - ); - - return ( - - ); -} diff --git a/common/interface/src/components/library/LibraryOptionsMenu.tsx b/common/interface/src/components/library/LibraryOptionsMenu.tsx deleted file mode 100644 index ead018d37..000000000 --- a/common/interface/src/components/library/LibraryOptionsMenu.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@chakra-ui/react'; -import { ArrowsClockwise, Binoculars, DotsThreeVertical } from 'phosphor-react'; -import EditLibraryModal from './EditLibraryModal'; -import DeleteLibraryModal from './DeleteLibraryModal'; -import type { Library } from '@stump/client'; -import { useNavigate } from 'react-router-dom'; -import { queryClient, useScanLibrary, useUserStore } from '@stump/client'; - -interface Props { - library: Library; -} - -export default function LibraryOptionsMenu({ library }: Props) { - const navigate = useNavigate(); - - const { user } = useUserStore(); - - const { scanAsync } = useScanLibrary(); - - function handleScan() { - // extra protection, should not be possible to reach this. - if (user?.role !== 'SERVER_OWNER') { - throw new Error('You do not have permission to scan libraries.'); - } - - // The UI will receive updates from SSE in fractions of ms lol and it can get bogged down. - // So, add a slight delay so the close animation of the menu can finish cleanly. - setTimeout(async () => { - await scanAsync(library.id); - await queryClient.invalidateQueries(['getJobReports']); - }, 50); - } - - // FIXME: so, disabled on the MenuItem doesn't seem to actually work... how cute. - return ( - // TODO: https://chakra-ui.com/docs/theming/customize-theme#customizing-component-styles - - - - - {/* TODO: scanMode */} - } - onClick={handleScan} - > - Scan - - - } - onClick={() => navigate(`libraries/${library.id}/explorer`)} - > - File Explorer - - - - - - ); -} diff --git a/common/interface/src/components/media/MediaCard.tsx b/common/interface/src/components/media/MediaCard.tsx deleted file mode 100644 index 7aa56b4bd..000000000 --- a/common/interface/src/components/media/MediaCard.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useMemo } from 'react'; - -import type { Media } from '@stump/client'; -import { prefetchMedia } from '@stump/client'; -import { getMediaThumbnail } from '@stump/client/api'; - -import Card from '../Card'; - -export default function MediaCard(media: Media) { - const fallback = useMemo(() => { - return '/fallbacks/image-file.svg'; - }, [media.extension]); - - return ( - prefetchMedia(media.id)} - title={media.name} - showMissingOverlay={media.status === 'MISSING'} - /> - ); -} diff --git a/common/interface/src/components/media/MediaGrid.tsx b/common/interface/src/components/media/MediaGrid.tsx deleted file mode 100644 index ccdc2fcaa..000000000 --- a/common/interface/src/components/media/MediaGrid.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Heading } from '@chakra-ui/react'; -import type { Media } from '@stump/client'; -import MediaCard from './MediaCard'; - -interface Props { - isLoading: boolean; - media?: Media[]; -} - -export default function MediaGrid({ media, isLoading }: Props) { - if (isLoading) { - return
Loading...
; - } else if (!media || !media.length) { - return ( -
- {/* TODO: If I take in pageData, I can determine if it is an out of bounds issue or if the series truly has - no media. */} - It doesn't look like there is any media here. -
- ); - } - - return ( -
- {media.map((s) => ( - - ))} -
- ); -} diff --git a/common/interface/src/components/readers/LazyEpubReader.tsx b/common/interface/src/components/readers/LazyEpubReader.tsx deleted file mode 100644 index 74fd120a6..000000000 --- a/common/interface/src/components/readers/LazyEpubReader.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import { Book, Rendition } from 'epubjs'; -import { useEffect, useMemo, useRef, useState } from 'react'; -import toast from 'react-hot-toast'; -import { useSwipeable } from 'react-swipeable'; - -import { useColorMode } from '@chakra-ui/react'; -import { useEpubLazy } from '@stump/client'; -import { API } from '@stump/client/api'; - -import { epubDarkTheme } from '../../utils/epubTheme'; -import EpubControls from './utils/EpubControls'; - -// Color manipulation reference: https://github.com/futurepress/epub.js/issues/1019 - -/** - -looks like epubcfi generates the first two elements of the cfi like /6/{(index+1) * 2} (indexing non-zero based): - - index 1 /6/2, index=2 /6/4, index=3 /6/8 etc. - -can't figure out rest yet -> https://www.heliconbooks.com/?id=blog&postid=EPUB3Links -*/ - -interface LazyEpubReaderProps { - id: string; - loc: string | null; -} - -// TODO: https://github.com/FormidableLabs/react-swipeable#how-to-share-ref-from-useswipeable - -export default function LazyEpubReader({ id, loc }: LazyEpubReaderProps) { - const { colorMode } = useColorMode(); - - const ref = useRef(null); - - const [book, setBook] = useState(null); - const [rendition, setRendition] = useState(null); - - const [location, setLocation] = useState({ epubcfi: loc }); - const [chapter, setChapter] = useState(''); - const [fontSize, setFontSize] = useState(13); - - const { epub, isLoading } = useEpubLazy(id); - - // TODO: type me - function handleLocationChange(changeState: any) { - const start = changeState?.start; - - if (!start) { - return; - } - - const newChapter = controls.getChapter(start.href); - - if (newChapter) { - setChapter(newChapter); - } - - setLocation({ - // @ts-ignore: types are wrong >:( - epubcfi: start.cfi ?? null, - // @ts-ignore: types are wrong >:( - page: start.displayed?.page, - // @ts-ignore: types are wrong >:( - total: start.displayed?.total, - href: start.href, - index: start.index, - }); - } - - useEffect(() => { - if (!ref.current) return; - - if (!book) { - setBook( - new Book(`${API.getUri()}/media/${id}/file`, { - openAs: 'epub', - // @ts-ignore: more incorrect types >:( I really truly cannot stress enough how much I want to just - // rip out my eyes working with epubjs... - requestCredentials: true, - }), - ); - } - }, [ref]); - - // Note: not sure this is possible anymore? epub.js isn't maintained it seems, - // and I haven't figured this out yet. - function pageAnimation(iframeView: any, _rendition: Rendition) { - // console.log('pageAnimation', { iframeView, _rendition }); - // window.setTimeout(() => { - // console.log('in pageAnimation timeout'); - // }, 100); - } - - useEffect(() => { - if (!book) return; - if (!ref.current) return; - - book.ready.then(() => { - if (book.spine) { - const defaultLoc = book.rendition?.location?.start?.cfi; - - const rendition_ = book.renderTo(ref.current!, { - width: '100%', - height: '100%', - }); - - // TODO more styles, probably separate this out - rendition_.themes.register('dark', epubDarkTheme); - - // book.spine.hooks.serialize // Section is being converted to text - // book.spine.hooks.content // Section has been loaded and parsed - // rendition.hooks.render // Section is rendered to the screen - // rendition.hooks.content // Section contents have been loaded - // rendition.hooks.unloaded // Section contents are being unloaded - rendition_.hooks.render.register(pageAnimation); - - rendition_.on('relocated', handleLocationChange); - - if (colorMode === 'dark') { - rendition_.themes.select('dark'); - } - - rendition_.themes.fontSize('13px'); - - setRendition(rendition_); - - // Note: this *does* work, returns epubcfi. I might consider this... - // console.log(book.spine.get('chapter001.xhtml')); - - if (location?.epubcfi) { - rendition_.display(location.epubcfi); - } else if (defaultLoc) { - rendition_.display(defaultLoc); - } else { - rendition_.display(); - } - } - }); - }, [book]); - - useEffect(() => { - if (!rendition) { - return; - } - - if (colorMode === 'dark') { - rendition.themes.select('dark'); - } else { - rendition.themes.select('default'); - } - }, [rendition, colorMode]); - - // epubcfi(/6/10!/4/2/2[Chapter1]/48/1:0) - - // I hate this... - const controls = useMemo( - () => ({ - async next() { - if (rendition) { - await rendition - .next() - .then(() => { - // rendition.hooks.render.trigger(pageAnimation); - }) - .catch((err) => { - console.error(err); - toast.error('Something went wrong!'); - }); - } - }, - - async prev() { - if (rendition) { - await rendition.prev().catch((err) => { - console.error(err); - toast.error('Something went wrong!'); - }); - } - }, - - // FIXME: make async? I just need to programmatically detect failures so - // I don't close the TOC drawer. - goTo(href: string) { - if (!book || !rendition || !ref.current) { - return; - } - - let adjusted = href.split('#')[0]; - - let match = book.spine.get(adjusted); - - if (!match) { - // @ts-ignore: types are wrong >:( - // Note: epubjs it literally terrible and this should be classified as torture dealing - // with this terrible library. The fact that I have to do this really blows my mind. - let matches = book.spine.items - .filter((item: any) => { - const withPrefix = `/${adjusted}`; - return ( - item.url === adjusted || - item.canonical == adjusted || - item.url === withPrefix || - item.canonical === withPrefix - ); - }) - .map((item: any) => book.spine.get(item.index)) - .filter(Boolean); - - if (matches.length > 0) { - match = matches[0]; - } else { - console.error(`Could not find ${href}`); - return; - } - } - - const epubcfi = match.cfiFromElement(ref.current); - - if (epubcfi) { - rendition.display(epubcfi); - } else { - toast.error('Could not generate a valid epubcfi.'); - } - }, - - // Note: some books have entries in the spine for each href, some don't. This means for some - // books the chapter will be null after the first page of that chapter. This function is - // used to get the current chapter, which will only work, in some cases, on the first page - // of the chapter. The chapter state will only get updated when this function returns a non-null - // value. - getChapter(href: string): string | null { - if (book) { - const filteredToc = book.navigation.toc.filter((toc) => toc.href === href); - - return filteredToc[0]?.label.trim() ?? null; - } - - return null; - }, - - changeFontSize(size: number) { - if (rendition) { - setFontSize(size); - rendition.themes.fontSize(`${size}px`); - } - }, - }), - [rendition, book, ref], - ); - - const swipeHandlers = useSwipeable({ - onSwipedRight: controls.prev, - onSwipedLeft: controls.next, - preventScrollOnSwipe: true, - }); - - if (isLoading) { - return
Loading TODO.....
; - } - - return ( - -
- - ); -} diff --git a/common/interface/src/components/readers/utils/Toolbar.tsx b/common/interface/src/components/readers/utils/Toolbar.tsx deleted file mode 100644 index 2ca7b07a3..000000000 --- a/common/interface/src/components/readers/utils/Toolbar.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { AnimatePresence, motion } from 'framer-motion'; -import { ArrowLeft } from 'phosphor-react'; -import { Link, useParams } from 'react-router-dom'; - -import { Heading } from '@chakra-ui/react'; -import { getMediaPage } from '@stump/client/api'; - -interface ToolbarProps { - title: string; - currentPage: number; - pages: number; - visible: boolean; - onPageChange(page: number): void; -} - -export default function Toolbar({ - title, - currentPage, - pages, - visible, - onPageChange, -}: ToolbarProps) { - const { id } = useParams(); - - if (!id) { - // should never happen - throw new Error('woah boy how strange 0.o'); - } - - return ( - - {visible && ( - <> - -
-
- - - - - {title} -
-
TODO: idk what
-
-
- -
- {/* TODO: don't do this, terrible loading for most people */} - {/* TODO: scroll to center current page */} - {/* TODO: tool tips? */} - {/* FIXME: styling isn't quite right, should have space on either side... */} - {Array.from({ length: pages }).map((_, i) => ( - onPageChange(i + 1)} - /> - ))} -
-
- - )} -
- ); -} diff --git a/common/interface/src/components/series/SeriesCard.tsx b/common/interface/src/components/series/SeriesCard.tsx deleted file mode 100644 index 99b24832b..000000000 --- a/common/interface/src/components/series/SeriesCard.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { prefetchSeries } from '@stump/client'; -import { getSeriesThumbnail } from '@stump/client/api'; - -import pluralizeStat from '../../utils/pluralize'; -import Card from '../Card'; - -import type { Series } from '@stump/client'; -export default function SeriesCard(series: Series) { - const bookCount = series.media ? series.media.length : series.media_count ?? 0; - - return ( - prefetchSeries(series.id)} - title={series.name} - subtitle={pluralizeStat('book', Number(bookCount))} - showMissingOverlay={series.status === 'MISSING'} - /> - ); -} diff --git a/common/interface/src/components/series/SeriesGrid.tsx b/common/interface/src/components/series/SeriesGrid.tsx deleted file mode 100644 index 456fb2e57..000000000 --- a/common/interface/src/components/series/SeriesGrid.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Heading } from '@chakra-ui/react'; - -import SeriesCard from './SeriesCard'; - -import type { Series } from '@stump/client'; -interface Props { - isLoading: boolean; - series?: Series[]; -} - -// TODO: I think this *might* need a redesign... Not sure, gotta do some UX research about this -export default function SeriesGrid({ series, isLoading }: Props) { - if (isLoading) { - return
Loading...
; - } else if (!series || !series.length) { - return ( -
- {/* TODO: If I take in pageData, I can determine if it is an out of bounds issue or if the series truly has - no media. */} - It doesn't look like there are any series here. -
- ); - } - - return ( -
- {series.map((s) => ( - - ))} -
- ); -} diff --git a/common/interface/src/components/series/UpNextButton.tsx b/common/interface/src/components/series/UpNextButton.tsx deleted file mode 100644 index 8f7670631..000000000 --- a/common/interface/src/components/series/UpNextButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Link } from 'react-router-dom'; -import Button from '../../ui/Button'; - -import { useUpNextInSeries } from '@stump/client'; - -interface Props { - seriesId: string; -} - -export default function UpNextButton({ seriesId }: Props) { - const { media, isLoading } = useUpNextInSeries(seriesId); - // TODO: Change this once Stump supports epub progress tracking. - if (media?.extension === 'epub') { - return null; - } - - return ( - - ); -} diff --git a/common/interface/src/components/settings/ServerStats.tsx b/common/interface/src/components/settings/ServerStats.tsx deleted file mode 100644 index bd5e72896..000000000 --- a/common/interface/src/components/settings/ServerStats.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Box, Text } from '@chakra-ui/react'; -import toast from 'react-hot-toast'; - -export function LogStats() { - // const { data: logMeta } = useQuery(['getLogFileMeta'], () => - // getLogFileMeta().then((res) => res.data), - // ); - // const { mutateAsync } = useMutation(['clearStumpLogs'], clearLogFile); - // function handleClearLogs() { - // toast - // .promise(mutateAsync(), { - // loading: 'Clearing...', - // success: 'Cleared logs!', - // error: 'Error clearing logs.', - // }) - // .then(() => client.invalidateQueries(['getLogFileMeta'])); - // } - // return ( - // - // {formatBytes(logMeta?.size)} - // - // - // ); -} - -export default function ServerStats() { - return
{/* */}
; -} diff --git a/common/interface/src/components/settings/SettingsLayout.tsx b/common/interface/src/components/settings/SettingsLayout.tsx deleted file mode 100644 index 76e026c47..000000000 --- a/common/interface/src/components/settings/SettingsLayout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Box, useColorModeValue, VStack } from '@chakra-ui/react'; -import SettingsNavigation from './SettingsNavigation'; -import { Outlet } from 'react-router-dom'; - -export default function SettingsLayout() { - return ( - - - - - - - ); -} diff --git a/common/interface/src/hooks/useGetPage.ts b/common/interface/src/hooks/useGetPage.ts deleted file mode 100644 index 1ad726348..000000000 --- a/common/interface/src/hooks/useGetPage.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useMemo } from 'react'; -import { useSearchParams } from 'react-router-dom'; - -export function useGetPage() { - const [search, setSearchParams] = useSearchParams(); - - const page = useMemo(() => { - const searchPage = search.get('page'); - - if (searchPage) { - return parseInt(searchPage, 10); - } - - return 1; - }, [search]); - - function setPage(page: number) { - search.set('page', page.toString()); - setSearchParams(search); - } - - return { page, setPage }; -} diff --git a/common/interface/src/hooks/useIsInView.ts b/common/interface/src/hooks/useIsInView.ts deleted file mode 100644 index 924bff20f..000000000 --- a/common/interface/src/hooks/useIsInView.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -export default function useIsInView( - rootMargin = '0px', -): [React.MutableRefObject, boolean] { - const ref = useRef(); - - const [isIntersecting, setIntersecting] = useState(false); - - useEffect(() => { - const observer = new IntersectionObserver(([entry]) => setIntersecting(entry.isIntersecting), { - rootMargin, - }); - - if (ref.current) { - observer.observe(ref.current); - } - return () => { - observer.disconnect(); - }; - }, []); - - return [ref, isIntersecting]; -} diff --git a/common/interface/src/hooks/useLocale.ts b/common/interface/src/hooks/useLocale.ts deleted file mode 100644 index dd2132d05..000000000 --- a/common/interface/src/hooks/useLocale.ts +++ /dev/null @@ -1,37 +0,0 @@ -import '../i18n/config'; -import { useTranslation } from 'react-i18next'; -import { useUserStore } from '@stump/client'; - -export enum Locale { - English = 'en', - French = 'fr', -} - -export function useLocale() { - // TODO: update DB on changes - const { userPreferences, setUserPreferences } = useUserStore(); - - function setLocaleFromStr(localeStr: string) { - let locale = localeStr as Locale; - - if (userPreferences && locale) { - setUserPreferences({ ...userPreferences, locale }); - } - } - - function setLocale(locale: Locale) { - if (userPreferences && locale) { - setUserPreferences({ ...userPreferences, locale }); - } - } - - const locale: string = userPreferences?.locale || 'en'; - - const { t } = useTranslation(locale); - - const locales = Object.keys(Locale) - .map((key) => ({ label: key, value: Locale[key as keyof typeof Locale] })) - .filter((option) => typeof option.value === 'string'); - - return { locale, setLocale, setLocaleFromStr, t, locales }; -} diff --git a/common/interface/src/i18n/locales/en.json b/common/interface/src/i18n/locales/en.json deleted file mode 100644 index d7c947465..000000000 --- a/common/interface/src/i18n/locales/en.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "loginPage": { - "claimText": "This Stump server is not yet claimed, you can use the form below to create a user account and claim it. Enter your preferred username and password.", - "form": { - "validation": { - "missingUsername": "Username is required", - "missingPassword": "Password is required" - }, - "labels": { - "username": "Username", - "password": "Password" - }, - "buttons": { - "createAccount": "Create Account", - "login": "Login" - } - }, - "toasts": { - "loggingIn": "Logging in...", - "loggedIn": "Welcome back! Redirecting...", - "loggedInFirstTime": "Welcome! Redirecting...", - "registering": "Registering...", - "registered": "Registered!", - "loginFailed": "Login failed. Please try again.", - "registrationFailed": "Registration failed. Please try again." - } - }, - "signOutModal": { - "title": "Sign out", - "message": "Are you sure you want to sign out?", - "buttons": { - "cancel": "Cancel", - "signOut": "Sign out" - } - }, - "settingsPage": { - "navigation": { - "generalSettings": "General Settings", - "serverSettings": "Server Settings", - "jobHistory": "Job History" - }, - "general": { - "profileForm": { - "validation": {}, - "labels": { - "username": "Username", - "password": "Password" - }, - "buttons": {} - } - } - }, - "sidebar": { - "buttons": { - "home": "Home", - "libraries": "Libraries", - "settings": "Settings" - } - }, - "libraryModalForm": { - "createLibraryHeading": "Create a new library", - "labels": { - "libraryName": "Library Name", - "libraryPath": "Library Path", - "libraryDescription": "Library Description", - "libraryTags": "Library Tags" - }, - "buttons": { - "cancel": "Cancel", - "editLibrary": "Save Changes", - "createLibrary": "Create Library" - } - }, - "search": { - "placeholder": "Search" - } -} \ No newline at end of file diff --git a/common/interface/src/i18n/locales/fr.json b/common/interface/src/i18n/locales/fr.json deleted file mode 100644 index 742bdd70b..000000000 --- a/common/interface/src/i18n/locales/fr.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "loginPage": { - "claimText": "Ce serveur Stump n'est pas encore réclamé, vous pouvez utiliser le formulaire ci-dessous pour créer un compte utilisateur et le réclamer. Entrez votre nom d'utilisateur et votre mot de passe préférés.", - "form": { - "validation": { - "missingUsername": "Nom d'utilisateur est nécessaire", - "missingPassword": "Mot de passe est nécessaire" - }, - "labels": { - "username": "Nom d'utilisateur", - "password": "Mot de passe" - }, - "buttons": { - "createAccount": "Créer un Compte", - "login": "Connexion" - } - }, - "toasts": { - "loggingIn": "Se connecter...", - "loggedIn": "Content de te revoir! Redirection...", - "loggedInFirstTime": "Accueillir! Redirection...", - "registering": "La création du compte...", - "registered": "Compte créé!", - "loginFailed": "Échec de la connexion. Veuillez réessayer.", - "registrationFailed": "La création du compte a échoué. Veuillez réessayer." - } - } -} \ No newline at end of file diff --git a/common/interface/src/index.ts b/common/interface/src/index.ts deleted file mode 100644 index 34330317e..000000000 --- a/common/interface/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import StumpInterface from './App'; - -export default StumpInterface; diff --git a/common/interface/src/pages/FourOhFour.tsx b/common/interface/src/pages/FourOhFour.tsx deleted file mode 100644 index 1d4ed4bb0..000000000 --- a/common/interface/src/pages/FourOhFour.tsx +++ /dev/null @@ -1,5 +0,0 @@ -// TODO: design page, don't be lazy and throw an error lmao -export default function FourOhFour() { - throw new Error("404, what you're looking for doesn't exist!"); - return
404
; -} diff --git a/common/interface/src/pages/Home.tsx b/common/interface/src/pages/Home.tsx deleted file mode 100644 index 4aa37059f..000000000 --- a/common/interface/src/pages/Home.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Box } from '@chakra-ui/react'; -import { Helmet } from 'react-helmet'; -import { useLibraries } from '@stump/client'; -import LibrariesStats from '../components/library/LibrariesStats'; -import NoLibraries from '../components/library/NoLibraries'; -// import { useDidMount } from 'rooks'; - -// TODO: account for new accounts, i.e. no media at all -export default function Home() { - const { libraries, isLoading } = useLibraries(); - - // const { setBackwardsUrl } = useTopBarStore(); - - // FIXME: NO - // useDidMount(() => { - // setBackwardsUrl(0); - // }); - - const helmet = ( - - {/* Doing this so Helmet splits the title into an array, I'm not just insane lol */} - Stump | {'Home'} - - ); - - if (isLoading) { - return null; - } - - if (!libraries?.length) { - return ( - <> - {helmet} - - - ); - } - - return ( - <> - {helmet} - - {/* */} - {/* */} - - - - ); -} diff --git a/common/interface/src/pages/SeriesOverview.tsx b/common/interface/src/pages/SeriesOverview.tsx deleted file mode 100644 index 983839f00..000000000 --- a/common/interface/src/pages/SeriesOverview.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { useEffect } from 'react'; -import { Helmet } from 'react-helmet'; -import { useParams } from 'react-router-dom'; - -import { Box, ButtonGroup, Heading, Spacer } from '@chakra-ui/react'; -import { useLayoutMode, useSeries, useSeriesMedia, useTopBarStore } from '@stump/client'; -import { getSeriesThumbnail } from '@stump/client/api'; - -import MediaGrid from '../components/media/MediaGrid'; -import MediaList from '../components/media/MediaList'; -import DownloadSeriesButton from '../components/series/DownloadSeriesButton'; -import UpNextButton from '../components/series/UpNextButton'; -import { useGetPage } from '../hooks/useGetPage'; -import useIsInView from '../hooks/useIsInView'; -import Pagination from '../ui/Pagination'; -import ReadMore from '../ui/ReadMore'; - -import type { Series } from '@stump/client'; - -interface OverviewTitleSectionProps { - isVisible: boolean; - series: Series; -} - -function OverviewTitleSection({ isVisible, series }: OverviewTitleSectionProps) { - if (!isVisible) { - return null; - } - - return ( -
-
- - - - - -
-
- - {series.name} - - - - - - - -
-
- ); -} - -export default function SeriesOverview() { - const [containerRef, isInView] = useIsInView(); - - const { id } = useParams(); - const { page } = useGetPage(); - - if (!id) { - throw new Error('Series id is required'); - } - - const { layoutMode } = useLayoutMode('SERIES'); - const { setBackwardsUrl } = useTopBarStore(); - - const { series, isLoading: isLoadingSeries } = useSeries(id); - - const { isLoading: isLoadingMedia, media, pageData } = useSeriesMedia(id, page); - - useEffect(() => { - if (!isInView) { - containerRef.current?.scrollIntoView({ - block: 'nearest', - inline: 'start', - }); - } - }, [pageData?.current_page]); - - useEffect(() => { - if (series?.library) { - setBackwardsUrl(`/libraries/${series.library.id}`); - } - - return () => { - setBackwardsUrl(); - }; - }, [series?.library?.id]); - - // FIXME: ugly - if (isLoadingSeries) { - return
Loading...
; - } else if (!series) { - throw new Error('Series not found'); - } - - return ( -
- - Stump | {series.name} - - - - - {/* @ts-ignore */} -
-
- - {layoutMode === 'GRID' ? ( - - ) : ( - - )} - - {/* FIXME: spacing when empty */} - - - -
-
- ); -} diff --git a/common/interface/src/pages/book/BookOverview.tsx b/common/interface/src/pages/book/BookOverview.tsx deleted file mode 100644 index 031ba4d44..000000000 --- a/common/interface/src/pages/book/BookOverview.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { useEffect, useMemo } from 'react'; -import { Helmet } from 'react-helmet'; -import { useParams } from 'react-router-dom'; - -import { useMedia, useTopBarStore } from '@stump/client'; -import { getMediaPage, getMediaThumbnail } from '@stump/client/api'; - -import Card from '../../components/Card'; - -export default function BookOverview() { - const { id } = useParams(); - - if (!id) { - throw new Error('Book id is required for this route.'); - } - - const { media, isLoading } = useMedia(id); - const { setBackwardsUrl } = useTopBarStore(); - - useEffect(() => { - if (media?.series) { - setBackwardsUrl(`/libraries/${media.series.id}`); - } - - return () => { - setBackwardsUrl(); - }; - }, [media?.series?.id]); - - const fallback = useMemo(() => { - return '/fallbacks/image-file.svg'; - }, [media?.extension]); - - function prefetchCurrentPage() { - if (!media) { - return; - } - - const currentPage = media.current_page ?? 1; - - const img = new Image(); - img.src = getMediaPage(media.id, currentPage); - } - - if (isLoading) { - return
Loading...
; - } else if (!media) { - throw new Error('Media not found'); - } - - return ( - <> - - Stump | {media.name ?? ''} - -
- -
- - ); -} diff --git a/common/interface/src/pages/book/ReadEpub.tsx b/common/interface/src/pages/book/ReadEpub.tsx deleted file mode 100644 index 0102bb373..000000000 --- a/common/interface/src/pages/book/ReadEpub.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useEpub } from '@stump/client'; -import { Navigate, useParams, useSearchParams } from 'react-router-dom'; -import EpubReader from '../../components/readers/EpubReader'; -import LazyEpubReader from '../../components/readers/LazyEpubReader'; - -export default function ReadEpub() { - const { id } = useParams(); - - const [search] = useSearchParams(); - - const loc = search.get('loc'); - - if (!id) { - throw new Error('Media id is required'); - } else if (search.get('stream') && search.get('stream') !== 'true') { - // TODO: remove the loc from search.. - return ; - } - - const { isFetchingBook, epub, ...rest } = useEpub(id, { loc }); - - if (isFetchingBook) { - return
Loading...
; - } - - if (!epub) { - throw new Error('Epub not found'); - } - - if (!epub.media_entity.extension.match(/epub/)) { - return ; - } - - // else if (!loc) { - // return ; - // } - - return ; -} diff --git a/common/interface/src/pages/library/LibraryOverview.tsx b/common/interface/src/pages/library/LibraryOverview.tsx deleted file mode 100644 index dc848a6a0..000000000 --- a/common/interface/src/pages/library/LibraryOverview.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useEffect } from 'react'; -import { Helmet } from 'react-helmet'; -import { useParams } from 'react-router-dom'; - -import { Spacer } from '@chakra-ui/react'; -import { useLayoutMode, useLibrary, useLibrarySeries } from '@stump/client'; - -import SeriesGrid from '../../components/series/SeriesGrid'; -import SeriesList from '../../components/series/SeriesList'; -import useIsInView from '../../hooks/useIsInView'; -import Pagination from '../../ui/Pagination'; -import { useGetPage } from '../../hooks/useGetPage'; - -export default function LibraryOverview() { - const [containerRef, isInView] = useIsInView(); - - const { id } = useParams(); - const { page } = useGetPage(); - - if (!id) { - throw new Error('Library id is required'); - } - - function handleError(err: unknown) { - console.error(err); - } - - const { isLoading, library } = useLibrary(id, { onError: handleError }); - - const { isLoading: isLoadingSeries, series, pageData } = useLibrarySeries(id, page); - - useEffect(() => { - if (!isInView) { - containerRef.current?.scrollIntoView(); - } - }, [pageData?.current_page]); - - if (isLoading) { - return null; - } else if (!library) { - throw new Error('Library not found'); - } - - const { layoutMode } = useLayoutMode('LIBRARY'); - - return ( - <> - - Stump | {library.name} - - - {/* @ts-ignore */} -
- -
- - - {layoutMode === 'GRID' ? ( - - ) : ( - - )} - - - - -
- - ); -} diff --git a/common/interface/src/pages/settings/GeneralSettings.tsx b/common/interface/src/pages/settings/GeneralSettings.tsx deleted file mode 100644 index 07229bbb1..000000000 --- a/common/interface/src/pages/settings/GeneralSettings.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Helmet } from 'react-helmet'; -// import PreferencesForm from '~components/Settings/General/PreferencesForm'; -// import ProfileForm from '~components/Settings/General/ProfileForm'; - -export default function GeneralSettings() { - return ( - <> - - {/* Doing this so Helmet splits the title into an array, I'm not just insane lol */} - Stump | {'General Settings'} - - -
I am not implemented yet
- - {/* */} - - {/* */} - - ); -} diff --git a/common/interface/src/pages/settings/UserSettings.tsx b/common/interface/src/pages/settings/UserSettings.tsx deleted file mode 100644 index edc186471..000000000 --- a/common/interface/src/pages/settings/UserSettings.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export default function UserSettings() { - return
I am not implemented yet
; -} diff --git a/common/interface/src/ui/Link.tsx b/common/interface/src/ui/Link.tsx deleted file mode 100644 index 88e1237d5..000000000 --- a/common/interface/src/ui/Link.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import clsx from 'clsx'; -import { ArrowSquareOut } from 'phosphor-react'; -import { Link as RouterLink, LinkProps as RouterLinkProps } from 'react-router-dom'; - -export interface LinkProps extends RouterLinkProps { - isExternal?: boolean; - noUnderline?: boolean; -} - -export default function Link({ isExternal, noUnderline, ...props }: LinkProps) { - const { children, className, title, ...rest } = props; - - return ( - - {children} - - {isExternal && } - - ); -} diff --git a/common/interface/src/ui/ReadMore.tsx b/common/interface/src/ui/ReadMore.tsx deleted file mode 100644 index cc7fe1ae5..000000000 --- a/common/interface/src/ui/ReadMore.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useMemo } from 'react'; -import { Text, TextProps, useBoolean } from '@chakra-ui/react'; - -interface Props extends Omit { - text?: string; -} - -// FIXME: does not render new lines properly, this is pretty basic and needs changing. -export default function ReadMore({ text, ...props }: Props) { - const [showingAll, { toggle }] = useBoolean(false); - - const canReadMore = useMemo(() => (text ?? '').length > 250, [text]); - - if (!text) { - return null; - } - - if (!canReadMore) { - return {text}; - } - - return ( - - {showingAll ? text : text.slice(0, 250)} - - {showingAll ? ' Read less' : '... Read more'} - - - ); -} diff --git a/common/interface/src/ui/table/Table.tsx b/common/interface/src/ui/table/Table.tsx deleted file mode 100644 index 643df157e..000000000 --- a/common/interface/src/ui/table/Table.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Box, useColorModeValue } from '@chakra-ui/react'; -import { ColumnDef, flexRender, TableOptions, useReactTable } from '@tanstack/react-table'; -import clsx from 'clsx'; -import TablePagination from './Pagination'; - -export interface TableProps { - data: T[]; - columns: ColumnDef[]; - options: Omit, 'data' | 'columns'>; - fullWidth?: boolean; -} - -export default function Table({ data, columns, options, ...props }: TableProps) { - const table = useReactTable({ - ...options, - data, - columns, - state: { - ...options.state, - }, - }); - - const { pageSize, pageIndex } = table.getState().pagination; - - const pageCount = table.getPageCount(); - const lastIndex = (pageIndex + 1) * pageSize; - const firstIndex = lastIndex - (pageSize - 1); - - return ( - -
- - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - ); - })} - - ))} - - - {table.getRowModel().rows.map((row) => { - return ( - - {row.getVisibleCells().map((cell) => { - return ( - - ); - })} - - ); - })} - -
-
{flexRender(header.column.columnDef.header, header.getContext())}
-
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
-
-
-
- -
- Showing {firstIndex} to {lastIndex} -
- of {table.getPageCount() * pageSize} -
- - -
- - table.setPageIndex(page)} - /> -
- - ); -} diff --git a/common/interface/src/utils/patterns.ts b/common/interface/src/utils/patterns.ts deleted file mode 100644 index 3aac00085..000000000 --- a/common/interface/src/utils/patterns.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const ARCHIVE_EXTENSION = /cbr|cbz|zip|rar/; -// export const EBOOK_EXTENSION = /epub|mobi/; -export const EBOOK_EXTENSION = /epub/; diff --git a/common/interface/tsconfig.json b/common/interface/tsconfig.json deleted file mode 100644 index dce390ea8..000000000 --- a/common/interface/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../config/base.tsconfig.json", - "compilerOptions": { - "types": ["vite/client", "node"], - "outDir": "./dist", - "skipLibCheck": true - }, - "include": ["src"] -} diff --git a/core/Cargo.toml b/core/Cargo.toml index ced05521a..7c3fe5346 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "stump_core" -version.workspace = true +version = { workspace = true } edition = "2021" [dependencies] -tokio.workspace = true -serde.workspace = true -prisma-client-rust.workspace = true -specta.workspace = true +tokio = { workspace = true } +serde = { workspace = true } +prisma-client-rust = { workspace = true } +specta = { workspace = true } rayon = "1.5.3" futures = "0.3.21" @@ -19,6 +19,8 @@ cuid = "1.2.0" xml-rs = "0.8.4" # used for creating XML docs serde-xml-rs = "0.5.1" # used for serializing/deserializing xml itertools = "0.10.5" +optional_struct = "0.2.0" +utoipa = { version = "3.0.3" } ### FILESYSTEM UTILS ### walkdir = "2.3.2" @@ -29,16 +31,16 @@ infer = "0.7.0" image = "0.24.2" webp = "0.2.2" zip = "0.5.13" -epub = "1.2.3" +epub = "1.2.4" unrar = { git = "https://github.com/aaronleopold/unrar.rs", branch = "aleopold--read-bytes" } data-encoding = "2.3.2" # include_dir = "0.7.2" ring = "0.16.20" ## ERROR HANDLING + LOGGING ### -thiserror.workspace = true +thiserror = { workspace = true } # figment = { version = "0.10.6", features = ["toml", "env"] } toml = "0.5.9" -tracing.workspace = true +tracing = { workspace = true } tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } tracing-appender = "0.2.2" \ No newline at end of file diff --git a/core/integration-tests/Cargo.toml b/core/integration-tests/Cargo.toml index 45736240f..7c078c5cf 100644 --- a/core/integration-tests/Cargo.toml +++ b/core/integration-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "integration-tests" -version.workspace = true +version = { workspace = true } autotests = false autobenches = false edition = "2021" @@ -11,8 +11,8 @@ path = "tests/lib.rs" harness = true [dev-dependencies] -serde.workspace = true -prisma-client-rust.workspace = true -tokio.workspace = true +serde = { workspace = true } +prisma-client-rust = { workspace = true } +tokio = { workspace = true } stump_core = { path = ".." } tempfile = "3.3.0" \ No newline at end of file diff --git a/core/integration-tests/tests/epub.rs b/core/integration-tests/tests/epub.rs index 098d3aa47..980f8a9b5 100644 --- a/core/integration-tests/tests/epub.rs +++ b/core/integration-tests/tests/epub.rs @@ -3,12 +3,12 @@ use std::path::PathBuf; use crate::utils::{init_test, TempLibrary}; use stump_core::{ - config::Ctx, + db::models::Epub, fs::media_file::epub::{ get_epub_chapter, get_epub_resource, normalize_resource_path, }, + prelude::{ContentType, CoreResult, Ctx}, prisma::media, - types::{models::epub::Epub, ContentType, CoreResult}, }; #[tokio::test] @@ -108,7 +108,7 @@ async fn can_get_chapter() -> CoreResult<()> { let get_chapter_result = get_chapter_result.unwrap(); - assert!(get_chapter_result.1.len() > 0); + assert!(!get_chapter_result.1.is_empty()); Ok(()) } diff --git a/core/integration-tests/tests/rar.rs b/core/integration-tests/tests/rar.rs index c2dceb813..9390850ac 100644 --- a/core/integration-tests/tests/rar.rs +++ b/core/integration-tests/tests/rar.rs @@ -1,13 +1,13 @@ use crate::utils::{init_test, make_tmp_file, TempLibrary}; use stump_core::{ - config::Ctx, + db::models::{LibraryPattern, LibraryScanMode}, fs::{ checksum, - media_file::rar::{convert_rar_to_zip, rar_sample}, + media_file::rar::{convert_to_zip, sample_size}, }, + prelude::{CoreResult, Ctx}, prisma::media, - types::{CoreResult, LibraryPattern, LibraryScanMode}, }; // TODO: fix these tests... @@ -21,7 +21,7 @@ fn test_rar_to_zip() -> CoreResult<()> { let path = tmp_file.path(); - let result = convert_rar_to_zip(path); + let result = convert_to_zip(path); assert!(result.is_ok()); let zip_path = result.unwrap(); @@ -60,13 +60,13 @@ async fn digest_rars_synchronous() -> CoreResult<()> { // assert_ne!(rars.len(), 0); // TODO: remove this check once I create rar test data - if rars.len() == 0 { + if rars.is_empty() { println!("STINKY: could not run digest_rars_synchronous test until aaron fixes his stuff"); return Ok(()); } for rar in rars { - let rar_sample_result = rar_sample(&rar.path); + let rar_sample_result = sample_size(&rar.path); assert!(rar_sample_result.is_ok()); let rar_sample = rar_sample_result.unwrap(); diff --git a/core/integration-tests/tests/scanner.rs b/core/integration-tests/tests/scanner.rs index 5097e9e6c..706af9e30 100644 --- a/core/integration-tests/tests/scanner.rs +++ b/core/integration-tests/tests/scanner.rs @@ -1,9 +1,9 @@ use crate::utils::{init_test, run_test_scan, TempLibrary}; use stump_core::{ - config::Ctx, + db::models::{LibraryPattern, LibraryScanMode}, + prelude::{CoreResult, Ctx}, prisma::{library, PrismaClient}, - types::{CoreResult, LibraryPattern, LibraryScanMode}, }; async fn check_library_post_scan( @@ -32,12 +32,11 @@ async fn check_library_post_scan( let library_series = library.series; assert_eq!(library_series.len(), series_count); - let library_media = library_series + let library_media_count = library_series .into_iter() - .map(|series| series.media) - .flatten() - .collect::>(); - assert_eq!(library_media.len(), media_count); + .flat_map(|series| series.media) + .count(); + assert_eq!(library_media_count, media_count); Ok(()) } @@ -111,7 +110,7 @@ async fn massive_library_batch_scan() -> CoreResult<()> { let client = ctx.get_db(); let temp_library = TempLibrary::massive_library(10000, LibraryPattern::SeriesBased)?; - let (library, _options) = temp_library.insert(&client, LibraryScanMode::None).await?; + let (library, _options) = temp_library.insert(client, LibraryScanMode::None).await?; let scan_result = run_test_scan(&ctx, &library, LibraryScanMode::Batched).await; assert!( diff --git a/core/integration-tests/tests/utils.rs b/core/integration-tests/tests/utils.rs index 5eca17129..b6f1266ab 100644 --- a/core/integration-tests/tests/utils.rs +++ b/core/integration-tests/tests/utils.rs @@ -4,12 +4,14 @@ use std::{fs, path::PathBuf}; use tempfile::{Builder, NamedTempFile, TempDir}; use stump_core::{ - config::Ctx, - db::migration::run_migrations, - fs::scanner::library_scanner::{scan_batch, scan_sync}, + db::{ + migration::run_migrations, + models::{LibraryPattern, LibraryScanMode}, + }, + fs::scanner::scan, job::{persist_new_job, runner::RunnerCtx, LibraryScanJob}, + prelude::{CoreResult, Ctx}, prisma::{library, library_options, PrismaClient}, - types::{CoreResult, LibraryPattern, LibraryScanMode}, }; // https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/second-edition/ch11-03-test-organization.html @@ -219,7 +221,7 @@ pub async fn init_db() { // TODO: once migration engine is built into pcr, replace with commented out code below // client._db_push().await.expect("Failed to push database schema"); - let migration_result = run_migrations(&client).await; + let migration_result = run_migrations(client).await; assert!( migration_result.is_ok(), @@ -269,7 +271,7 @@ pub fn get_test_data_dir() -> PathBuf { pub fn get_test_file_contents(name: &str) -> Vec { let path = get_test_data_dir().join(name); - fs::read(path).expect(format!("Failed to read test file: {}", name).as_str()) + fs::read(path).unwrap_or_else(|_| panic!("Failed to read test file: {}", name)) } pub async fn persist_test_job( @@ -294,19 +296,21 @@ pub async fn run_test_scan( library: &library::Data, scan_mode: LibraryScanMode, ) -> CoreResult { - persist_test_job(&library.id, &ctx, &library, scan_mode).await?; + persist_test_job(&library.id, ctx, library, scan_mode).await?; let fake_runner_ctx = RunnerCtx::new(ctx.get_ctx(), library.id.clone()); if scan_mode == LibraryScanMode::None { return Ok(0); - } else if scan_mode == LibraryScanMode::Batched { - return scan_batch(fake_runner_ctx, library.path.clone(), library.id.clone()) - .await; - } else { - return scan_sync(fake_runner_ctx, library.path.clone(), library.id.clone()) - .await; } + + scan( + fake_runner_ctx, + library.path.clone(), + library.id.clone(), + scan_mode, + ) + .await } /// Creates a library with the given name, path, and pattern. If the scan mode is diff --git a/core/integration-tests/tests/zip.rs b/core/integration-tests/tests/zip.rs index 0671758e4..600bb7cc2 100644 --- a/core/integration-tests/tests/zip.rs +++ b/core/integration-tests/tests/zip.rs @@ -1,8 +1,8 @@ use stump_core::{ - config::Ctx, - fs::{checksum, media_file::zip::zip_sample}, + db::models::{LibraryPattern, LibraryScanMode}, + fs::{checksum, media_file::zip}, + prelude::{CoreResult, Ctx}, prisma::media, - types::{CoreResult, LibraryPattern, LibraryScanMode}, }; use crate::utils::{init_test, TempLibrary}; @@ -33,7 +33,7 @@ async fn digest_zips() -> CoreResult<()> { assert_ne!(zips.len(), 0); for zip in zips { - let zip_sample = zip_sample(&zip.path); + let zip_sample = zip::sample_size(&zip.path); let digest_result = checksum::digest(&zip.path, zip_sample); assert!(digest_result.is_ok()); diff --git a/core/moon.yml b/core/moon.yml new file mode 100644 index 000000000..da96d9dbe --- /dev/null +++ b/core/moon.yml @@ -0,0 +1,37 @@ +type: 'application' + +workspace: + inheritedTasks: + exclude: ['buildPackage'] + +fileGroups: + app: + - 'src/**/*' + +language: 'rust' + +tasks: + test: + command: 'cargo test --all-targets' + + prisma-generate: + command: 'cargo prisma generate' + + codegen: + command: 'cargo test --package stump_core --lib -- types::tests::codegen --ignored' + deps: + - '~:prisma-generate' + + lint: + command: 'cargo clippy --package stump_core -- -D warnings' + options: + mergeArgs: 'replace' + mergeDeps: 'replace' + mergeInputs: 'replace' + + format: + command: 'cargo fmt --package stump_core' + options: + mergeArgs: 'replace' + mergeDeps: 'replace' + mergeInputs: 'replace' diff --git a/core/package.json b/core/package.json index 2f816a642..446dff56a 100644 --- a/core/package.json +++ b/core/package.json @@ -7,7 +7,6 @@ "scripts": { "prisma": "cargo prisma", "setup": "cargo prisma generate", - "check": "cargo check", "build": "cargo build --release", "fmt": "cargo fmt --all --manifest-path=./Cargo.toml --", "typegen": "cargo test --package stump_core --lib -- types::tests::codegen --ignored", diff --git a/core/prisma/Cargo.toml b/core/prisma/Cargo.toml deleted file mode 100644 index f9bdd4ba0..000000000 --- a/core/prisma/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "prisma" -version = "0.1.0" -rust-version = "1.64.0" -edition = "2021" - -[dependencies] -# prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.1", features = [ -# 'rspc', -# # 'sqlite-create-many', -# # "migrations", -# # "sqlite", -# ] } -prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "79ab6bd700199b92103711f01d4df42e4cef62a6", features = [ - 'rspc', - # 'sqlite-create-many', - "migrations", - "sqlite", -], default-features = false } \ No newline at end of file diff --git a/core/prisma/migrations/20220920020355_lets_try_again/migration.sql b/core/prisma/migrations/20221231022841_lets_start_again_again/migration.sql similarity index 90% rename from core/prisma/migrations/20220920020355_lets_try_again/migration.sql rename to core/prisma/migrations/20221231022841_lets_start_again_again/migration.sql index 36698c996..68f053748 100644 --- a/core/prisma/migrations/20220920020355_lets_try_again/migration.sql +++ b/core/prisma/migrations/20221231022841_lets_start_again_again/migration.sql @@ -36,6 +36,7 @@ CREATE TABLE "series" ( "name" TEXT NOT NULL, "description" TEXT, "updated_at" DATETIME NOT NULL, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "path" TEXT NOT NULL, "status" TEXT NOT NULL DEFAULT 'READY', "library_id" TEXT, @@ -51,12 +52,16 @@ CREATE TABLE "media" ( "extension" TEXT NOT NULL, "pages" INTEGER NOT NULL, "updated_at" DATETIME NOT NULL, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "modified_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "downloaded" BOOLEAN NOT NULL DEFAULT false, "checksum" TEXT, "path" TEXT NOT NULL, "status" TEXT NOT NULL DEFAULT 'READY', "series_id" TEXT, - CONSTRAINT "media_series_id_fkey" FOREIGN KEY ("series_id") REFERENCES "series" ("id") ON DELETE CASCADE ON UPDATE CASCADE + "reading_list_id" TEXT, + CONSTRAINT "media_series_id_fkey" FOREIGN KEY ("series_id") REFERENCES "series" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "media_reading_list_id_fkey" FOREIGN KEY ("reading_list_id") REFERENCES "reading_lists" ("id") ON DELETE SET NULL ON UPDATE CASCADE ); -- CreateTable @@ -75,15 +80,6 @@ CREATE TABLE "reading_lists" ( CONSTRAINT "reading_lists_creating_user_id_fkey" FOREIGN KEY ("creating_user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE ); --- CreateTable -CREATE TABLE "reading_list_access" ( - "id" TEXT NOT NULL PRIMARY KEY, - "user_id" TEXT NOT NULL, - "reading_list_id" TEXT NOT NULL, - CONSTRAINT "reading_list_access_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT "reading_list_access_reading_list_id_fkey" FOREIGN KEY ("reading_list_id") REFERENCES "reading_lists" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); - -- CreateTable CREATE TABLE "collections" ( "id" TEXT NOT NULL PRIMARY KEY, @@ -97,6 +93,8 @@ CREATE TABLE "read_progresses" ( "id" TEXT NOT NULL PRIMARY KEY, "page" INTEGER NOT NULL, "epubcfi" TEXT, + "is_completed" BOOLEAN NOT NULL DEFAULT false, + "updated_at" DATETIME NOT NULL, "media_id" TEXT NOT NULL, "user_id" TEXT NOT NULL, CONSTRAINT "read_progresses_media_id_fkey" FOREIGN KEY ("media_id") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE CASCADE, @@ -184,9 +182,6 @@ CREATE UNIQUE INDEX "tags_name_key" ON "tags"("name"); -- CreateIndex CREATE UNIQUE INDEX "reading_lists_creating_user_id_name_key" ON "reading_lists"("creating_user_id", "name"); --- CreateIndex -CREATE UNIQUE INDEX "reading_list_access_user_id_reading_list_id_key" ON "reading_list_access"("user_id", "reading_list_id"); - -- CreateIndex CREATE UNIQUE INDEX "read_progresses_user_id_media_id_key" ON "read_progresses"("user_id", "media_id"); diff --git a/core/prisma/migrations/migrations.sql b/core/prisma/migrations/migrations.sql deleted file mode 100644 index 06419574b..000000000 --- a/core/prisma/migrations/migrations.sql +++ /dev/null @@ -1,11 +0,0 @@ --- CreateTable -CREATE TABLE "migrations" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "name" TEXT NOT NULL, - "checksum" TEXT NOT NULL, - "success" BOOLEAN NOT NULL DEFAULT false, - "applied_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -); - --- CreateIndex -CREATE UNIQUE INDEX "migrations_checksum_key" ON "migrations"("checksum"); \ No newline at end of file diff --git a/core/prisma/schema.prisma b/core/prisma/schema.prisma index cbcaf4e4d..4a57e705c 100644 --- a/core/prisma/schema.prisma +++ b/core/prisma/schema.prisma @@ -38,9 +38,7 @@ model User { // The role of the user. Defaults to "MEMBER". role String @default("MEMBER") // The media the user currently has progress on. - // TODO: don't love this name but wanted to emphasize plural. I could do something like - // `currentlyReading` but that wouldn't match the relation in the Media model. UGH. Naming - // is so hard lol. + // TODO: rename to `reading_history` read_progresses ReadProgress[] reading_lists ReadingList[] @@ -83,7 +81,7 @@ model LibraryOptions { id String @id @default(uuid()) // Flag indicating whether or not to attempt to convert rar files to zip on scans. convert_rar_to_zip Boolean @default(false) - // Flag indicating whether or not to *hard* delete rar files that were sucessfully converted to zip. + // Flag indicating whether or not to *hard* delete rar files that were successfully converted to zip. // Hard delete **will not be recoverable**. When false, converted files will be moved to the systems native // trash location, or a custom location when running in Docker (which will clear on an interval). hard_delete_conversions Boolean @default(false) @@ -109,6 +107,8 @@ model Series { description String? // The date in which the series was last updated in the FS. ex: "2020-01-01" updated_at DateTime @updatedAt + // The date in which the series was created. ex: "2020-01-01" + created_at DateTime @default(now()) // The url of the series. ex: "/home/user/media/comics/The Amazing Spider-Man" path String // The status of the series since last scan or access @@ -139,6 +139,10 @@ model Media { pages Int // The date in which the media was last updated. ex: "2022-04-20 04:20:69" updated_at DateTime @updatedAt + // The date in which the media was last updated. ex: "2022-04-20 04:20:69" + created_at DateTime @default(now()) + // The date in which the file was last modified. Defaults to now() for safety. + modified_at DateTime @default(now()) // Whether or not the media is downloaded to the client. ex: true downloaded Boolean @default(false) // The checksum hash of the file contents. Used to find multuple instances of a file in the database @@ -152,13 +156,13 @@ model Media { // The id of the series this media belongs to. series_id String? // The read progresses of the media - read_progresses ReadProgress[] // TODO: don't love this name but wanted to emphasize plural + read_progresses ReadProgress[] // TODO: rename to `reading_history` // The user assigned tags for the media. ex: ["Spider-Man", "Marvel"] tags Tag[] // readingList ReadingList? @relation(fields: [readingListId], references: [id]) // readingListId String? - reading_list ReadingList? @relation(fields: [reading_list_id], references: [id]) + reading_list ReadingList? @relation(fields: [reading_list_id], references: [id]) reading_list_id String? @@map("media") @@ -239,6 +243,9 @@ model ReadProgress { epubcfi String? + is_completed Boolean @default(false) + updated_at DateTime @updatedAt + media_id String media Media @relation(fields: [media_id], references: [id], onDelete: Cascade) @@ -290,10 +297,6 @@ model Log { @@map("logs") } -// TODO: I think I want to remove the view modes from database persisted preferences. -// Instead, I'll have a sessionStorage/localStorage store just for those, I think -// it might be silly to ping the database everytime this gets updated is mainly -// the reason. Not sure though... model UserPreferences { id String @id @default(cuid()) // Flag to indicate if the user wants to reduce some of the animations when using the build-in client @@ -306,9 +309,8 @@ model UserPreferences { collection_layout_mode String @default("GRID") // The locale the user has selected. E.g. 'en' or 'fr'. Default is 'en' locale String @default("en") - - // user User[] - user User? + // The user which these preferences belong to + user User? @@map("user_preferences") } @@ -318,8 +320,6 @@ model ServerPreferences { // // Flag indicating whether or not to attempt to rename scanned series according to a ComicInfo.xml file inside the directory. // // If none found, the series name will be the directory name. // rename_series Boolean @default(false) - // // Flag indicating whether or not to attempt to convert .cbr files to .cbz files on scan automatically. - // convertCbrToCbz Boolean @default(false) @@map("server_preferences") } diff --git a/core/src/config/env.rs b/core/src/config/env.rs index 2fb895f34..7d1c995a2 100644 --- a/core/src/config/env.rs +++ b/core/src/config/env.rs @@ -5,7 +5,7 @@ use tracing::debug; use crate::{ config::get_config_dir, - types::{errors::CoreError, CoreResult}, + prelude::{errors::CoreError, CoreResult}, }; /// [`StumpEnvironment`] is the the representation of the Stump configuration file. @@ -156,7 +156,7 @@ impl StumpEnvironment { let port = &self.port.unwrap_or(10801); env::set_var("STUMP_PORT", port.to_string()); - env::set_var("STUMP_VERBOSITY", &self.verbosity.unwrap_or(1).to_string()); + env::set_var("STUMP_VERBOSITY", self.verbosity.unwrap_or(1).to_string()); if let Some(config_dir) = &self.config_dir { if !config_dir.is_empty() { diff --git a/core/src/config/logging.rs b/core/src/config/logging.rs index 1930fd811..743556ca0 100644 --- a/core/src/config/logging.rs +++ b/core/src/config/logging.rs @@ -12,6 +12,7 @@ pub const STUMP_SHADOW_TEXT: &str = include_str!("stump_shadow_text.txt"); pub fn get_log_file() -> PathBuf { get_config_dir().join("Stump.log") } + pub fn get_log_verbosity() -> u64 { match std::env::var("STUMP_VERBOSITY") { Ok(s) => s.parse::().unwrap_or(1), @@ -19,13 +20,14 @@ pub fn get_log_verbosity() -> u64 { } } +// TODO: allow for overriding of format /// Initializes the logging system, which uses the [tracing] crate. Logs are written to /// both the console and a file in the config directory. The file is called `Stump.log` /// by default. pub fn init_tracing() { let config_dir = get_config_dir(); - let file_appender = tracing_appender::rolling::never(&config_dir, "Stump.log"); + let file_appender = tracing_appender::rolling::never(config_dir, "Stump.log"); let verbosity = get_log_verbosity(); let max_level = match verbosity { @@ -42,12 +44,22 @@ pub fn init_tracing() { .add_directive( "stump_core=trace" .parse() - .expect("Error invalid tracing directive!"), + .expect("Error invalid tracing directive for stump_core!"), ) .add_directive( "stump_server=trace" .parse() - .expect("Error invalid tracing directive!"), + .expect("Error invalid tracing directive for stump_server!"), + ) + .add_directive( + "tower_http=debug" + .parse() + .expect("Error invalid tracing directive for tower_http!"), + ) + .add_directive( + "quaint::connector::metrics=debug" + .parse() + .expect("Failed to parse tracing directive for quaint!"), ), ) // Note: I have two layers here, separating the file appender and the stdout. diff --git a/core/src/config/mod.rs b/core/src/config/mod.rs index 45fa2d4e9..c643a77d1 100644 --- a/core/src/config/mod.rs +++ b/core/src/config/mod.rs @@ -1,10 +1,10 @@ use std::path::{Path, PathBuf}; -pub(crate) mod context; -pub use context::Ctx; - pub mod env; pub mod logging; +mod stump_config; + +pub use stump_config::*; /// Gets the home directory of the system running Stump fn home() -> PathBuf { diff --git a/core/src/config/stump_config.rs b/core/src/config/stump_config.rs new file mode 100644 index 000000000..907047ae6 --- /dev/null +++ b/core/src/config/stump_config.rs @@ -0,0 +1,209 @@ +use std::{env, path::Path}; + +use optional_struct::OptionalStruct; +use serde::{Deserialize, Serialize}; +use tracing::debug; + +use crate::{ + config::get_config_dir, + prelude::{errors::CoreError, CoreResult}, +}; + +// TODO: before I actually use this, test to see if it works well. + +#[derive(Serialize, Deserialize, Debug, Clone, OptionalStruct)] +#[optional_name = "PartialStumpConfig"] +#[optional_derive(Deserialize)] +pub struct StumpConfig { + pub profile: String, + pub port: u16, + pub verbosity: u64, + pub client_dir: String, + pub config_dir: String, + pub allowed_origins: Option>, +} + +impl Default for StumpConfig { + fn default() -> Self { + Self { + profile: String::from("debug"), + port: 10801, + // TODO: change default back to 0 + verbosity: 1, + client_dir: String::from("client"), + config_dir: get_config_dir().to_string_lossy().to_string(), + allowed_origins: None, + } + } +} + +impl StumpConfig { + /// Will load the [StumpConfig] object from the Stump.toml file. If the file does not exist, + /// it will create it with the default values. Internally, it will call `StumpConfig::from_env` + /// to override the toml values with newly set environment variables. This is done so that a user + /// can set an environment variable if they prefer to not manually edit the toml. + /// + /// ## Example + /// ```rust + /// use stump_core::config::StumpConfig; + /// use std::env; + /// + /// env::set_var("STUMP_PORT", "8080"); + /// let env = StumpConfig::load().unwrap(); + /// assert_eq!(env.port, 8080); + /// ``` + pub fn load() -> CoreResult { + let config_dir = get_config_dir(); + let stump_toml = config_dir.join("Stump.toml"); + + let environment = if stump_toml.exists() { + StumpConfig::from_toml(&stump_toml)? + } else { + debug!("Stump.toml does not exist, creating it"); + std::fs::File::create(stump_toml)?; + debug!("Stump.toml created"); + StumpConfig::default() + }; + + // I reassign the env here to make sure it picks up changes when a user manually sets a value + let environment = StumpConfig::from_env(Some(environment))?; + // I then reassign the env here to make sure the vars it previously set are correct + environment.set_env()?; + + Ok(environment) + } + + /// Will load the [StumpConfig] object from the Stump.toml file. + pub fn from_toml>(path: P) -> CoreResult { + let path = path.as_ref(); + let toml_str = std::fs::read_to_string(path)?; + let partial_config = toml::from_str::(&toml_str) + .map_err(|e| CoreError::InitializationError(e.to_string()))?; + + Ok(StumpConfig::from(partial_config)) + } + + /// Will try to create a new [StumpConfig] object from set environment variables. If none are set, + /// will return the default [StumpConfig] object. + /// + /// ## Example + /// ```rust + /// use std::env; + /// use stump_core::config::stump_config::StumpConfig; + /// + /// env::set_var("STUMP_PORT", "8080"); + /// env::set_var("STUMP_VERBOSITY", "3"); + /// + /// let config = StumpConfig::from_env(None); + /// assert!(env.is_ok()); + /// let config = config.unwrap(); + /// assert_eq!(config.port, 8080); + /// assert_eq!(config.verbosity, 3); + /// ``` + pub fn from_env(existing: Option) -> CoreResult { + let mut config = existing.unwrap_or_default(); + + if let Ok(port) = env::var("STUMP_PORT") { + config.port = port + .parse::() + .map_err(|e| CoreError::InitializationError(e.to_string()))?; + } + + if let Ok(profile) = env::var("STUMP_PROFILE") { + if profile == "release" || profile == "debug" { + config.profile = profile; + } else { + debug!("Invalid PROFILE value: {}", profile); + config.profile = String::from("debug"); + } + } + + if let Ok(verbosity) = env::var("STUMP_VERBOSITY") { + config.verbosity = verbosity + .parse::() + .map_err(|e| CoreError::InitializationError(e.to_string()))?; + } + + if let Ok(client_dir) = env::var("STUMP_CLIENT_DIR") { + config.client_dir = client_dir; + } + + if let Ok(config_dir) = env::var("STUMP_CONFIG_DIR") { + if Path::new(&config_dir).exists() { + config.config_dir = config_dir; + } else { + return Err(CoreError::ConfigDirDoesNotExist(config_dir)); + } + } + + config.config_dir = get_config_dir().to_string_lossy().to_string(); + config.write()?; + + Ok(config) + } + + /// Will set the environment variables to the values in the [StumpConfig] object. + /// If the values are not set, will use the default values. + /// + /// ## Example + /// ```rust + /// use std::env; + /// use stump_core::config::StumpConfig; + /// + /// let mut config = StumpConfig::default(); + /// config.port = 8080; + /// + /// assert_eq!(env::var("STUMP_PORT").is_err(), true); + /// config.set_env().unwrap(); + /// assert_eq!(env::var("STUMP_PORT").unwrap(), "8080"); + /// ``` + pub fn set_env(&self) -> CoreResult<()> { + if self.profile != "debug" { + env::set_var("STUMP_PROFILE", "release"); + } else { + env::set_var("STUMP_PROFILE", "debug"); + } + + env::set_var("STUMP_PORT", self.port.to_string()); + env::set_var("STUMP_VERBOSITY", self.verbosity.to_string()); + + if Path::new(self.config_dir.as_str()).exists() { + env::set_var("STUMP_CONFIG_DIR", self.config_dir.as_str()); + } + + if Path::new(self.client_dir.as_str()).exists() { + env::set_var("STUMP_CLIENT_DIR", self.client_dir.as_str()); + } + + if let Some(allowed_origins) = &self.allowed_origins { + if !allowed_origins.is_empty() { + env::set_var("STUMP_ALLOWED_ORIGINS", allowed_origins.join(",")); + } + } + + Ok(()) + } + + /// Will write the [StumpConfig] object to the Stump.toml file. + pub fn write(&self) -> CoreResult<()> { + let config_dir = get_config_dir(); + let stump_toml = config_dir.join("Stump.toml"); + + std::fs::write( + stump_toml.as_path(), + toml::to_string(&self) + .map_err(|e| CoreError::InitializationError(e.to_string()))?, + )?; + + Ok(()) + } +} + +impl From for StumpConfig { + fn from(partial: PartialStumpConfig) -> StumpConfig { + let mut default = StumpConfig::default(); + default.apply_options(partial); + + default + } +} diff --git a/core/src/db/dao/library_dao.rs b/core/src/db/dao/library_dao.rs new file mode 100644 index 000000000..f55acee51 --- /dev/null +++ b/core/src/db/dao/library_dao.rs @@ -0,0 +1,78 @@ +use std::sync::Arc; + +use crate::{ + db::models::Library, + prelude::{CoreError, CoreResult}, + prisma::{library, library_options, PrismaClient}, +}; + +use super::{Dao, LibraryOptionsDaoImpl}; + +pub struct LibraryDaoImpl { + client: Arc, +} + +#[async_trait::async_trait] +impl Dao for LibraryDaoImpl { + type Model = Library; + + fn new(client: Arc) -> Self { + Self { client } + } + + async fn insert(&self, data: Self::Model) -> CoreResult { + let library_options_dao = LibraryOptionsDaoImpl::new(self.client.clone()); + let library_options = library_options_dao.insert(data.library_options).await?; + + let created_library = self + .client + .library() + .create( + data.name, + data.path, + library_options::id::equals(library_options.id.unwrap()), + vec![], + ) + .with(library::library_options::fetch()) + .exec() + .await?; + + Ok(Library::from(created_library)) + } + + async fn delete(&self, id: &str) -> CoreResult { + let deleted_library = self + .client + .library() + .delete(library::id::equals(id.to_string())) + .exec() + .await?; + + Ok(Library::from(deleted_library)) + } + + async fn find_by_id(&self, id: &str) -> CoreResult { + let library = self + .client + .library() + .find_unique(library::id::equals(id.to_string())) + .with(library::library_options::fetch()) + .exec() + .await?; + + if library.is_none() { + return Err(CoreError::NotFound(format!( + "Library with id {} not found", + id + ))); + } + + Ok(Library::from(library.unwrap())) + } + + async fn find_all(&self) -> CoreResult> { + let libraries = self.client.library().find_many(vec![]).exec().await?; + + Ok(libraries.into_iter().map(Library::from).collect()) + } +} diff --git a/core/src/db/dao/library_options_dao.rs b/core/src/db/dao/library_options_dao.rs new file mode 100644 index 000000000..df123e70a --- /dev/null +++ b/core/src/db/dao/library_options_dao.rs @@ -0,0 +1,83 @@ +use std::sync::Arc; + +use crate::{ + db::models::LibraryOptions, + prelude::{CoreError, CoreResult}, + prisma::{library_options, PrismaClient}, +}; + +use super::Dao; + +pub struct LibraryOptionsDaoImpl { + client: Arc, +} + +#[async_trait::async_trait] +impl Dao for LibraryOptionsDaoImpl { + type Model = LibraryOptions; + + fn new(client: Arc) -> Self { + Self { client } + } + + async fn insert(&self, data: Self::Model) -> CoreResult { + let created_library_options = self + .client + .library_options() + .create(vec![ + library_options::convert_rar_to_zip::set(data.convert_rar_to_zip), + library_options::hard_delete_conversions::set( + data.hard_delete_conversions, + ), + library_options::create_webp_thumbnails::set(data.create_webp_thumbnails), + library_options::library_pattern::set(data.library_pattern.to_string()), + ]) + .exec() + .await?; + + Ok(LibraryOptions::from(created_library_options)) + } + + async fn delete(&self, id: &str) -> CoreResult { + let deleted_library_options = self + .client + .library_options() + .delete(library_options::id::equals(id.to_string())) + .exec() + .await?; + + Ok(LibraryOptions::from(deleted_library_options)) + } + + async fn find_by_id(&self, id: &str) -> CoreResult { + let library_options = self + .client + .library_options() + .find_unique(library_options::id::equals(id.to_string())) + .exec() + .await?; + + if library_options.is_none() { + return Err(CoreError::NotFound(format!( + "LibraryOptions with id {} not found", + id + ))); + } + + Ok(LibraryOptions::from(library_options.unwrap())) + } + + async fn find_all(&self) -> CoreResult> { + let library_options = self + .client + .library_options() + .find_many(vec![]) + .exec() + .await?; + + Ok(library_options + .into_iter() + .map(LibraryOptions::from) + .collect()) + } +} diff --git a/core/src/db/dao/media_dao.rs b/core/src/db/dao/media_dao.rs new file mode 100644 index 000000000..dc28b250e --- /dev/null +++ b/core/src/db/dao/media_dao.rs @@ -0,0 +1,345 @@ +use std::sync::Arc; + +use prisma_client_rust::{raw, Direction, PrismaValue}; + +use crate::{ + db::{models::Media, utils::CountQueryReturn}, + prelude::{CoreError, CoreResult, PageParams, Pageable}, + prisma::{media, read_progress, series, PrismaClient}, +}; + +use super::{Dao, DaoBatch}; + +#[async_trait::async_trait] +pub trait MediaDao: Dao { + async fn get_in_progress_media( + &self, + viewer_id: &str, + page_params: PageParams, + ) -> CoreResult>>; + + async fn get_duplicate_media(&self) -> CoreResult>; + async fn get_duplicate_media_page( + &self, + page_params: PageParams, + ) -> CoreResult>>; + + async fn update_many(&self, data: Vec) -> CoreResult>; +} + +pub struct MediaDaoImpl { + client: Arc, +} + +#[async_trait::async_trait] +impl MediaDao for MediaDaoImpl { + async fn get_in_progress_media( + &self, + viewer_id: &str, + page_params: PageParams, + ) -> CoreResult>> { + let page_bounds = page_params.get_page_bounds(); + + let progresses_with_media = self + .client + .read_progress() + .find_many(vec![ + read_progress::user_id::equals(viewer_id.to_string()), + read_progress::is_completed::equals(false), + ]) + .with(read_progress::media::fetch()) + .order_by(read_progress::updated_at::order(Direction::Desc)) + .skip(page_bounds.skip) + .take(page_bounds.take) + .exec() + .await?; + + let media_in_progress = progresses_with_media + .into_iter() + .filter(|progress| progress.media.is_some()) + .map(Media::try_from) + .filter_map(Result::ok) + .collect(); + + let db_total = self + .client + .read_progress() + .count(vec![ + read_progress::user_id::equals(viewer_id.to_string()), + read_progress::is_completed::equals(false), + ]) + .exec() + .await?; + + Ok(Pageable::with_count( + media_in_progress, + db_total, + page_params, + )) + } + + async fn get_duplicate_media(&self) -> CoreResult> { + let duplicates = self.client + ._query_raw::(raw!("SELECT * FROM media WHERE checksum IN (SELECT checksum FROM media GROUP BY checksum HAVING COUNT(*) > 1)")) + .exec() + .await?; + + Ok(duplicates) + } + + async fn get_duplicate_media_page( + &self, + page_params: PageParams, + ) -> CoreResult>> { + let page_bounds = page_params.get_page_bounds(); + + let duplicated_media_page = self + .client + ._query_raw::(raw!( + r#" + SELECT * FROM media + WHERE checksum IN ( + SELECT checksum FROM media GROUP BY checksum HAVING COUNT(*) > 1 + ) + LIMIT {} OFFSET {}"#, + PrismaValue::Int(page_bounds.take), + PrismaValue::Int(page_bounds.skip) + )) + .exec() + .await?; + + let count_result = self + .client + ._query_raw::(raw!( + r#" + SELECT COUNT(*) as count FROM media + WHERE checksum IN ( + SELECT checksum FROM media GROUP BY checksum HAVING COUNT(*) s> 1 + )"# + )) + .exec() + .await?; + + if let Some(db_total) = count_result.first() { + Ok(Pageable::with_count( + duplicated_media_page, + db_total.count, + page_params, + )) + } else { + Err(CoreError::InternalError( + "A failure occurred when trying to query for the count of duplicate media".to_string(), + )) + } + } + + async fn update_many(&self, data: Vec) -> CoreResult> { + let queries = data.into_iter().map(|media| { + self.client.media().update( + media::id::equals(media.id), + vec![ + media::name::set(media.name), + media::description::set(media.description), + media::size::set(media.size), + media::pages::set(media.pages), + media::checksum::set(media.checksum), + ], + ) + }); + + Ok(self + .client + ._batch(queries) + .await? + .into_iter() + .map(Media::from) + .collect()) + } +} + +#[async_trait::async_trait] +impl Dao for MediaDaoImpl { + type Model = Media; + + fn new(client: Arc) -> Self { + Self { client } + } + + async fn insert(&self, data: Self::Model) -> CoreResult { + let created_media = self + .client + .media() + .create( + data.name.to_owned(), + data.size, + data.extension.to_owned(), + data.pages, + data.path.to_owned(), + vec![ + media::checksum::set(data.checksum.to_owned()), + media::description::set(data.description.to_owned()), + media::series::connect(series::id::equals(data.series_id.to_owned())), + ], + ) + .exec() + .await?; + + Ok(Media::from(created_media)) + } + + async fn delete(&self, id: &str) -> CoreResult { + let deleted_media = self + .client + .media() + .delete(media::id::equals(id.to_string())) + .exec() + .await?; + + Ok(Media::from(deleted_media)) + } + + async fn find_all(&self) -> CoreResult> { + Ok(self + .client + .media() + .find_many(vec![]) + .exec() + .await? + .into_iter() + .map(Media::from) + .collect()) + } + + async fn find_by_id(&self, id: &str) -> CoreResult { + let media = self + .client + .media() + .find_unique(media::id::equals(id.to_string())) + .exec() + .await?; + + if media.is_none() { + return Err(CoreError::NotFound(format!( + "Media with id {} not found.", + id + ))); + } + + Ok(Media::from(media.unwrap())) + } +} + +#[async_trait::async_trait] +impl DaoBatch for MediaDaoImpl { + type Model = Media; + + async fn insert_many(&self, data: Vec) -> CoreResult> { + let queries = data.into_iter().map(|media| { + self.client.media().create( + media.name, + media.size, + media.extension, + media.pages, + media.path, + vec![ + media::checksum::set(media.checksum), + media::description::set(media.description), + media::series::connect(series::id::equals(media.series_id)), + ], + ) + }); + + Ok(self + .client + ._batch(queries) + .await? + .into_iter() + .map(Media::from) + .collect()) + } + + async fn _insert_batch(&self, models: T) -> CoreResult> + where + T: Iterator + Send + Sync, + { + let queries = models.map(|media| { + self.client.media().create( + media.name, + media.size, + media.extension, + media.pages, + media.path, + vec![ + media::checksum::set(media.checksum), + media::description::set(media.description), + media::series::connect(series::id::equals(media.series_id)), + ], + ) + }); + + let created_media = self.client._batch(queries).await?; + + Ok(created_media.into_iter().map(Media::from).collect()) + } + + async fn delete_many(&self, ids: Vec) -> CoreResult { + Ok(self + .client + .media() + .delete_many(vec![media::id::in_vec(ids)]) + .exec() + .await?) + } + + async fn _delete_batch(&self, ids: Vec) -> CoreResult> { + let queries = ids + .into_iter() + .map(|id| self.client.media().delete(media::id::equals(id))); + + let deleted_media = self.client._batch(queries).await?; + + Ok(deleted_media.into_iter().map(Media::from).collect()) + } +} + +// #[async_trait::async_trait] +// impl DaoUpsert for MediaDao { +// type Model = Media; + +// async fn upsert(&self, data: &Self::Model) -> CoreResult { +// let client = self.client; +// let resulting_media = client +// .media() +// .upsert( +// media::id::equals(data.id.clone()), +// ( +// data.name.clone(), +// data.size, +// data.extension.clone(), +// data.pages, +// data.path.clone(), +// vec![ +// media::checksum::set(data.checksum.clone()), +// media::description::set(data.description.clone()), +// media::series::connect(series::id::equals( +// data.series_id.clone(), +// )), +// ], +// ), +// vec![ +// media::name::set(data.name.clone()), +// media::size::set(data.size), +// media::extension::set(data.extension.clone()), +// media::pages::set(data.pages), +// media::path::set(data.path.clone()), +// media::checksum::set(data.checksum.clone()), +// media::description::set(data.description.clone()), +// media::series::connect(series::id::equals(data.series_id.clone())), +// ], +// ) +// .exec() +// .await?; + +// Ok(Media::from(resulting_media)) +// } +// } diff --git a/core/src/db/dao/mod.rs b/core/src/db/dao/mod.rs new file mode 100644 index 000000000..d79764abd --- /dev/null +++ b/core/src/db/dao/mod.rs @@ -0,0 +1,104 @@ +use std::sync::Arc; + +mod library_dao; +mod library_options_dao; +mod media_dao; +mod read_progress_dao; +mod reading_list_dao; +mod series_dao; + +pub use library_dao::*; +pub use library_options_dao::*; +pub use media_dao::*; +pub use read_progress_dao::*; +pub use reading_list_dao::*; +pub use series_dao::*; + +use crate::{prelude::CoreResult, prisma::PrismaClient}; + +// TODO: once my dao impls are more complete, add some integration tests. + +/// [`Dao`] trait defines the basic DB operations for a model. Update operations are not included since +/// they are more niche per model, and are not used in the generic way as the other operations. +#[async_trait::async_trait] +pub trait Dao: Sync + Sized { + type Model: Sync; + + // Creates a new Dao instance. + fn new(client: Arc) -> Self; + /// Creates a new record in the database. + async fn insert(&self, data: Self::Model) -> CoreResult; + /// Deletes a record from the database. + async fn delete(&self, id: &str) -> CoreResult; + /// Finds a record by its id. + async fn find_by_id(&self, id: &str) -> CoreResult; + /// Finds all records. + async fn find_all(&self) -> CoreResult>; +} + +// TODO: look into put an patch operations, remove DaoUpdate and merge with Dao + +/// [`DaoUpdate`] trait defines a single update operation for a model. This is a generic type signature +/// for the update operation, and since not all models really need this, it is contained in +/// a separate trait. +#[async_trait::async_trait] +pub trait DaoUpdate { + type Model: Sync; + + /// Updates a record in the database. + async fn update(&self, id: &str, data: Self::Model) -> CoreResult; + /// Updates a record in the database, or creates it if it does not exist. + async fn upsert(&self, data: Self::Model) -> CoreResult; + + // async fn update_many(&self, data: Vec) -> CoreResult>; + // async fn patch_many(&self, data: Vec) -> CoreResult>; +} + +#[async_trait::async_trait] +pub trait DaoBatch: Sync + Sized { + type Model: Sync; + + /// Creates multiple new records in the database. + async fn insert_many(&self, data: Vec) -> CoreResult>; + + // async fn _insert_batch, Marker>( + // &self, + // queries: T, + // ) -> CoreResult>; + + // FIXME: maybe refactor to take something like IntoIterator ? + async fn _insert_batch>( + &self, + models: T, + ) -> CoreResult> + where + T: Iterator + Send + Sync; + + // async fn _update_batch, Marker>( + // &self, + // queries: T, + // ) -> CoreResult>; + + /// Deletes multiple records from the database. Returns the number of deleted records. + async fn delete_many(&self, ids: Vec) -> CoreResult; + + /// Deletes multiple records from the database. Returns the records that were deleted. + async fn _delete_batch(&self, ids: Vec) -> CoreResult>; +} + +#[async_trait::async_trait] +pub trait DaoCount: Sync + Sized { + /// Counts the number of records in the database. + async fn count_all(&self) -> CoreResult; +} + +#[async_trait::async_trait] +pub trait DaoRestricted: Sync + Sized { + type Model: Sync; + + /// Finds a record by its id, if the user has access to it. + async fn find_by_id(&self, id: &str, user_id: &str) -> CoreResult; + + /// Finds all records, if the user has access to them. + async fn find_all(&self, user_id: &str) -> CoreResult>; +} diff --git a/core/src/db/dao/read_progress_dao.rs b/core/src/db/dao/read_progress_dao.rs new file mode 100644 index 000000000..0c01d30fe --- /dev/null +++ b/core/src/db/dao/read_progress_dao.rs @@ -0,0 +1,119 @@ +use std::sync::Arc; + +use crate::{ + db::models::ReadProgress, + prelude::{CoreError, CoreResult}, + prisma::{ + media, + read_progress::{self, UniqueWhereParam}, + user, PrismaClient, + }, +}; + +use super::{Dao, DaoUpdate}; + +pub struct ReadProgressDaoImpl { + client: Arc, +} + +#[async_trait::async_trait] +impl Dao for ReadProgressDaoImpl { + type Model = ReadProgress; + + fn new(client: Arc) -> Self { + Self { client } + } + + async fn insert(&self, data: Self::Model) -> CoreResult { + if data.media_id.is_empty() { + return Err(CoreError::InvalidQuery( + "ReadProgress::media_id must be set".to_string(), + )); + } else if data.user_id.is_empty() { + return Err(CoreError::InvalidQuery( + "ReadProgress::user_id must be set".to_string(), + )); + } + + let created_read_progress = self + .client + .read_progress() + .create( + data.page, + media::id::equals(data.media_id), + user::id::equals(data.user_id), + vec![], + ) + .exec() + .await?; + + Ok(ReadProgress::from(created_read_progress)) + } + + async fn delete(&self, id: &str) -> CoreResult { + let deleted_read_progress = self + .client + .read_progress() + .delete(read_progress::id::equals(id.to_string())) + .exec() + .await?; + + Ok(ReadProgress::from(deleted_read_progress)) + } + + async fn find_by_id(&self, id: &str) -> CoreResult { + let read_progress = self + .client + .read_progress() + .find_unique(read_progress::id::equals(id.to_string())) + .exec() + .await?; + + if read_progress.is_none() { + return Err(CoreError::NotFound(format!( + "ReadProgress with id {} not found", + id + ))); + } + + Ok(ReadProgress::from(read_progress.unwrap())) + } + + async fn find_all(&self) -> CoreResult> { + let read_progress = self.client.read_progress().find_many(vec![]).exec().await?; + + Ok(read_progress.into_iter().map(ReadProgress::from).collect()) + } +} + +#[async_trait::async_trait] +impl DaoUpdate for ReadProgressDaoImpl { + type Model = ReadProgress; + + async fn update(&self, _id: &str, _data: Self::Model) -> CoreResult { + unreachable!("ReadProgressDaoImpl::update will not be implemented"); + } + + async fn upsert(&self, data: Self::Model) -> CoreResult { + let read_progress = self + .client + .read_progress() + .upsert( + UniqueWhereParam::UserIdMediaIdEquals( + data.user_id.clone(), + data.id.clone(), + ), + ( + data.page, + media::id::equals(data.media_id.clone()), + user::id::equals(data.user_id.clone()), + vec![], + ), + vec![read_progress::page::set(data.page)], + ) + .exec() + .await?; + + Ok(ReadProgress::from(read_progress)) + } +} diff --git a/core/src/db/dao/reading_list_dao.rs b/core/src/db/dao/reading_list_dao.rs new file mode 100644 index 000000000..a24b82522 --- /dev/null +++ b/core/src/db/dao/reading_list_dao.rs @@ -0,0 +1,95 @@ +use std::sync::Arc; + +use crate::{ + db::models::ReadingList, + prelude::{CoreError, CoreResult}, + prisma::{media, reading_list, user, PrismaClient}, +}; + +use super::Dao; + +pub struct ReadingListDaoImpl { + client: Arc, +} + +#[async_trait::async_trait] +impl Dao for ReadingListDaoImpl { + type Model = ReadingList; + + fn new(client: Arc) -> Self { + Self { client } + } + + async fn insert(&self, data: Self::Model) -> CoreResult { + let media_ids = data + .media + .map(|m| m.into_iter().map(|m| m.id).collect::>()) + .unwrap_or_default(); + + let mut params = Vec::with_capacity(1); + if !media_ids.is_empty() { + params.push(reading_list::media::connect( + media_ids + .iter() + .map(|id| media::id::equals(id.to_string())) + .collect(), + )); + } + + let reading_list = self + .client + .reading_list() + .create( + data.name.to_owned(), + user::id::equals(data.creating_user_id.to_owned()), + params, + ) + .with(reading_list::media::fetch(vec![])) + .exec() + .await?; + + Ok(ReadingList::from(reading_list)) + } + + async fn delete(&self, id: &str) -> CoreResult { + let reading_list = self + .client + .reading_list() + .delete(reading_list::id::equals(id.to_string())) + .exec() + .await?; + + Ok(ReadingList::from(reading_list)) + } + + async fn find_by_id(&self, id: &str) -> CoreResult { + let reading_list = self + .client + .reading_list() + .find_unique(reading_list::id::equals(id.to_string())) + .with(reading_list::media::fetch(vec![])) + .exec() + .await?; + + if reading_list.is_none() { + return Err(CoreError::NotFound(format!( + "Reading list with ID {} not found", + id + ))); + } + + Ok(ReadingList::from(reading_list.unwrap())) + } + + async fn find_all(&self) -> CoreResult> { + let reading_lists = self + .client + .reading_list() + .find_many(vec![]) + .with(reading_list::media::fetch(vec![])) + .exec() + .await?; + + Ok(reading_lists.into_iter().map(ReadingList::from).collect()) + } +} diff --git a/core/src/db/dao/series_dao.rs b/core/src/db/dao/series_dao.rs new file mode 100644 index 000000000..8c9cc800b --- /dev/null +++ b/core/src/db/dao/series_dao.rs @@ -0,0 +1,200 @@ +use std::sync::Arc; + +use prisma_client_rust::{raw, PrismaValue}; +use tracing::{error, trace}; + +use crate::{ + db::{ + models::{Media, Series}, + utils::CountQueryReturn, + }, + prelude::{CoreError, CoreResult, PageParams, Pageable}, + prisma::{library, media, series, PrismaClient}, +}; + +use super::{Dao, DaoCount}; + +#[async_trait::async_trait] +pub trait SeriesDao { + async fn get_recently_added_series_page( + &self, + viewer_id: &str, + page_params: PageParams, + ) -> CoreResult>>; + async fn get_series_media(&self, series_id: &str) -> CoreResult>; +} + +pub struct SeriesDaoImpl { + client: Arc, +} + +#[async_trait::async_trait] +impl SeriesDao for SeriesDaoImpl { + // TODO: Once PCR is more mature, I think this kind of query can be possible without writing raw SQL. + // I know it's possible in JS prisma, so hopefully these kinds of manual queries can be phased out. + /// Returns a vector of [Series] in the order of most recently created in the database. + /// The number of books and unread books is included in the resulting [Series] objects. + /// This is used to populate the "Recently Added Series" section of the UI. + async fn get_recently_added_series_page( + &self, + viewer_id: &str, + page_params: PageParams, + ) -> CoreResult>> { + let page_bounds = page_params.get_page_bounds(); + let recently_added_series = self + .client + ._query_raw::(raw!( + r#" + SELECT + series.id AS id, + series.name AS name, + series.path AS path, + series.description AS description, + series.status AS status, + series.updated_at AS updated_at, + series.created_at AS created_at, + series.library_id AS library_id, + COUNT(series_media.id) AS media_count, + COUNT(series_media.id) - COUNT(media_progress.id) AS unread_media_count + FROM + series + LEFT OUTER JOIN media series_media ON series_media.series_id = series.id + LEFT OUTER JOIN read_progresses media_progress ON media_progress.media_id = series_media.id AND media_progress.user_id = {} + GROUP BY + series.id + ORDER BY + series.created_at DESC + LIMIT {} OFFSET {}"#, + PrismaValue::String(viewer_id.to_string()), + PrismaValue::Int(page_bounds.take), + PrismaValue::Int(page_bounds.skip) + )) + .exec() + .await?; + + // NOTE: removed the `GROUP BY` clause from the query below because it would cause an empty count result + // set to be returned. This makes sense, but is ~annoying~. + let count_result = self + .client + ._query_raw::(raw!( + r#" + SELECT + COUNT(DISTINCT series.id) as count + FROM + series + LEFT OUTER JOIN media series_media ON series_media.series_id = series.id + LEFT OUTER JOIN read_progresses media_progress ON media_progress.media_id = series_media.id AND media_progress.user_id = {} + ORDER BY + series.created_at DESC"#, + PrismaValue::String(viewer_id.to_string()) + )) + .exec() + .await.map_err(|e| { + error!(error = ?e, "Failed to compute count of recently added series"); + e + })?; + + trace!( + ?count_result, + "Count result for recently added series query." + ); + + if let Some(db_total) = count_result.first() { + Ok(Pageable::with_count( + recently_added_series, + db_total.count, + page_params, + )) + } else { + Err(CoreError::InternalError( + "A database error occurred while counting recently added series." + .to_string(), + )) + } + } + + async fn get_series_media(&self, series_id: &str) -> CoreResult> { + let series_media = self + .client + .media() + .find_many(vec![media::series_id::equals(Some(series_id.to_string()))]) + .exec() + .await?; + + Ok(series_media + .into_iter() + .map(Media::from) + .collect::>()) + } +} + +#[async_trait::async_trait] +impl DaoCount for SeriesDaoImpl { + async fn count_all(&self) -> CoreResult { + let count = self.client.series().count(vec![]).exec().await?; + + Ok(count) + } +} + +#[async_trait::async_trait] +impl Dao for SeriesDaoImpl { + type Model = Series; + + fn new(client: Arc) -> Self { + Self { client } + } + + async fn insert(&self, data: Self::Model) -> CoreResult { + let created_series = self + .client + .series() + .create( + data.name, + data.path, + vec![ + series::library::connect(library::id::equals(data.library_id)), + series::status::set(data.status.to_string()), + ], + ) + .exec() + .await?; + + Ok(Self::Model::from(created_series)) + } + + async fn delete(&self, id: &str) -> CoreResult { + let deleted_series = self + .client + .series() + .delete(series::id::equals(id.to_string())) + .exec() + .await?; + + Ok(Series::from(deleted_series)) + } + + async fn find_by_id(&self, id: &str) -> CoreResult { + let series = self + .client + .series() + .find_unique(series::id::equals(id.to_string())) + .exec() + .await?; + + if series.is_none() { + return Err(CoreError::NotFound(format!( + "Series with id {} not found", + id + ))); + } + + Ok(Series::from(series.unwrap())) + } + + async fn find_all(&self) -> CoreResult> { + let series = self.client.series().find_many(vec![]).exec().await?; + + Ok(series.into_iter().map(Series::from).collect()) + } +} diff --git a/core/src/db/migration.rs b/core/src/db/migration.rs index 2c2ee6b64..545d26636 100644 --- a/core/src/db/migration.rs +++ b/core/src/db/migration.rs @@ -1,6 +1,6 @@ use tracing::{debug, info}; -use crate::{prisma, types::CoreResult, CoreError}; +use crate::{prelude::CoreResult, prisma, CoreError}; pub async fn run_migrations(client: &prisma::PrismaClient) -> CoreResult<()> { info!("Running migrations..."); diff --git a/core/src/db/mod.rs b/core/src/db/mod.rs index fc9da54b6..28684deec 100644 --- a/core/src/db/mod.rs +++ b/core/src/db/mod.rs @@ -1,6 +1,10 @@ +pub(crate) mod dao; pub mod migration; +pub mod models; pub mod utils; +pub use dao::*; + use std::path::Path; use tracing::trace; diff --git a/core/src/types/models/epub.rs b/core/src/db/models/epub.rs similarity index 92% rename from core/src/types/models/epub.rs rename to core/src/db/models/epub.rs index e4c26e701..1c1db7dc0 100644 --- a/core/src/types/models/epub.rs +++ b/core/src/db/models/epub.rs @@ -1,11 +1,11 @@ -use std::{collections::HashMap, fs::File, path::PathBuf}; +use std::{collections::HashMap, fs::File, io::BufReader, path::PathBuf}; use epub::doc::{EpubDoc, NavPoint}; use serde::{Deserialize, Serialize}; use specta::Type; use tracing::error; -use crate::{prisma::media, types::errors::ProcessFileError}; +use crate::{prelude::errors::ProcessFileError, prisma::media}; use super::media::Media; @@ -67,7 +67,7 @@ pub struct Epub { /// resources/chapters. This struct isn't really used after that first request, everything else is file IO using EpubDoc. impl Epub { /// Creates an Epub from a media entity and an open EpubDoc - pub fn from(media: media::Data, epub: EpubDoc) -> Epub { + pub fn from(media: media::Data, epub: EpubDoc>) -> Epub { Epub { media_entity: media.into(), spine: epub.spine, diff --git a/core/src/types/models/library.rs b/core/src/db/models/library.rs similarity index 95% rename from core/src/types/models/library.rs rename to core/src/db/models/library.rs index 83c4a6821..02508fe6e 100644 --- a/core/src/types/models/library.rs +++ b/core/src/db/models/library.rs @@ -2,12 +2,13 @@ use std::{fmt, str::FromStr}; use serde::{Deserialize, Serialize}; use specta::Type; +use utoipa::ToSchema; use crate::prisma; use super::{series::Series, tag::Tag}; -#[derive(Debug, Clone, Deserialize, Serialize, Type)] +#[derive(Debug, Clone, Deserialize, Serialize, Type, ToSchema)] pub struct Library { pub id: String, /// The name of the library. ex: "Marvel Comics" @@ -28,7 +29,7 @@ pub struct Library { pub library_options: LibraryOptions, } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Type)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Type, ToSchema)] pub enum LibraryPattern { #[serde(rename = "SERIES_BASED")] SeriesBased, @@ -72,7 +73,7 @@ impl fmt::Display for LibraryPattern { } } -#[derive(Debug, Clone, Deserialize, Serialize, Type, Default)] +#[derive(Debug, Clone, Deserialize, Serialize, Type, ToSchema, Default)] pub struct LibraryOptions { // Note: this isn't really an Option, but I felt it was a little verbose // to create an entirely new struct Create/UpdateLibraryOptions for just one @@ -108,7 +109,7 @@ impl LibraryOptions { // } // } -#[derive(Deserialize, Debug, PartialEq, Eq, Copy, Clone, Type)] +#[derive(Deserialize, Debug, PartialEq, Eq, Copy, Clone, Type, ToSchema)] pub enum LibraryScanMode { #[serde(rename = "SYNC")] Sync, @@ -146,7 +147,7 @@ impl Default for LibraryScanMode { } } -#[derive(Deserialize, Serialize, Type)] +#[derive(Deserialize, Serialize, Type, ToSchema)] pub struct LibrariesStats { series_count: u64, book_count: u64, diff --git a/core/src/types/models/log.rs b/core/src/db/models/log.rs similarity index 74% rename from core/src/types/models/log.rs rename to core/src/db/models/log.rs index 6e8280352..0059a0a84 100644 --- a/core/src/types/models/log.rs +++ b/core/src/db/models/log.rs @@ -2,25 +2,27 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; use specta::Type; +use utoipa::ToSchema; use crate::event::CoreEvent; /// Information about the Stump log file, located at STUMP_CONFIG_DIR/Stump.log, or /// ~/.stump/Stump.log by default. Information such as the file size, last modified date, etc. -#[derive(Serialize, Deserialize, Type)] +#[derive(Serialize, Deserialize, Type, ToSchema)] pub struct LogMetadata { pub path: PathBuf, pub size: u64, pub modified: String, } -#[derive(Clone, Serialize, Deserialize, Type)] +#[derive(Clone, Default, Serialize, Deserialize, Type, ToSchema)] pub enum LogLevel { #[serde(rename = "ERROR")] Error, #[serde(rename = "WARN")] Warn, #[serde(rename = "INFO")] + #[default] Info, #[serde(rename = "DEBUG")] Debug, @@ -37,39 +39,33 @@ impl std::fmt::Display for LogLevel { } } -#[derive(Clone, Serialize, Deserialize, Type)] +#[derive(Clone, Serialize, Deserialize, Default, Type, ToSchema)] pub struct Log { pub id: String, pub level: LogLevel, pub message: String, pub created_at: String, - - pub job_id: Option, -} - -/// A helper struct mainly to convert client events to structs easier to persist to DB. -pub struct TentativeLog { - pub level: LogLevel, - pub message: String, pub job_id: Option, } -impl From for TentativeLog { +impl From for Log { fn from(event: CoreEvent) -> Self { match event { - CoreEvent::JobFailed { runner_id, message } => TentativeLog { + CoreEvent::JobFailed { runner_id, message } => Self { level: LogLevel::Error, message, job_id: Some(runner_id), + ..Default::default() }, CoreEvent::CreateEntityFailed { runner_id, path, message, - } => TentativeLog { + } => Self { level: LogLevel::Error, message: format!("{}: {}", path, message), job_id: runner_id, + ..Default::default() }, _ => unimplemented!(), } diff --git a/core/src/db/models/media.rs b/core/src/db/models/media.rs new file mode 100644 index 000000000..ee1fefb5d --- /dev/null +++ b/core/src/db/models/media.rs @@ -0,0 +1,173 @@ +use std::{path::Path, str::FromStr}; + +use optional_struct::OptionalStruct; +use serde::{Deserialize, Serialize}; +use specta::Type; +use utoipa::ToSchema; + +use crate::{ + prelude::{enums::FileStatus, CoreError, CoreResult}, + prisma::{media, read_progress}, +}; + +use super::{read_progress::ReadProgress, series::Series, tag::Tag, LibraryOptions}; + +#[derive( + Debug, Clone, Deserialize, Serialize, Type, Default, OptionalStruct, ToSchema, +)] +#[optional_name = "PartialMedia"] +#[optional_derive(Deserialize, Serialize)] +pub struct Media { + pub id: String, + /// The name of the media. ex: "The Amazing Spider-Man (2018) #69" + pub name: String, + /// The description of the media. ex: "Spidey and his superspy sister, Teresa Parker, dig to uncover THE CHAMELEON CONSPIRACY." + pub description: Option, + /// The size of the media in bytes. + pub size: i32, + /// The file extension of the media. ex: "cbz" + pub extension: String, + /// The number of pages in the media. ex: "69" + pub pages: i32, + /// The timestamp when the media was last updated. + pub updated_at: String, + /// The timestamp when the media was created. + pub created_at: String, + /// The timestamp when the file was last modified. + pub modified_at: String, + /// The checksum hash of the file contents. Used to ensure only one instance of a file in the database. + pub checksum: Option, + /// The path of the media. ex: "/home/user/media/comics/The Amazing Spider-Man (2018) #69.cbz" + pub path: String, + /// The status of the media + pub status: FileStatus, + /// The ID of the series this media belongs to. + pub series_id: String, + // The series this media belongs to. Will be `None` only if the relation is not loaded. + #[serde(skip_serializing_if = "Option::is_none")] + pub series: Option, + /// The read progresses of the media. Will be `None` only if the relation is not loaded. + #[serde(skip_serializing_if = "Option::is_none")] + pub read_progresses: Option>, + /// The current page of the media, computed from `read_progresses`. Will be `None` only + /// if the `read_progresses` relation is not loaded. + #[serde(skip_serializing_if = "Option::is_none")] + pub current_page: Option, + /// Whether or not the media is completed. Only None if the relation is not loaded. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_completed: Option, + /// The user assigned tags for the media. ex: ["comic", "spiderman"]. Will be `None` only if the relation is not loaded. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, +} + +impl Media { + // NOTE: currently, this will only look at a few fields: + // - name + // - description + // - size + // - pages + // - modified_at + // - status + // It's also a fairly naive implementation, effectively blindly overwriting + // the fields of the current media with the newer media. This is fine for now, + // but will eventually need to change to be more intelligent. + pub fn resolve_changes(&self, newer: &Media) -> Media { + Media { + name: newer.name.clone(), + description: newer.description.clone(), + size: newer.size, + pages: newer.pages, + modified_at: newer.modified_at.clone(), + status: newer.status, + checksum: newer.checksum.clone().or_else(|| self.checksum.clone()), + ..self.clone() + } + } +} + +impl TryFrom for Media { + type Error = CoreError; + + /// Creates a [Media] instance from the loaded relation of a [media::Data] on + /// a [read_progress::Data] instance. If the relation is not loaded, it will + /// return an error. + fn try_from(data: read_progress::Data) -> Result { + let relation = data.media(); + + if relation.is_err() { + return Err(CoreError::InvalidQuery( + "Failed to load media for read progress".to_string(), + )); + } + + let mut media = Media::from(relation.unwrap().to_owned()); + media.current_page = Some(data.page); + + Ok(media) + } +} + +#[derive(Default)] +pub struct MediaBuilderOptions { + pub series_id: String, + pub library_options: LibraryOptions, +} + +pub trait MediaBuilder { + fn build(path: &Path, series_id: &str) -> CoreResult; + fn build_with_options(path: &Path, options: MediaBuilderOptions) + -> CoreResult; +} + +impl From for Media { + fn from(data: media::Data) -> Media { + let series = match data.series() { + Ok(series) => Some(series.unwrap().to_owned().into()), + Err(_e) => None, + }; + + let (read_progresses, current_page, is_completed) = match data.read_progresses() { + Ok(read_progresses) => { + let progress = read_progresses + .iter() + .map(|rp| rp.to_owned().into()) + .collect::>(); + + // Note: ugh. + if let Some(p) = progress.first().cloned() { + (Some(progress), Some(p.page), Some(p.is_completed)) + } else { + (Some(progress), None, None) + } + }, + Err(_e) => (None, None, None), + }; + + let tags = match data.tags() { + Ok(tags) => Some(tags.iter().map(|tag| tag.to_owned().into()).collect()), + Err(_e) => None, + }; + + Media { + id: data.id, + name: data.name, + description: data.description, + size: data.size, + extension: data.extension, + pages: data.pages, + updated_at: data.updated_at.to_string(), + created_at: data.created_at.to_string(), + modified_at: data.modified_at.to_string(), + checksum: data.checksum, + path: data.path, + status: FileStatus::from_str(&data.status).unwrap_or(FileStatus::Error), + series_id: data.series_id.unwrap(), + series, + read_progresses, + current_page, + is_completed, + tags, + } + } +} diff --git a/core/src/types/models/mod.rs b/core/src/db/models/mod.rs similarity index 61% rename from core/src/types/models/mod.rs rename to core/src/db/models/mod.rs index 009eadcc0..9c47d37b0 100644 --- a/core/src/types/models/mod.rs +++ b/core/src/db/models/mod.rs @@ -1,21 +1,20 @@ pub mod epub; pub mod library; -pub mod list_directory; pub mod log; pub mod media; pub mod read_progress; +pub mod reading_list; pub mod series; pub mod tag; pub mod user; -pub mod readinglist; -pub use crate::types::models::epub::*; -pub use crate::types::models::log::*; +pub use crate::db::models::epub::*; +pub use crate::db::models::log::*; pub use library::*; -pub use list_directory::*; pub use media::*; pub use read_progress::*; +pub use reading_list::*; pub use series::*; pub use tag::*; pub use user::*; diff --git a/core/src/types/models/read_progress.rs b/core/src/db/models/read_progress.rs similarity index 83% rename from core/src/types/models/read_progress.rs rename to core/src/db/models/read_progress.rs index 105df067c..d14a364fd 100644 --- a/core/src/types/models/read_progress.rs +++ b/core/src/db/models/read_progress.rs @@ -1,16 +1,19 @@ use serde::{Deserialize, Serialize}; use specta::Type; use tracing::trace; +use utoipa::ToSchema; use crate::prisma; use super::{media::Media, user::User}; -#[derive(Debug, Clone, Deserialize, Serialize, Type)] +#[derive(Debug, Clone, Deserialize, Serialize, Type, ToSchema, Default)] pub struct ReadProgress { pub id: String, /// The current page pub page: i32, + /// boolean to indicate if the media is completed + pub is_completed: bool, /// The ID of the media which has progress. pub media_id: String, /// The media which has progress. Will be `None` if the relation is not loaded. @@ -36,6 +39,7 @@ impl From for ReadProgress { ReadProgress { id: data.id, page: data.page, + is_completed: data.is_completed, media_id: data.media_id, media, user_id: data.user_id, diff --git a/core/src/db/models/reading_list.rs b/core/src/db/models/reading_list.rs new file mode 100644 index 000000000..f848083a9 --- /dev/null +++ b/core/src/db/models/reading_list.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; +use utoipa::ToSchema; + +use crate::{db::models::Media, prisma::reading_list}; + +#[derive(Debug, Clone, Serialize, Deserialize, Type, ToSchema, Default)] +pub struct ReadingList { + pub id: String, + pub name: String, + pub creating_user_id: String, + pub description: Option, + pub media: Option>, +} + +impl From for ReadingList { + fn from(data: reading_list::Data) -> ReadingList { + ReadingList { + id: data.id, + name: data.name, + creating_user_id: data.creating_user_id, + description: data.description, + media: None, + } + } +} diff --git a/core/src/types/models/series.rs b/core/src/db/models/series.rs similarity index 82% rename from core/src/types/models/series.rs rename to core/src/db/models/series.rs index 8d1a84429..cea653e40 100644 --- a/core/src/types/models/series.rs +++ b/core/src/db/models/series.rs @@ -2,12 +2,13 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; use specta::Type; +use utoipa::ToSchema; -use crate::{prisma, types::enums::FileStatus}; +use crate::{prelude::enums::FileStatus, prisma}; use super::{library::Library, media::Media, tag::Tag}; -#[derive(Debug, Clone, Deserialize, Serialize, Type)] +#[derive(Debug, Clone, Deserialize, Serialize, Type, ToSchema)] pub struct Series { pub id: String, /// The name of the series. ex: "The Amazing Spider-Man (2018)" @@ -18,16 +19,22 @@ pub struct Series { pub description: Option, /// The status of the series since last scan or access pub status: FileStatus, - // pub updated_at: DateTime, + /// The timestamp of when the series was last updated pub updated_at: String, + /// The timestamp of when the series was created + pub created_at: String, /// The ID of the library this series belongs to. pub library_id: String, /// The library this series belongs to. Will be `None` only if the relation is not loaded. pub library: Option, /// The media that are in this series. Will be `None` only if the relation is not loaded. pub media: Option>, + #[serde(skip_serializing_if = "Option::is_none")] /// The number of media in this series. Optional for safety, but should be loaded if possible. pub media_count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// The number of media in this series which have not been read. Only loaded on some queries. + pub unread_media_count: Option, /// The user assigned tags for the series. ex: ["comic", "family"]. Will be `None` only if the relation is not loaded. pub tags: Option>, } @@ -80,10 +87,12 @@ impl From for Series { description: data.description, status: FileStatus::from_str(&data.status).unwrap_or(FileStatus::Error), updated_at: data.updated_at.to_string(), + created_at: data.created_at.to_string(), library_id: data.library_id.unwrap(), library, media, media_count, + unread_media_count: None, tags, } } diff --git a/core/src/types/models/tag.rs b/core/src/db/models/tag.rs similarity index 78% rename from core/src/types/models/tag.rs rename to core/src/db/models/tag.rs index 0d48666e6..0a6f74c3f 100644 --- a/core/src/types/models/tag.rs +++ b/core/src/db/models/tag.rs @@ -1,9 +1,10 @@ use serde::{Deserialize, Serialize}; use specta::Type; +use utoipa::ToSchema; use crate::prisma; -#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[derive(Debug, Clone, Serialize, Deserialize, Type, ToSchema)] pub struct Tag { pub id: String, /// The name of the tag. ex: "comic" diff --git a/core/src/types/models/user.rs b/core/src/db/models/user.rs similarity index 91% rename from core/src/types/models/user.rs rename to core/src/db/models/user.rs index 71781f467..b873220bd 100644 --- a/core/src/types/models/user.rs +++ b/core/src/db/models/user.rs @@ -1,9 +1,10 @@ use serde::{Deserialize, Serialize}; use specta::Type; +use utoipa::ToSchema; use crate::prisma; -#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[derive(Debug, Clone, Serialize, Deserialize, Type, ToSchema)] pub struct User { pub id: String, pub username: String, @@ -39,7 +40,7 @@ impl From for User { } } -#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[derive(Debug, Clone, Serialize, Deserialize, Type, ToSchema)] pub struct UserPreferences { pub id: String, diff --git a/core/src/db/utils.rs b/core/src/db/utils.rs index a3a77a9c2..7ff1e6c64 100644 --- a/core/src/db/utils.rs +++ b/core/src/db/utils.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use prisma_client_rust::{raw, PrismaValue}; use serde::Deserialize; -use crate::{prisma::PrismaClient, types::CoreResult}; +use crate::{prelude::CoreResult, prisma::PrismaClient}; #[derive(Deserialize, Debug, Default)] pub struct CountQueryReturn { diff --git a/core/src/event/event_manager.rs b/core/src/event/event_manager.rs index 4fdacaf19..f00ffcb45 100644 --- a/core/src/event/event_manager.rs +++ b/core/src/event/event_manager.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::{config::context::Ctx, event::InternalCoreTask, job::pool::JobPool}; +use crate::{event::InternalCoreTask, job::pool::JobPool, prelude::Ctx}; use tokio::{self, sync::mpsc}; use tracing::error; diff --git a/core/src/event/mod.rs b/core/src/event/mod.rs index ea0f57d47..27332ba2e 100644 --- a/core/src/event/mod.rs +++ b/core/src/event/mod.rs @@ -5,9 +5,10 @@ use specta::Type; use tokio::sync::oneshot; use crate::{ + db::models::Media, job::{Job, JobReport, JobStatus, JobUpdate}, + prelude::CoreResult, prisma, - types::CoreResult, }; pub enum InternalCoreTask { @@ -39,7 +40,7 @@ pub enum CoreEvent { path: String, message: String, }, - CreatedMedia(prisma::media::Data), + CreatedMedia(Box), // TODO: not sure if I should send the number of insertions or the insertions themselves. // cloning the vector is potentially expensive. CreatedMediaBatch(u64), diff --git a/core/src/fs/archive.rs b/core/src/fs/archive.rs index ceac2ee68..d570570c5 100644 --- a/core/src/fs/archive.rs +++ b/core/src/fs/archive.rs @@ -13,7 +13,7 @@ pub(crate) fn zip_dir( destination: &Path, prefix: &Path, ) -> zip::result::ZipResult<()> { - let zip_file = std::fs::File::create(&destination).unwrap(); + let zip_file = std::fs::File::create(destination).unwrap(); let mut zip_writer = zip::ZipWriter::new(zip_file); @@ -40,7 +40,7 @@ pub(crate) fn zip_dir( let mut f = File::open(path)?; f.read_to_end(&mut buffer)?; - zip_writer.write_all(&*buffer)?; + zip_writer.write_all(&buffer)?; buffer.clear(); } else if !name.as_os_str().is_empty() { diff --git a/core/src/fs/image.rs b/core/src/fs/image.rs index 73167b304..70bad4b30 100644 --- a/core/src/fs/image.rs +++ b/core/src/fs/image.rs @@ -8,11 +8,13 @@ use std::{ use tracing::{debug, error, trace}; use webp::{Encoder, WebPMemory}; -use crate::{config::get_thumbnails_dir, prisma::media, types::errors::ProcessFileError}; +use crate::{ + config::get_thumbnails_dir, db::models::Media, prelude::errors::ProcessFileError, +}; use super::media_file; -pub fn get_image_bytes>(path: P) -> Result, ProcessFileError> { +pub fn get_bytes>(path: P) -> Result, ProcessFileError> { let mut file = File::open(path)?; let mut buf = Vec::new(); @@ -90,9 +92,7 @@ pub fn generate_thumbnail(id: &str, path: &str) -> Result, -) -> Result, ProcessFileError> { +pub fn generate_thumbnails(media: &[Media]) -> Result, ProcessFileError> { debug!("Enter generate_thumbnails"); // TODO: this might make the stack overflow lol @@ -102,7 +102,7 @@ pub fn generate_thumbnails( .map(|m| generate_thumbnail(m.id.as_str(), m.path.as_str())) .filter_map(|res| { if res.is_err() { - error!("Error generating thumbnail: {:?}", res.err()); + error!(error = ?res.err(), "Error generating thumbnail"); None } else { res.ok() diff --git a/core/src/fs/media_file/constants.rs b/core/src/fs/media_file/constants.rs new file mode 100644 index 000000000..f8fa3c796 --- /dev/null +++ b/core/src/fs/media_file/constants.rs @@ -0,0 +1,6 @@ +pub(crate) fn is_accepted_cover_name(name: &str) -> bool { + let cover_file_names = vec!["cover", "thumbnail", "folder"]; + cover_file_names + .iter() + .any(|&cover_name| name.eq_ignore_ascii_case(cover_name)) +} diff --git a/core/src/fs/media_file/epub.rs b/core/src/fs/media_file/epub.rs index 974691c21..a1f11ec97 100644 --- a/core/src/fs/media_file/epub.rs +++ b/core/src/fs/media_file/epub.rs @@ -1,5 +1,6 @@ use std::{ fs::File, + io::BufReader, path::{Path, PathBuf}, }; @@ -9,22 +10,22 @@ use std::os::unix::prelude::MetadataExt; #[cfg(target_family = "windows")] use std::os::windows::prelude::*; +const ACCEPTED_EPUB_COVER_MIMES: [&str; 2] = ["image/jpeg", "image/png"]; +const DEFAULT_EPUB_COVER_ID: &str = "cover"; + use crate::{ - fs::{ - checksum, - media_file::{get_content_type_from_mime, guess_content_type}, - }, - types::{errors::ProcessFileError, models::media::ProcessedMediaFile, ContentType}, + fs::checksum, + prelude::{errors::ProcessFileError, fs::ProcessedMediaFile, ContentType}, }; use epub::doc::EpubDoc; -use tracing::{debug, error, warn}; +use tracing::{debug, error, trace, warn}; /* epubcfi usually starts with /6, referring to spine element of package file file has three groups of elements: metadata, manifest and spine. */ // TODO: options: &LibraryOptions -pub fn digest_epub(path: &Path, size: u64) -> Option { +pub fn digest(path: &Path, size: u64) -> Option { let mut bytes_to_read = size; // FIXME: this isn't ideal @@ -44,11 +45,11 @@ pub fn digest_epub(path: &Path, size: u64) -> Option { } } -fn load_epub(path: &str) -> Result, ProcessFileError> { +fn load_epub(path: &str) -> Result>, ProcessFileError> { EpubDoc::new(path).map_err(|e| ProcessFileError::EpubOpenError(e.to_string())) } -pub fn process_epub(path: &Path) -> Result { +pub fn process(path: &Path) -> Result { debug!("Processing Epub: {}", path.display()); let epub_file = load_epub(path.to_str().unwrap())?; @@ -68,26 +69,80 @@ pub fn process_epub(path: &Path) -> Result Ok(ProcessedMediaFile { thumbnail_path: None, path: path.to_path_buf(), - checksum: digest_epub(path, file_size), + checksum: digest(path, file_size), metadata: None, pages, }) } // TODO: change return type to make more sense -pub fn get_epub_cover(file: &str) -> Result<(ContentType, Vec), ProcessFileError> { +/// Returns the cover image for the epub file. If a cover image cannot be extracted via the +/// metadata, it will go through two rounds of fallback methods: +/// +/// 1. Attempt to find a resource with the default ID of "cover" +/// 2. Attempt to find a resource with a mime type of "image/jpeg" or "image/png", and weight the +/// results based on how likely they are to be the cover. For example, if the cover is named +/// "cover.jpg", it's probably the cover. The entry with the heighest weight, if any, will be +/// returned. +pub fn get_cover(file: &str) -> Result<(ContentType, Vec), ProcessFileError> { let mut epub_file = EpubDoc::new(file).map_err(|e| { error!("Failed to open epub file: {}", e); ProcessFileError::EpubOpenError(e.to_string()) })?; - let cover = epub_file.get_cover().map_err(|e| { - error!("Failed to get cover from epub file: {}", e); - ProcessFileError::EpubReadError(e.to_string()) - })?; + let cover_id = epub_file.get_cover_id().unwrap_or_else(|_| { + debug!("Epub file does not contain cover metadata"); + DEFAULT_EPUB_COVER_ID.to_string() + }); + + if let Ok(cover) = epub_file.get_resource(&cover_id) { + let mime = epub_file + .get_resource_mime(&cover_id) + .unwrap_or_else(|_| "image/png".to_string()); + + return Ok((ContentType::from(mime.as_str()), cover)); + } - // FIXME: mime type - Ok((get_content_type_from_mime("image/png"), cover)) + debug!( + "Explicit cover image could not be found, falling back to searching for best match..." + ); + // FIXME: this is hack, i do NOT want to clone this entire hashmap... + let cloned_resources = epub_file.resources.clone(); + let search_result = cloned_resources + .iter() + .filter(|(_, (_, mime))| { + ACCEPTED_EPUB_COVER_MIMES + .iter() + .any(|accepted_mime| accepted_mime == mime) + }) + .map(|(id, (path, _))| { + trace!(name = ?path, "Found possible cover image"); + // I want to weight the results based on how likely they are to be the cover. + // For example, if the cover is named "cover.jpg", it's probably the cover. + // TODO: this is SUPER naive, and should be improved at some point... + if path.starts_with("cover") { + let weight = if path.ends_with("png") { 100 } else { 75 }; + (weight, id) + } else { + (0, id) + } + }) + .max_by_key(|(weight, _)| *weight); + + if let Some((_, id)) = search_result { + if let Ok(c) = epub_file.get_resource(id) { + let mime = epub_file + .get_resource_mime(id) + .unwrap_or_else(|_| "image/png".to_string()); + + return Ok((ContentType::from(mime.as_str()), c)); + } + } + + error!("Failed to find cover for epub file"); + Err(ProcessFileError::EpubReadError( + "Failed to find cover for epub file".to_string(), + )) } pub fn get_epub_chapter( @@ -107,15 +162,15 @@ pub fn get_epub_chapter( })?; let content_type = match epub_file.get_current_mime() { - Ok(mime) => get_content_type_from_mime(&mime), + Ok(mime) => ContentType::from(mime.as_str()), Err(e) => { - warn!( - "Failed to get explicit definition of resource mime for {}: {}", - path, e + error!( + error = ?e, + chapter_path = ?path, + "Failed to get explicit resource mime for chapter. Returning default.", ); - // FIXME: when did I write this? lmao - guess_content_type("REMOVEME.xhml") + ContentType::XHTML }, }; @@ -138,7 +193,7 @@ pub fn get_epub_resource( ProcessFileError::EpubReadError(e.to_string()) })?; - Ok((get_content_type_from_mime(&content_type), contents)) + Ok((ContentType::from(content_type.as_str()), contents)) } pub fn normalize_resource_path(path: PathBuf, root: &str) -> PathBuf { @@ -200,7 +255,7 @@ pub fn get_epub_resource_from_path( // package.opf, etc.). let content_type = match epub_file.get_resource_mime_by_path(adjusted_path.as_path()) { - Ok(mime) => get_content_type_from_mime(&mime), + Ok(mime) => ContentType::from(mime.as_str()), Err(e) => { warn!( "Failed to get explicit definition of resource mime for {}: {}", @@ -208,7 +263,7 @@ pub fn get_epub_resource_from_path( e ); - guess_content_type(adjusted_path.as_path().to_str().unwrap()) + ContentType::from_path(adjusted_path.as_path()) }, }; diff --git a/core/src/fs/media_file/mod.rs b/core/src/fs/media_file/mod.rs index c7f2eff06..c77dbf7f2 100644 --- a/core/src/fs/media_file/mod.rs +++ b/core/src/fs/media_file/mod.rs @@ -1,32 +1,21 @@ +pub mod constants; pub mod epub; pub mod pdf; pub mod rar; pub mod zip; use std::path::Path; -use tracing::{debug, warn}; +use tracing::debug; use crate::{ - fs::media_file::{epub::process_epub, rar::process_rar, zip::process_zip}, - types::{ + db::models::LibraryOptions, + prelude::{ errors::ProcessFileError, - models::{ - library::LibraryOptions, - media::{MediaMetadata, ProcessedMediaFile}, - }, + fs::media_file::{MediaMetadata, ProcessedMediaFile}, ContentType, }, }; -use self::{epub::get_epub_cover, rar::get_rar_image, zip::get_zip_image}; - -// FIXME: this module does way too much. It should be cleaned up, way too many vaguely -// similar things shoved in here with little distinction. - -// TODO: replace all these match statements with an custom enum that handles it all. -// The enum itself will have some repetition, however it'll be cleaner than -// doing this stuff over and over as this file currently does. - // TODO: move trait, maybe merge with another. pub trait IsImage { fn is_image(&self) -> bool; @@ -43,110 +32,27 @@ pub fn process_comic_info(buffer: String) -> Option { } } -fn temporary_content_workarounds(extension: &str) -> ContentType { - if extension == "opf" || extension == "ncx" { - return ContentType::XML; - } - - ContentType::UNKNOWN -} - -pub fn guess_content_type(file: &str) -> ContentType { - let file = Path::new(file); - - let extension = file.extension().unwrap_or_default(); - let extension = extension.to_string_lossy().to_string(); - - // TODO: if this fails manually check the extension - match ContentType::from_extension(&extension) { - Some(content_type) => content_type, - // None => ContentType::Any, - None => temporary_content_workarounds(&extension), - } -} - -pub fn get_content_type_from_mime(mime: &str) -> ContentType { - ContentType::from(mime) -} - -/// Guess the MIME type of a file based on its extension. -pub fn guess_mime(path: &Path) -> Option { - let extension = path.extension().and_then(|ext| ext.to_str()); - - if extension.is_none() { - warn!( - "Unable to guess mime for file without extension: {:?}", - path - ); - return None; - } - - let extension = extension.unwrap(); - - let content_type = ContentType::from_extension(extension); - - if let Some(content_type) = content_type { - return Some(content_type.to_string()); - } - - // TODO: add more? - match extension.to_lowercase().as_str() { - "pdf" => Some("application/pdf".to_string()), - "epub" => Some("application/epub+zip".to_string()), - "zip" => Some("application/zip".to_string()), - "cbz" => Some("application/vnd.comicbook+zip".to_string()), - "rar" => Some("application/vnd.rar".to_string()), - "cbr" => Some("application/vnd.comicbook-rar".to_string()), - "png" => Some("image/png".to_string()), - "jpg" => Some("image/jpeg".to_string()), - "jpeg" => Some("image/jpeg".to_string()), - "webp" => Some("image/webp".to_string()), - "gif" => Some("image/gif".to_string()), - _ => None, - } -} - -/// Infer the MIME type of a file. If the MIME type cannot be inferred via reading -/// the first few bytes of the file, then the file extension is used via `guess_mime`. -pub fn infer_mime_from_path(path: &Path) -> Option { - match infer::get_from_path(path) { - Ok(mime) => { - debug!("Inferred mime for file {:?}: {:?}", path, mime); - mime.map(|m| m.mime_type().to_string()) - }, - Err(e) => { - warn!( - "Unable to infer mime for file {:?}: {:?}", - path, - e.to_string() - ); - - guess_mime(path) - }, - } -} - pub fn get_page( file: &str, page: i32, ) -> Result<(ContentType, Vec), ProcessFileError> { - let mime = guess_mime(Path::new(file)); - - match mime.as_deref() { - Some("application/zip") => get_zip_image(file, page), - Some("application/vnd.comicbook+zip") => get_zip_image(file, page), - Some("application/vnd.rar") => get_rar_image(file, page), - Some("application/vnd.comicbook-rar") => get_rar_image(file, page), - Some("application/epub+zip") => { + let mime = ContentType::from_file(file).mime_type(); + + match mime.as_str() { + "application/zip" => zip::get_image(file, page), + "application/vnd.comicbook+zip" => zip::get_image(file, page), + "application/vnd.rar" => rar::get_image(file, page), + "application/vnd.comicbook-rar" => rar::get_image(file, page), + "application/epub+zip" => { if page == 1 { - get_epub_cover(file) + epub::get_cover(file) } else { Err(ProcessFileError::UnsupportedFileType( "You may only request the cover page (first page) for epub files on this endpoint".into() )) } }, - None => Err(ProcessFileError::Unknown(format!( + "unknown" => Err(ProcessFileError::Unknown(format!( "Unable to determine mime type for file: {:?}", file ))), @@ -154,21 +60,32 @@ pub fn get_page( } } +fn process_rar( + convert: bool, + path: &Path, +) -> Result { + if convert { + let zip_path = rar::convert_to_zip(path)?; + zip::process(zip_path.as_path()) + } else { + rar::process(path) + } +} + pub fn process( path: &Path, options: &LibraryOptions, ) -> Result { - debug!("Processing entry {:?} with options: {:?}", path, options); - - let mime = infer_mime_from_path(path); - - match mime.as_deref() { - Some("application/zip") => process_zip(path), - Some("application/vnd.comicbook+zip") => process_zip(path), - Some("application/vnd.rar") => process_rar(path, options), - Some("application/vnd.comicbook-rar") => process_rar(path, options), - Some("application/epub+zip") => process_epub(path), - None => Err(ProcessFileError::Unknown(format!( + debug!(?path, ?options, "Processing entry"); + let mime = ContentType::from_path(path).mime_type(); + + match mime.as_str() { + "application/zip" => zip::process(path), + "application/vnd.comicbook+zip" => zip::process(path), + "application/vnd.rar" => process_rar(options.convert_rar_to_zip, path), + "application/vnd.comicbook-rar" => process_rar(options.convert_rar_to_zip, path), + "application/epub+zip" => epub::process(path), + "unknown" => Err(ProcessFileError::Unknown(format!( "Unable to determine mime type for file: {:?}", path ))), diff --git a/core/src/fs/media_file/pdf.rs b/core/src/fs/media_file/pdf.rs index ca7346ba5..0c823afbe 100644 --- a/core/src/fs/media_file/pdf.rs +++ b/core/src/fs/media_file/pdf.rs @@ -1,6 +1,6 @@ -use crate::types::{errors::ProcessFileError, ContentType}; +use crate::prelude::{errors::ProcessFileError, ContentType}; -pub fn get_pdf_page( +pub fn get_page( _file: &str, _page: usize, ) -> Result<(ContentType, Vec), ProcessFileError> { diff --git a/core/src/fs/media_file/rar.rs b/core/src/fs/media_file/rar.rs index 8a6675d25..9b046a87b 100644 --- a/core/src/fs/media_file/rar.rs +++ b/core/src/fs/media_file/rar.rs @@ -7,12 +7,10 @@ use crate::{ fs::{ archive::create_zip_archive, checksum::{self, DIGEST_SAMPLE_COUNT, DIGEST_SAMPLE_SIZE}, - media_file::{self, zip, IsImage}, + media_file::{self, IsImage}, }, - types::{ - errors::ProcessFileError, - models::{library::LibraryOptions, media::ProcessedMediaFile}, - ContentType, + prelude::{ + errors::ProcessFileError, fs::media_file::ProcessedMediaFile, ContentType, }, }; @@ -31,7 +29,7 @@ impl IsImage for Entry { } } -pub fn convert_rar_to_zip(path: &Path) -> Result { +pub fn convert_to_zip(path: &Path) -> Result { debug!("Converting {:?} to zip format.", &path); let archive = unrar::Archive::new(path)?; @@ -91,18 +89,7 @@ pub fn convert_rar_to_zip(path: &Path) -> Result { /// Processes a rar file in its entirety. Will return a tuple of the comic info and the list of /// files in the rar. -pub fn process_rar( - path: &Path, - options: &LibraryOptions, -) -> Result { - if options.convert_rar_to_zip { - let new_path = convert_rar_to_zip(path)?; - - trace!("Using `process_zip` with converted rar."); - - return zip::process_zip(&new_path); - } - +pub fn process(path: &Path) -> Result { // or platform is windows if stump_in_docker() || cfg!(windows) { return Err(ProcessFileError::UnsupportedFileType( @@ -120,7 +107,7 @@ pub fn process_rar( #[allow(unused_mut)] let mut metadata_buf = Vec::::new(); - let checksum = digest_rar(&path_str); + let checksum = digest(&path_str); match archive.list_extract() { Ok(open_archive) => { @@ -165,7 +152,7 @@ pub fn process_rar( // FIXME: this is a temporary work around for the issue wonderful people on Discord // discovered. -pub fn rar_sample(file: &str) -> Result { +pub fn sample_size(file: &str) -> Result { debug!("Calculating checksum sample size for: {}", file); let file = std::fs::File::open(file)?; @@ -204,10 +191,10 @@ pub fn rar_sample(file: &str) -> Result { // .fold(0, |acc, e| acc + e.unpacked_size as u64)) } -pub fn digest_rar(file: &str) -> Option { +pub fn digest(file: &str) -> Option { debug!("Attempting to generate checksum for: {}", file); - let sample = rar_sample(file); + let sample = sample_size(file); // Error handled in `rar_sample` if sample.is_err() { @@ -242,7 +229,7 @@ pub fn digest_rar(file: &str) -> Option { // OpenArchive handle stored in Entry is no more. That's why I create another archive to grab what I want before // the iterator is done. At least, I *think* that is what is happening. // Fix location: https://github.com/aaronleopold/unrar.rs/tree/aleopold--read-bytes -pub fn get_rar_image( +pub fn get_image( file: &str, page: i32, ) -> Result<(ContentType, Vec), ProcessFileError> { diff --git a/core/src/fs/media_file/zip.rs b/core/src/fs/media_file/zip.rs index 9d6ec5c73..fb4ed1902 100644 --- a/core/src/fs/media_file/zip.rs +++ b/core/src/fs/media_file/zip.rs @@ -7,20 +7,16 @@ use crate::{ checksum, media_file::{self, IsImage}, }, - types::{errors::ProcessFileError, models::media::ProcessedMediaFile, ContentType}, + prelude::{ + errors::ProcessFileError, fs::media_file::ProcessedMediaFile, ContentType, + }, }; impl<'a> IsImage for ZipFile<'a> { - // FIXME: use infer here fn is_image(&self) -> bool { if self.is_file() { - let content_type = media_file::guess_content_type(self.name()); - trace!( - "Content type of file {:?} is {:?}", - self.name(), - content_type - ); - + let content_type = ContentType::from_file(self.name()); + trace!(name = self.name(), content_type = ?content_type, "ContentType of file"); return content_type.is_image(); } @@ -31,7 +27,7 @@ impl<'a> IsImage for ZipFile<'a> { /// Get the sample size (in bytes) to use for generating a checksum of a zip file. Rather than /// computing the sample size via the file size, we instead calculate the sample size by /// summing the size of the first 5 files in the zip file. -pub fn zip_sample(file: &str) -> u64 { +pub fn sample_size(file: &str) -> u64 { let zip_file = File::open(file).unwrap(); let mut archive = zip::ZipArchive::new(zip_file).unwrap(); @@ -53,8 +49,8 @@ pub fn zip_sample(file: &str) -> u64 { } /// Calls `checksum::digest` to attempt generating a checksum for the zip file. -pub fn digest_zip(path: &str) -> Option { - let size = zip_sample(path); +pub fn digest(path: &str) -> Option { + let size = sample_size(path); debug!( "Calculated sample size (in bytes) for generating checksum: {}", @@ -77,7 +73,7 @@ pub fn digest_zip(path: &str) -> Option { /// Processes a zip file in its entirety, includes: medatadata, page count, and the /// generated checksum for the file. // TODO: do I need to pass in the library options here? -pub fn process_zip(path: &Path) -> Result { +pub fn process(path: &Path) -> Result { debug!("Processing Zip: {}", path.display()); let zip_file = File::open(path)?; @@ -99,10 +95,16 @@ pub fn process_zip(path: &Path) -> Result } } + let checksum = if let Some(path) = path.to_str() { + digest(path) + } else { + None + }; + Ok(ProcessedMediaFile { thumbnail_path: None, path: path.to_path_buf(), - checksum: digest_zip(path.to_str().unwrap()), + checksum, metadata: comic_info, pages, }) @@ -111,7 +113,7 @@ pub fn process_zip(path: &Path) -> Result // FIXME: this solution is terrible, was just fighting with borrow checker and wanted // a quick solve. TODO: rework this! /// Get an image from a zip file by index (page). -pub fn get_zip_image( +pub fn get_image( file: &str, page: i32, ) -> Result<(ContentType, Vec), ProcessFileError> { @@ -139,7 +141,7 @@ pub fn get_zip_image( let mut contents = Vec::new(); // Note: guessing mime here since this file isn't accessible from the filesystem, // it lives inside the zip file. - let content_type = media_file::guess_content_type(name); + let content_type = ContentType::from_file(name); if images_seen + 1 == page && file.is_image() { trace!("Found target image: {}", name); diff --git a/core/src/fs/mod.rs b/core/src/fs/mod.rs index 7dc73e697..05b9fde65 100644 --- a/core/src/fs/mod.rs +++ b/core/src/fs/mod.rs @@ -1,7 +1,11 @@ +// TODO: remove pubs, expose only what is needed + pub mod archive; pub mod checksum; pub mod image; pub mod media_file; pub mod scanner; +mod traits; pub use media_file::*; +pub use traits::*; diff --git a/core/src/fs/scanner/batch_scanner.rs b/core/src/fs/scanner/batch_scanner.rs new file mode 100644 index 000000000..fabdb31b2 --- /dev/null +++ b/core/src/fs/scanner/batch_scanner.rs @@ -0,0 +1,210 @@ +use std::{ + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + time::Duration, +}; +use tokio::{self, task::JoinHandle}; +use tracing::{debug, error, trace}; + +use crate::{ + db::models::LibraryOptions, + event::CoreEvent, + fs::{ + image, + scanner::{ + setup::{setup_series, SeriesSetup}, + utils::{batch_media_operations, file_updated_since_scan}, + BatchScanOperation, ScannedFileTrait, + }, + }, + job::{persist_job_start, runner::RunnerCtx, JobUpdate}, + prelude::{CoreError, CoreResult, Ctx}, + prisma::series, +}; + +use super::setup::{setup_library, LibrarySetup}; + +// TODO: return result... +// TODO: investigate this with LARGE libraries. I am noticing the UI huff and puff a bit +// trying to keep up with the shear amount of updates it gets. I might have to throttle the +// updates to the UI when libraries reach a certain size and send updates in batches instead. +async fn scan_series( + ctx: Ctx, + series: series::Data, + library_path: &str, + library_options: LibraryOptions, + mut on_progress: impl FnMut(String) + Send + Sync + 'static, +) -> Vec { + debug!(?series, "Scanning series"); + let SeriesSetup { + mut visited_media, + media_by_path, + walkdir, + glob_set, + } = setup_series(&ctx, &series, library_path, &library_options).await; + + let mut operations = vec![]; + let iter = walkdir + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().is_file()); + for entry in iter { + let path = entry.path(); + let path_str = path.to_str().unwrap_or(""); + trace!(?path, "Scanning file"); + + // Tell client we are on the next file, this will increment the counter in the + // callback, as well. + on_progress(format!("Analyzing {:?}", path)); + + let glob_match = glob_set.is_match(path); + + if path.should_ignore() || glob_match { + trace!(?path, glob_match, "Skipping ignored file"); + continue; + } else if visited_media.get(path_str).is_some() { + trace!(media_path = ?path, "Existing media found"); + + let media = media_by_path.get(path_str).unwrap(); + let has_been_modified = + file_updated_since_scan(&entry, media.modified_at.clone()) + .map_err(|err| { + error!( + error = ?err, + path = path_str, + "Failed to determine if entry has been modified since last scan" + ) + }) + .unwrap_or(false); + + // If the file has been modified since the last scan, we need to update it. + if has_been_modified { + debug!(?media, "Media file has been modified since last scan"); + operations.push(BatchScanOperation::UpdateMedia(media.clone())); + } + + *visited_media.entry(path_str.to_string()).or_insert(true) = true; + continue; + } + + debug!(series_id = ?series.id, new_media_path = ?path, "New media found in series"); + operations.push(BatchScanOperation::CreateMedia { + path: path.to_path_buf(), + series_id: series.id.clone(), + }); + } + + visited_media + .into_iter() + .filter(|(_, visited)| !visited) + .for_each(|(path, _)| { + operations.push(BatchScanOperation::MarkMediaMissing { path }) + }); + + operations +} + +pub async fn scan(ctx: RunnerCtx, path: String, runner_id: String) -> CoreResult { + let core_ctx = ctx.core_ctx.clone(); + + ctx.progress(JobUpdate::job_initializing( + runner_id.clone(), + Some("Preparing library scan...".to_string()), + )); + + let LibrarySetup { + library, + library_options, + library_series, + tasks, + } = setup_library(&core_ctx, path).await?; + + // Sleep for a little to let the UI breathe. + tokio::time::sleep(Duration::from_millis(1000)).await; + persist_job_start(&core_ctx, runner_id.clone(), tasks).await?; + + let counter = Arc::new(AtomicU64::new(0)); + let handles: Vec>> = library_series + .into_iter() + .map(|s| { + let progress_ctx = ctx.clone(); + let scanner_ctx = core_ctx.clone(); + + let r_id = runner_id.clone(); + let counter_ref = counter.clone(); + let library_path = library.path.clone(); + + let library_options = library_options.clone(); + + tokio::spawn(async move { + scan_series(scanner_ctx, s, &library_path, library_options, move |msg| { + let previous = counter_ref.fetch_add(1, Ordering::SeqCst); + + progress_ctx.progress(JobUpdate::job_progress( + r_id.to_owned(), + Some(previous + 1), + tasks, + Some(msg), + )); + }) + .await + }) + }) + .collect(); + + let operations: Vec = futures::future::join_all(handles) + .await + .into_iter() + // TODO: log errors + .filter_map(|res| res.ok()) + .flatten() + .collect(); + + let final_count = counter.load(Ordering::SeqCst); + + let created_media = batch_media_operations(&core_ctx, operations, &library_options) + .await + .map_err(|e| { + error!("Failed to batch media operations: {:?}", e); + CoreError::InternalError(e.to_string()) + })?; + + if !created_media.is_empty() { + core_ctx + .emit_client_event(CoreEvent::CreatedMediaBatch(created_media.len() as u64)); + } + + // TODO: change task_count and send progress? + if library_options.create_webp_thumbnails { + trace!("Library configured to create WEBP thumbnails."); + + ctx.progress(JobUpdate::job_progress( + runner_id.clone(), + Some(final_count), + tasks, + Some(format!( + "Creating {} WEBP thumbnails (this can take some time)", + created_media.len() + )), + )); + + // sleep for a bit to let client catch up + tokio::time::sleep(Duration::from_millis(50)).await; + + if let Err(err) = image::generate_thumbnails(&created_media) { + error!("Failed to generate thumbnails: {:?}", err); + } + } + + ctx.progress(JobUpdate::job_finishing( + runner_id, + Some(final_count), + tasks, + None, + )); + tokio::time::sleep(Duration::from_millis(1000)).await; + + Ok(final_count) +} diff --git a/core/src/fs/scanner/library_scanner.rs b/core/src/fs/scanner/library_scanner.rs deleted file mode 100644 index eb7acc146..000000000 --- a/core/src/fs/scanner/library_scanner.rs +++ /dev/null @@ -1,642 +0,0 @@ -use globset::GlobSetBuilder; -use itertools::Itertools; -use rayon::prelude::{ParallelBridge, ParallelIterator}; -use std::{ - collections::HashMap, - path::Path, - sync::{ - atomic::{AtomicU64, Ordering}, - Arc, - }, - time::Duration, -}; -use tokio::{self, task::JoinHandle}; -use tracing::{debug, error, trace, warn}; -use walkdir::{DirEntry, WalkDir}; - -use crate::{ - config::context::Ctx, - event::CoreEvent, - fs::{ - image, - scanner::{ - utils::{insert_series_batch, mark_media_missing}, - ScannedFileTrait, - }, - }, - job::{persist_job_start, runner::RunnerCtx, JobUpdate}, - prisma::{library, media, series}, - types::{ - enums::FileStatus, errors::CoreError, models::library::LibraryOptions, - CoreResult, LibraryScanMode, - }, -}; - -use super::{ - utils::{batch_media_operations, mark_library_missing, populate_glob_builder}, - BatchScanOperation, -}; - -// FIXME: clean up usage of Ctx vs RunnerCtx... It can get confusing. - -// TODO: take in bottom up or top down scan option -fn check_series( - library_path: &str, - series: Vec, - library_options: &LibraryOptions, -) -> (Vec, Vec, Vec) { - let series_map = series - .iter() - .map(|data| (data.path.as_str(), false)) - .collect::>(); - - let missing_series = series - .iter() - .filter(|s| { - let path = Path::new(&s.path); - !path.exists() - }) - .map(|s| s.id.clone()) - .collect::>(); - - let mut walkdir = WalkDir::new(library_path); - - let is_collection_based = library_options.is_collection_based(); - - if is_collection_based { - walkdir = walkdir.max_depth(1); - } - - let new_entries = walkdir - // Set min_depth to 0 so we include the library path itself, - // which allows us to add it as a series when there are media items in it - .min_depth(0) - .into_iter() - .filter_entry(|e| e.path().is_dir()) - .filter_map(|e| e.ok()) - .par_bridge() - .filter(|entry| { - let path = entry.path(); - - let path_str = path.as_os_str().to_string_lossy().to_string(); - - if is_collection_based && path_str != library_path { - // If we're doing a top level scan, we need to check that the path - // has media deeply nested. Exception for when the path is the library path, - // then we only need to check if it has media in it directly - path.dir_has_media_deep() && !series_map.contains_key(path_str.as_str()) - } else { - // If we're doing a bottom up scan, we need to check that the path has - // media directly in it. - path.dir_has_media() && !series_map.contains_key(path_str.as_str()) - } - }) - .collect::>(); - - (series, missing_series, new_entries) -} - -/// Queries the database for the library by the given `path` and performs basic -/// checks to ensure the library is in a valid state for scanning. Returns the -/// library itself, its series, and the number of files that will be processed. -async fn precheck( - ctx: &Ctx, - path: String, - runner_id: &str, -) -> CoreResult<(library::Data, LibraryOptions, Vec, u64)> { - let db = ctx.get_db(); - - let library = db - .library() - .find_unique(library::path::equals(path.clone())) - .with(library::series::fetch(vec![])) - .with(library::library_options::fetch()) - .exec() - .await?; - - if library.is_none() { - return Err(CoreError::NotFound(format!("Library not found: {}", path))); - } - - let library = library.unwrap(); - - if !Path::new(&path).exists() { - mark_library_missing(library, ctx).await?; - - return Err(CoreError::FileNotFound(format!( - "Library path does not exist in fs: {}", - path - ))); - } - - let library_options: LibraryOptions = library - .library_options() - .map(|o| o.into()) - .unwrap_or_default(); - - let series = library.series()?.to_owned(); - - let (mut series, missing_series_ids, new_entries) = - check_series(&path, series, &library_options); - - if !missing_series_ids.is_empty() { - ctx.db - .series() - .update_many( - vec![series::id::in_vec(missing_series_ids)], - vec![series::status::set(FileStatus::Missing.to_string())], - ) - .exec() - .await?; - } - - if !new_entries.is_empty() { - trace!("Found {} new series", new_entries.len()); - - let insertion_result = - insert_series_batch(ctx, new_entries, library.id.clone()).await; - - if let Err(e) = insertion_result { - error!("Failed to batch insert series: {}", e); - - ctx.emit_client_event(CoreEvent::CreateEntityFailed { - runner_id: Some(runner_id.to_string()), - message: format!("Failed to batch insert series: {}", e), - path: path.clone(), - }); - } else { - let mut inserted_series = insertion_result.unwrap(); - - ctx.emit_client_event(CoreEvent::CreatedSeriesBatch( - inserted_series.len() as u64 - )); - - series.append(&mut inserted_series); - } - } - - let start = std::time::Instant::now(); - - let is_collection_based = library_options.is_collection_based(); - - // TODO: perhaps use rayon instead...? - // TODO: check this to see if it is still correct after adding the collection based - // vs series based options - let files_to_process: u64 = futures::future::join_all( - series - .iter() - .map(|data| { - let path = data.path.clone(); - - let mut series_walkdir = WalkDir::new(&path); - - // When the series is the library itself, we want to set the max_depth - // to 1 so it doesn't walk through the entire library (effectively doubling - // the return result, instead of the actual number of files to process) - if !is_collection_based || path == library.path { - series_walkdir = series_walkdir.max_depth(1) - } - - tokio::task::spawn_blocking(move || { - series_walkdir - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.path().is_file()) - .count() as u64 - }) - }) - .collect::>>(), - ) - .await - .into_iter() - .filter_map(|res| res.ok()) - .sum(); - - let duration = start.elapsed(); - - debug!( - "Files to process: {:?} (calculated in {}.{:03} seconds)", - files_to_process, - duration.as_secs(), - duration.subsec_millis() - ); - - Ok((library, library_options, series, files_to_process)) -} - -async fn scan_series( - ctx: Ctx, - runner_id: String, - series: series::Data, - library_path: &str, - library_options: LibraryOptions, - mut on_progress: impl FnMut(String) + Send + Sync + 'static, -) { - let db = ctx.get_db(); - - let series_ignore_file = Path::new(series.path.as_str()).join(".stumpignore"); - let library_ignore_file = Path::new(library_path).join(".stumpignore"); - - let media = db - .media() - .find_many(vec![media::series_id::equals(Some(series.id.clone()))]) - .exec() - .await - .unwrap(); - - let mut visited_media = media - .iter() - .map(|data| (data.path.clone(), false)) - .collect::>(); - - let mut walkdir = WalkDir::new(&series.path); - - let is_collection_based = library_options.is_collection_based(); - - if !is_collection_based || series.path == library_path { - walkdir = walkdir.max_depth(1); - } - - let mut builder = GlobSetBuilder::new(); - - if series_ignore_file.exists() || library_ignore_file.exists() { - populate_glob_builder( - &mut builder, - vec![series_ignore_file, library_ignore_file] - .into_iter() - .unique() - .filter(|p| p.exists()) - .collect::>() - .as_slice(), - ); - } - - // TODO: make this an error to enforce correct glob patterns in an ignore file. - // This way, no scan will ever add things a user wants to ignore. - let glob_set = builder.build().unwrap(); - - for entry in walkdir - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.path().is_file()) - { - let path = entry.path(); - let path_str = path.to_str().unwrap_or(""); - - debug!("Currently scanning: {:?}", path); - - // Tell client we are on the next file, this will increment the counter in the - // callback, as well. - on_progress(format!("Analyzing {:?}", path)); - - let glob_match = glob_set.is_match(path); - // println!("Path: {:?} -> Matches: {}", path, glob_match); - - if path.should_ignore() || glob_match { - trace!("Skipping ignored file: {:?}", path); - trace!("Globbed ignore?: {}", glob_match); - continue; - } else if visited_media.get(path_str).is_some() { - debug!("Existing media found: {:?}", path); - *visited_media.entry(path_str.to_string()).or_insert(true) = true; - continue; - } - - debug!("New media found at {:?} in series {:?}", &path, &series.id); - - match super::utils::insert_media(&ctx, path, series.id.clone(), &library_options) - .await - { - Ok(media) => { - visited_media.insert(media.path.clone(), true); - - ctx.emit_client_event(CoreEvent::CreatedMedia(media.clone())); - }, - Err(e) => { - error!("Failed to insert media: {:?}", e); - - ctx.handle_failure_event(CoreEvent::CreateEntityFailed { - runner_id: Some(runner_id.clone()), - path: path.to_str().unwrap_or_default().to_string(), - message: e.to_string(), - }) - .await; - }, - } - } - - let missing_media = visited_media - .into_iter() - .filter(|(_, visited)| !visited) - .map(|(path, _)| path) - .collect::>(); - - if missing_media.is_empty() { - warn!( - "{} media were unable to be located during scan.", - missing_media.len(), - ); - - debug!("Missing media paths: {:?}", missing_media); - - let result = mark_media_missing(&ctx, missing_media).await; - - if let Err(err) = result { - error!("Failed to mark media as MISSING: {:?}", err); - } else { - debug!("Marked {} media as MISSING", result.unwrap()); - } - } -} - -// Note: if this function signature gets much larger I probably want to refactor it... -// TODO: return result... -// TODO: investigate this with LARGE libraries. I am noticing the UI huff and puff a bit -// trying to keep up with the shear amount of updates it gets. I might have to throttle the -// updates to the UI when libraries reach a certain size and send updates in batches instead. -async fn scan_series_batch( - ctx: Ctx, - series: series::Data, - library_path: &str, - library_options: LibraryOptions, - mut on_progress: impl FnMut(String) + Send + Sync + 'static, -) -> Vec { - let db = ctx.get_db(); - - let series_ignore_file = Path::new(series.path.as_str()).join(".stumpignore"); - let library_ignore_file = Path::new(library_path).join(".stumpignore"); - - let media = db - .media() - .find_many(vec![media::series_id::equals(Some(series.id.clone()))]) - .exec() - .await - .unwrap(); - - let mut visited_media = media - .iter() - .map(|data| (data.path.clone(), false)) - .collect::>(); - - let mut operations = vec![]; - - let mut walkdir = WalkDir::new(&series.path); - - let is_collection_based = library_options.is_collection_based(); - - if !is_collection_based || series.path == library_path { - walkdir = walkdir.max_depth(1); - } - - let mut builder = GlobSetBuilder::new(); - - if series_ignore_file.exists() || library_ignore_file.exists() { - populate_glob_builder( - &mut builder, - vec![series_ignore_file, library_ignore_file] - .into_iter() - // We have to remove duplicates here otherwise the glob will double some patterns. - // An example would be when the library has media in root. Not the end of the world. - .unique() - .filter(|p| p.exists()) - .collect::>() - .as_slice(), - ); - } - - // TODO: make this an error to enforce correct glob patterns in an ignore file. - // This way, no scan will ever add things a user wants to ignore. - let glob_set = builder.build().unwrap_or_default(); - - for entry in walkdir - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.path().is_file()) - { - let path = entry.path(); - let path_str = path.to_str().unwrap_or(""); - - debug!("Currently scanning: {:?}", path); - - // Tell client we are on the next file, this will increment the counter in the - // callback, as well. - on_progress(format!("Analyzing {:?}", path)); - - let glob_match = glob_set.is_match(path); - // println!("Path: {:?} -> Matches: {}", path, glob_match); - - if path.should_ignore() || glob_match { - trace!("Skipping ignored file: {:?}", path); - trace!("Globbed ignore?: {}", glob_match); - continue; - } else if visited_media.get(path_str).is_some() { - debug!("Existing media found: {:?}", path); - *visited_media.entry(path_str.to_string()).or_insert(true) = true; - continue; - } - - debug!("New media found at {:?} in series {:?}", &path, &series.id); - - operations.push(BatchScanOperation::CreateMedia { - path: path.to_path_buf(), - series_id: series.id.clone(), - }); - } - - visited_media - .into_iter() - .filter(|(_, visited)| !visited) - .for_each(|(path, _)| { - operations.push(BatchScanOperation::MarkMediaMissing { path }) - }); - - operations -} - -pub async fn scan_batch( - ctx: RunnerCtx, - path: String, - runner_id: String, -) -> CoreResult { - let core_ctx = ctx.core_ctx.clone(); - - ctx.progress(JobUpdate::job_initializing( - runner_id.clone(), - Some("Preparing library scan...".to_string()), - )); - - let (library, library_options, series, files_to_process) = - precheck(&core_ctx, path, &runner_id).await?; - // Sleep for a little to let the UI breathe. - tokio::time::sleep(Duration::from_millis(1000)).await; - - let _job = persist_job_start(&core_ctx, runner_id.clone(), files_to_process).await?; - - let counter = Arc::new(AtomicU64::new(0)); - - let tasks: Vec>> = series - .into_iter() - .map(|s| { - let progress_ctx = ctx.clone(); - let scanner_ctx = core_ctx.clone(); - - let r_id = runner_id.clone(); - let counter_ref = counter.clone(); - let library_path = library.path.clone(); - - let library_options = library_options.clone(); - - tokio::spawn(async move { - scan_series_batch( - scanner_ctx, - s, - &library_path, - library_options, - move |msg| { - let previous = counter_ref.fetch_add(1, Ordering::SeqCst); - - progress_ctx.progress(JobUpdate::job_progress( - r_id.to_owned(), - Some(previous + 1), - files_to_process, - Some(msg), - )); - }, - ) - .await - }) - }) - .collect(); - - let operations: Vec = futures::future::join_all(tasks) - .await - .into_iter() - // TODO: log errors - .filter_map(|res| res.ok()) - .flatten() - .collect(); - - let final_count = counter.load(Ordering::SeqCst); - - let created_media = batch_media_operations(&core_ctx, operations, &library_options) - .await - .map_err(|e| { - error!("Failed to batch media operations: {:?}", e); - CoreError::InternalError(e.to_string()) - })?; - - if !created_media.is_empty() { - core_ctx - .emit_client_event(CoreEvent::CreatedMediaBatch(created_media.len() as u64)); - } - - // TODO: change task_count and send progress? - if library_options.create_webp_thumbnails { - trace!("Library configured to create WEBP thumbnails."); - - ctx.progress(JobUpdate::job_progress( - runner_id.clone(), - Some(final_count), - files_to_process, - Some(format!( - "Creating {} WEBP thumbnails (this can take some time)", - created_media.len() - )), - )); - - // sleep for a bit to let client catch up - tokio::time::sleep(Duration::from_millis(50)).await; - - if let Err(err) = image::generate_thumbnails(created_media) { - error!("Failed to generate thumbnails: {:?}", err); - } - } - - ctx.progress(JobUpdate::job_finishing( - runner_id, - Some(final_count), - files_to_process, - None, - )); - tokio::time::sleep(Duration::from_millis(1000)).await; - - Ok(final_count) -} - -pub async fn scan_sync( - ctx: RunnerCtx, - path: String, - runner_id: String, -) -> CoreResult { - let core_ctx = ctx.core_ctx.clone(); - - let (library, library_options, series, files_to_process) = - precheck(&core_ctx, path, &runner_id).await?; - - // TODO: I am not sure if jobs should fail when the job fails to persist to DB. - let _job = persist_job_start(&core_ctx, runner_id.clone(), files_to_process).await?; - - ctx.progress(JobUpdate::job_started( - runner_id.clone(), - 0, - files_to_process, - Some(format!("Starting library scan at {}", &library.path)), - )); - - let counter = Arc::new(AtomicU64::new(0)); - - for s in series { - let progress_ctx = ctx.clone(); - let scanner_ctx = core_ctx.clone(); - let r_id = runner_id.clone(); - - let counter_ref = counter.clone(); - let runner_id = runner_id.clone(); - let library_path = library.path.clone(); - // Note: I don't ~love~ having to clone this struct each iteration. I think it's fine for now, - // considering it consists of just a few booleans. - let library_options = library_options.clone(); - - scan_series( - scanner_ctx, - runner_id, - s, - &library_path, - library_options, - move |msg| { - let previous = counter_ref.fetch_add(1, Ordering::SeqCst); - - progress_ctx.progress(JobUpdate::job_progress( - r_id.to_owned(), - Some(previous + 1), - files_to_process, - Some(msg), - )); - }, - ) - .await; - } - - ctx.progress(JobUpdate::job_finishing( - runner_id, - Some(counter.load(Ordering::SeqCst)), - files_to_process, - None, - )); - tokio::time::sleep(Duration::from_millis(1000)).await; - - Ok(counter.load(Ordering::SeqCst)) -} - -pub async fn scan( - ctx: RunnerCtx, - path: String, - runner_id: String, - scan_mode: LibraryScanMode, -) -> CoreResult { - match scan_mode { - LibraryScanMode::Batched => scan_batch(ctx, path, runner_id).await, - LibraryScanMode::Sync => scan_sync(ctx, path, runner_id).await, - _ => unreachable!("A job should not have reached this point if the scan mode is not batch or sync."), - } -} diff --git a/core/src/fs/scanner/mod.rs b/core/src/fs/scanner/mod.rs index acdd6bcf5..b736b38fc 100644 --- a/core/src/fs/scanner/mod.rs +++ b/core/src/fs/scanner/mod.rs @@ -1,16 +1,31 @@ use std::path::{Path, PathBuf}; -pub mod library_scanner; -pub mod utils; -use tracing::debug; +mod batch_scanner; +mod setup; +mod sync_scanner; +mod utils; use walkdir::WalkDir; use crate::{ - fs::media_file::{self, guess_mime}, - types::ContentType, + db::models::{LibraryScanMode, Media}, + job::runner::RunnerCtx, + prelude::{ContentType, CoreResult}, }; +pub async fn scan( + ctx: RunnerCtx, + path: String, + runner_id: String, + scan_mode: LibraryScanMode, +) -> CoreResult { + match scan_mode { + LibraryScanMode::Batched => batch_scanner::scan(ctx, path, runner_id).await, + LibraryScanMode::Sync => sync_scanner::scan(ctx, path, runner_id).await, + _ => unreachable!("A job should not have reached this point if the scan mode is not batch or sync."), + } +} + // TODO: refactor this trait? yes please pub trait ScannedFileTrait { fn get_kind(&self) -> std::io::Result>; @@ -41,20 +56,8 @@ impl ScannedFileTrait for Path { /// Returns true if the file is a supported media file. This is a strict check when /// infer can determine the file type, and a loose extension-based check when infer cannot. fn is_supported(&self) -> bool { - if let Ok(Some(typ)) = infer::get_from_path(self) { - let mime = typ.mime_type(); - let content_type = media_file::get_content_type_from_mime(mime); - - return content_type != ContentType::UNKNOWN; - } - - if let Some(guessed_mime) = guess_mime(self) { - return !guessed_mime.starts_with("image/"); - } - - debug!("Unsupported file {:?}", self); - - false + let content_type = ContentType::from_path(self); + content_type != ContentType::UNKNOWN && !content_type.is_image() } /// Returns true when the scanner should not persist the file to the database. @@ -74,18 +77,7 @@ impl ScannedFileTrait for Path { /// Returns true if the file is an image. This is a strict check when infer /// can determine the file type, and a loose extension-based check when infer cannot. fn is_img(&self) -> bool { - if let Ok(Some(file_type)) = infer::get_from_path(self) { - return file_type.mime_type().starts_with("image/"); - } - - // TODO: more, or refactor. Too lazy rn - self.extension() - .map(|ext| { - ext.eq_ignore_ascii_case("jpg") - || ext.eq_ignore_ascii_case("png") - || ext.eq_ignore_ascii_case("jpeg") - }) - .unwrap_or(false) + ContentType::from_path(self).is_image() } /// Returns true if the file is a thumbnail image. This calls the `is_img` function @@ -146,8 +138,11 @@ impl ScannedFileTrait for Path { } } +// FIXME: I don't want to allow this, however Box won't work +#[allow(clippy::large_enum_variant)] pub enum BatchScanOperation { CreateMedia { path: PathBuf, series_id: String }, + UpdateMedia(Media), MarkMediaMissing { path: String }, // Note: this will be tricky. I will need to have this as a separate operation so I don't chance // issuing concurrent writes to the database. But will be a bit of a pain, not too bad though. diff --git a/core/src/fs/scanner/setup.rs b/core/src/fs/scanner/setup.rs new file mode 100644 index 000000000..a57eae562 --- /dev/null +++ b/core/src/fs/scanner/setup.rs @@ -0,0 +1,266 @@ +use std::{collections::HashMap, path::Path}; + +use globset::{GlobSet, GlobSetBuilder}; +use itertools::Itertools; +use rayon::prelude::{ParallelBridge, ParallelIterator}; +use tokio::task::JoinHandle; +use tracing::{debug, error, trace}; +use walkdir::{DirEntry, WalkDir}; + +use crate::{ + db::{ + models::{LibraryOptions, Media}, + Dao, SeriesDao, SeriesDaoImpl, + }, + event::CoreEvent, + fs::scanner::utils::{insert_series_batch, mark_library_missing}, + prelude::{CoreError, CoreResult, Ctx, FileStatus}, + prisma::{library, series}, +}; + +use super::{utils::populate_glob_builder, ScannedFileTrait}; + +pub struct LibrarySetup { + pub library: library::Data, + pub library_options: LibraryOptions, + pub library_series: Vec, + pub tasks: u64, +} + +pub(crate) async fn setup_library(ctx: &Ctx, path: String) -> CoreResult { + let start = std::time::Instant::now(); + + let db = ctx.get_db(); + let library = db + .library() + .find_unique(library::path::equals(path.clone())) + .with(library::series::fetch(vec![])) + .with(library::library_options::fetch()) + .exec() + .await?; + + if library.is_none() { + return Err(CoreError::NotFound(format!("Library not found: {}", path))); + } + + let library = library.unwrap(); + + if !Path::new(&path).exists() { + mark_library_missing(library, ctx).await?; + + return Err(CoreError::FileNotFound(format!( + "Library path does not exist in fs: {}", + path + ))); + } + + let library_options: LibraryOptions = library + .library_options() + .map(LibraryOptions::from) + .unwrap_or_default(); + + let is_collection_based = library_options.is_collection_based(); + let series = setup_library_series(ctx, &library, is_collection_based).await?; + + let is_collection_based = library_options.is_collection_based(); + let tasks: u64 = futures::future::join_all( + series + .iter() + .map(|data| { + let path = data.path.clone(); + + let mut series_walkdir = WalkDir::new(&path); + + // When the series is the library itself, we want to set the max_depth + // to 1 so it doesn't walk through the entire library (effectively doubling + // the return result, instead of the actual number of files to process) + if !is_collection_based || path == library.path { + series_walkdir = series_walkdir.max_depth(1) + } + + tokio::task::spawn_blocking(move || { + series_walkdir + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().is_file()) + .count() as u64 + }) + }) + .collect::>>(), + ) + .await + .into_iter() + .filter_map(|res| res.ok()) + .sum(); + + let duration = start.elapsed(); + let seconds = duration.as_secs(); + let setup_time = format!("{seconds}.{:03} seconds", duration.subsec_millis()); + + debug!( + task_count = tasks, + ?setup_time, + "Scan setup for library completed" + ); + + Ok(LibrarySetup { + library, + library_options, + library_series: series, + tasks, + }) +} + +async fn setup_library_series( + ctx: &Ctx, + library: &library::Data, + is_collection_based: bool, +) -> CoreResult> { + let library_path = library.path.clone(); + let mut series = library.series()?.to_owned(); + let series_map = series + .iter() + .map(|data| (data.path.as_str(), false)) + .collect::>(); + + let missing_series = series + .iter() + .filter(|s| { + let path = Path::new(&s.path); + !path.exists() + }) + .map(|s| s.id.clone()) + .collect::>(); + + let mut walkdir = WalkDir::new(library_path.as_str()); + + if is_collection_based { + walkdir = walkdir.max_depth(1); + } + + let new_entries = walkdir + // Set min_depth to 0 so we include the library path itself, + // which allows us to add it as a series when there are media items in it + .min_depth(0) + .into_iter() + .filter_entry(|e| e.path().is_dir()) + .filter_map(|e| e.ok()) + .par_bridge() + .filter(|entry| { + let path = entry.path(); + let path_str = path.as_os_str().to_string_lossy().to_string(); + + if is_collection_based && path_str != library_path { + // If we're doing a top level scan, we need to check that the path + // has media deeply nested. Exception for when the path is the library path, + // then we only need to check if it has media in it directly + path.dir_has_media_deep() && !series_map.contains_key(path_str.as_str()) + } else { + // If we're doing a bottom up scan, we need to check that the path has + // media directly in it. + path.dir_has_media() && !series_map.contains_key(path_str.as_str()) + } + }) + .collect::>(); + + if !missing_series.is_empty() { + ctx.db + .series() + .update_many( + vec![series::id::in_vec(missing_series)], + vec![series::status::set(FileStatus::Missing.to_string())], + ) + .exec() + .await?; + } + + if !new_entries.is_empty() { + trace!(new_series_count = new_entries.len(), "Inserting new series"); + // TODO: replace with dao? + let result = insert_series_batch(ctx, new_entries, library.id.clone()).await; + if let Err(e) = result { + error!("Failed to batch insert series: {}", e); + + // TODO: uncomment once ctx has runner_id + // ctx.emit_client_event(CoreEvent::CreateEntityFailed { + // runner_id: Some(runner_id.to_string()), + // message: format!("Failed to batch insert series: {}", e), + // path: library_path.clone(), + // }); + } else { + let mut inserted_series = result.unwrap(); + ctx.emit_client_event(CoreEvent::CreatedSeriesBatch( + inserted_series.len() as u64 + )); + series.append(&mut inserted_series); + } + } + + Ok(series) +} + +pub struct SeriesSetup { + pub visited_media: HashMap, + pub media_by_path: HashMap, + pub walkdir: WalkDir, + pub glob_set: GlobSet, +} + +pub(crate) async fn setup_series( + ctx: &Ctx, + series: &series::Data, + library_path: &str, + library_options: &LibraryOptions, +) -> SeriesSetup { + let series_dao = SeriesDaoImpl::new(ctx.db.clone()); + let series_ignore_file = Path::new(series.path.as_str()).join(".stumpignore"); + let library_ignore_file = Path::new(library_path).join(".stumpignore"); + + let media = series_dao + .get_series_media(series.id.as_str()) + .await + .unwrap_or_else(|e| { + error!(error = ?e, "Error occurred trying to fetch media for series"); + vec![] + }); + + let mut visited_media = HashMap::with_capacity(media.len()); + let mut media_by_path = HashMap::with_capacity(media.len()); + for m in media { + visited_media.insert(m.path.clone(), false); + media_by_path.insert(m.path.clone(), m); + } + + let mut walkdir = WalkDir::new(&series.path); + let is_collection_based = library_options.is_collection_based(); + + if !is_collection_based || series.path == library_path { + walkdir = walkdir.max_depth(1); + } + + let mut builder = GlobSetBuilder::new(); + if series_ignore_file.exists() || library_ignore_file.exists() { + populate_glob_builder( + &mut builder, + vec![series_ignore_file, library_ignore_file] + .into_iter() + // We have to remove duplicates here otherwise the glob will double some patterns. + // An example would be when the library has media in root. Not the end of the world. + .unique() + .filter(|p| p.exists()) + .collect::>() + .as_slice(), + ); + } + + // TODO: make this an error to enforce correct glob patterns in an ignore file. + // This way, no scan will ever add things a user wants to ignore. + let glob_set = builder.build().unwrap_or_default(); + + SeriesSetup { + visited_media, + media_by_path, + walkdir, + glob_set, + } +} diff --git a/core/src/fs/scanner/sync_scanner.rs b/core/src/fs/scanner/sync_scanner.rs new file mode 100644 index 000000000..2d6d2dd4a --- /dev/null +++ b/core/src/fs/scanner/sync_scanner.rs @@ -0,0 +1,185 @@ +use std::{ + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + time::Duration, +}; +use tracing::{debug, error, trace, warn}; + +use crate::{ + db::models::LibraryOptions, + event::CoreEvent, + fs::scanner::{ + setup::{setup_series, SeriesSetup}, + utils, ScannedFileTrait, + }, + job::{persist_job_start, runner::RunnerCtx, JobUpdate}, + prelude::{CoreResult, Ctx}, + prisma::series, +}; + +use super::setup::{setup_library, LibrarySetup}; + +async fn scan_series( + ctx: Ctx, + runner_id: String, + series: series::Data, + library_path: &str, + library_options: LibraryOptions, + mut on_progress: impl FnMut(String) + Send + Sync + 'static, +) { + debug!(?series, "Scanning series"); + let SeriesSetup { + mut visited_media, + media_by_path, + walkdir, + glob_set, + } = setup_series(&ctx, &series, library_path, &library_options).await; + + for entry in walkdir + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().is_file()) + { + let path = entry.path(); + let path_str = path.to_str().unwrap_or(""); + trace!(?path, "Scanning file"); + + // Tell client we are on the next file, this will increment the counter in the + // callback, as well. + on_progress(format!("Analyzing {:?}", path)); + + let glob_match = glob_set.is_match(path); + + if path.should_ignore() || glob_match { + trace!(?path, glob_match, "Skipping ignored file"); + continue; + } else if visited_media.get(path_str).is_some() { + trace!(media_path = ?path, "Existing media found"); + let media = media_by_path.get(path_str).unwrap(); + let check_modified_at = + utils::file_updated_since_scan(&entry, media.modified_at.clone()); + + if let Ok(has_been_modified) = check_modified_at { + // If the file has been modified since the last scan, we need to update it. + if has_been_modified { + debug!(?media, "Media file has been modified since last scan"); + // TODO: do something with media_updates + warn!( + outdated_media = ?media, + "Stump does not support updating media entities yet", + ); + } + } + + *visited_media.entry(path_str.to_string()).or_insert(true) = true; + continue; + } + + debug!(series_id = ?series.id, new_media_path = ?path, "New media found in series"); + match utils::insert_media(&ctx, path, series.id.clone(), &library_options).await { + Ok(media) => { + visited_media.insert(media.path.clone(), true); + ctx.emit_client_event(CoreEvent::CreatedMedia(Box::new(media))); + }, + Err(e) => { + error!(error = ?e, "Failed to create media"); + ctx.handle_failure_event(CoreEvent::CreateEntityFailed { + runner_id: Some(runner_id.clone()), + path: path.to_str().unwrap_or_default().to_string(), + message: e.to_string(), + }) + .await; + }, + } + } + + let missing_media = visited_media + .into_iter() + .filter(|(_, visited)| !visited) + .map(|(path, _)| path) + .collect::>(); + + if missing_media.is_empty() { + warn!( + missing_paths = ?missing_media, + "Some media files could not be located during scan." + ); + let result = utils::mark_media_missing(&ctx, missing_media).await; + + if let Err(err) = result { + error!(error = ?err, "Failed to mark media as MISSING"); + } else { + debug!("Marked {} media as MISSING", result.unwrap()); + } + } +} + +pub async fn scan(ctx: RunnerCtx, path: String, runner_id: String) -> CoreResult { + let core_ctx = ctx.core_ctx.clone(); + + let LibrarySetup { + library, + library_options, + library_series, + tasks, + } = setup_library(&core_ctx, path).await?; + + // Sleep for a little to let the UI breathe. + tokio::time::sleep(Duration::from_millis(700)).await; + + // TODO: I am not sure if jobs should fail when the job fails to persist to DB. + let _job = persist_job_start(&core_ctx, runner_id.clone(), tasks).await?; + + ctx.progress(JobUpdate::job_started( + runner_id.clone(), + 0, + tasks, + Some(format!("Starting library scan at {}", &library.path)), + )); + + let counter = Arc::new(AtomicU64::new(0)); + + for series in library_series { + let progress_ctx = ctx.clone(); + let scanner_ctx = core_ctx.clone(); + let r_id = runner_id.clone(); + + let counter_ref = counter.clone(); + let runner_id = runner_id.clone(); + let library_path = library.path.clone(); + // Note: I don't ~love~ having to clone this struct each iteration. I think it's fine for now, + // considering it consists of just a few booleans. + let library_options = library_options.clone(); + + scan_series( + scanner_ctx, + runner_id, + series, + &library_path, + library_options, + move |msg| { + let previous = counter_ref.fetch_add(1, Ordering::SeqCst); + + progress_ctx.progress(JobUpdate::job_progress( + r_id.to_owned(), + Some(previous + 1), + tasks, + Some(msg), + )); + }, + ) + .await; + } + + ctx.progress(JobUpdate::job_finishing( + runner_id, + Some(counter.load(Ordering::SeqCst)), + tasks, + None, + )); + tokio::time::sleep(Duration::from_millis(1000)).await; + + Ok(counter.load(Ordering::SeqCst)) +} diff --git a/core/src/fs/scanner/utils.rs b/core/src/fs/scanner/utils.rs index 29a740dee..045a88633 100644 --- a/core/src/fs/scanner/utils.rs +++ b/core/src/fs/scanner/utils.rs @@ -5,203 +5,196 @@ use std::{ }; use globset::{Glob, GlobSetBuilder}; -use prisma_client_rust::{raw, PrismaValue, QueryError}; -use tracing::{debug, error, trace}; +use prisma_client_rust::{ + chrono::{DateTime, Utc}, + QueryError, +}; +use tracing::{debug, error, trace, warn}; use walkdir::DirEntry; use crate::{ - config::context::Ctx, - event::CoreEvent, - fs::{image, media_file}, - prisma::{library, media, series}, - types::{ - enums::FileStatus, - errors::ScanError, - models::{library::LibraryOptions, media::TentativeMedia}, - CoreResult, + db::{ + models::{LibraryOptions, Media, MediaBuilder, MediaBuilderOptions}, + Dao, DaoBatch, MediaDao, MediaDaoImpl, }, + fs::{image, media_file, scanner::BatchScanOperation}, + prelude::{CoreError, CoreResult, Ctx, FileStatus}, + prisma::{library, media, series}, }; -use super::BatchScanOperation; +// TODO: I hate this here, but I don't know where else to put it. +// Like naming variables, stuff like this is hard lol +impl MediaBuilder for Media { + fn build(path: &Path, series_id: &str) -> CoreResult { + Media::build_with_options( + path, + MediaBuilderOptions { + series_id: series_id.to_string(), + ..Default::default() + }, + ) + } -/// Will mark all series and media within the library as MISSING. Requires the -/// series and series.media relations to have been loaded to function properly. -pub async fn mark_library_missing(library: library::Data, ctx: &Ctx) -> CoreResult<()> { - let db = ctx.get_db(); + fn build_with_options( + path: &Path, + options: MediaBuilderOptions, + ) -> CoreResult { + let processed_entry = media_file::process(path, &options.library_options)?; - db._execute_raw(raw!( - "UPDATE series SET status={} WHERE library_id={}", - PrismaValue::String("MISSING".to_owned()), - PrismaValue::String(library.id.clone()) - )) - .exec() - .await?; - - let media_query = format!( - "UPDATE media SET status\"{}\" WHERE seriesId in ({})", - "MISSING".to_owned(), - library - .series() - .unwrap_or(&vec![]) - .iter() - .cloned() - .map(|s| format!("\"{}\"", s.id)) - .collect::>() - .join(",") - ); + let pathbuf = processed_entry.path; + let path = pathbuf.as_path(); - db._execute_raw(raw!(&media_query)).exec().await?; + let path_str = path.to_str().unwrap_or_default().to_string(); - Ok(()) -} + let name = path + .file_stem() + .unwrap_or_default() + .to_str() + .unwrap_or_default() + .to_string(); -pub fn get_tentative_media( - path: &Path, - series_id: String, - library_options: &LibraryOptions, -) -> Result { - let processed_entry = media_file::process(path, library_options)?; + let ext = path + .extension() + .unwrap_or_default() + .to_str() + .unwrap_or_default() + .to_string(); - let pathbuf = processed_entry.path; - let path = pathbuf.as_path(); + // Note: make this return a tuple if I need to grab anything else from metadata. + let size = match path.metadata() { + Ok(metadata) => metadata.len(), + _ => 0, + }; - let path_str = path.to_str().unwrap_or_default().to_string(); + let comic_info = processed_entry.metadata.unwrap_or_default(); - // EW, I hate that I need to do this over and over lol time to make a trait for Path. - let name = path - .file_stem() - .unwrap_or_default() - .to_str() - .unwrap_or_default() - .to_string(); - - let ext = path - .extension() - .unwrap_or_default() - .to_str() - .unwrap_or_default() - .to_string(); - - // Note: make this return a tuple if I need to grab anything else from metadata. - let size = match path.metadata() { - Ok(metadata) => metadata.len(), - _ => 0, - }; - - let comic_info = processed_entry.metadata.unwrap_or_default(); - - Ok(TentativeMedia { - name, - description: comic_info.summary, - size: size.try_into().unwrap_or_else(|e| { - error!("Failed to calculate file size: {:?}", e); - - 0 - }), - extension: ext, - pages: match comic_info.page_count { - Some(count) => count as i32, - None => processed_entry.pages, - }, - checksum: processed_entry.checksum, - path: path_str, - series_id, - }) + Ok(Media { + name, + description: comic_info.summary, + size: size.try_into().unwrap_or_else(|e| { + error!("Failed to calculate file size: {:?}", e); + + 0 + }), + extension: ext, + pages: match comic_info.page_count { + Some(count) => count as i32, + None => processed_entry.pages, + }, + checksum: processed_entry.checksum, + path: path_str, + series_id: options.series_id, + ..Default::default() + }) + } } -pub async fn insert_media( - ctx: &Ctx, - path: &Path, - series_id: String, - library_options: &LibraryOptions, -) -> Result { - let path_str = path.to_str().unwrap_or_default().to_string(); - - let tentative_media = get_tentative_media(path, series_id, library_options)?; - let create_action = tentative_media.into_action(ctx); - let media = create_action.exec().await?; +pub(crate) fn file_updated_since_scan( + entry: &DirEntry, + last_modified_at: String, +) -> CoreResult { + if let Ok(Ok(system_time)) = entry.metadata().map(|m| m.modified()) { + let media_modified_at = + last_modified_at.parse::>().map_err(|e| { + error!( + path = ?entry.path(), + error = ?e, + "Error occurred trying to read modified date for media", + ); + + CoreError::Unknown(e.to_string()) + })?; + let system_time_converted: DateTime = system_time.into(); + trace!(?system_time_converted, ?media_modified_at,); + + if system_time_converted > media_modified_at { + return Ok(true); + } - trace!("Media entity created: {:?}", media); + Ok(false) + } else { + error!( + path = ?entry.path(), + "Error occurred trying to read modified date for media", + ); - if library_options.create_webp_thumbnails { - debug!("Attempting to create WEBP thumbnail"); - let thumbnail_path = image::generate_thumbnail(&media.id, &path_str)?; - debug!("Created WEBP thumbnail: {:?}", thumbnail_path); + Ok(true) } - - debug!("Media for {} created successfully", path_str); - - Ok(media) } -pub async fn insert_series( - ctx: &Ctx, - entry: &DirEntry, - library_id: String, -) -> Result { - let path = entry.path(); - - // TODO: use this?? - // let metadata = match path.metadata() { - // Ok(metadata) => Some(metadata), - // _ => None, - // }; - - // TODO: change error - let name = match path.file_name() { - Some(name) => match name.to_str() { - Some(name) => name.to_string(), - _ => { - return Err(ScanError::FileParseError( - "Failed to get name for series".to_string(), - )) - }, - }, - _ => { - return Err(ScanError::FileParseError( - "Failed to get name for series".to_string(), - )) - }, - }; +/// Will mark all series and media within the library as MISSING. Requires the +/// series and series.media relations to have been loaded to function properly. +pub async fn mark_library_missing(library: library::Data, ctx: &Ctx) -> CoreResult<()> { + let db = ctx.get_db(); - let series = ctx - .db + let library_id = library.id.clone(); + let series_ids = library .series() - .create( - name, - path.to_str().unwrap().to_string(), - vec![series::library::connect(library::id::equals(library_id))], - ) - .exec() + .unwrap_or(&vec![]) + .iter() + .map(|s| s.id.clone()) + .collect(); + + let result = db + ._transaction() + .run(|client| async move { + client + .series() + .update_many( + vec![series::library_id::equals(Some(library.id.clone()))], + vec![series::status::set(FileStatus::Missing.to_string())], + ) + .exec() + .await?; + + client + .media() + .update_many( + vec![media::series_id::in_vec(series_ids)], + vec![media::status::set(FileStatus::Missing.to_string())], + ) + .exec() + .await + }) .await?; - debug!("Created new series: {:?}", series); + debug!( + records_updated = result, + ?library_id, + "Marked library as missing" + ); - Ok(series) + Ok(()) } -// TODO: remove -pub async fn insert_series_many( +pub async fn insert_media( ctx: &Ctx, - entries: Vec, - library_id: String, -) -> Vec { - let mut inserted_series = vec![]; + path: &Path, + series_id: String, + library_options: &LibraryOptions, +) -> CoreResult { + let path_str = path.to_str().unwrap_or_default().to_string(); + let media_dao = MediaDaoImpl::new(ctx.db.clone()); + let media = Media::build_with_options( + path, + MediaBuilderOptions { + series_id, + library_options: library_options.clone(), + }, + )?; + let created_media = media_dao.insert(media).await?; - for entry in entries { - match insert_series(ctx, &entry, library_id.clone()).await { - Ok(series) => { - ctx.emit_client_event(CoreEvent::CreatedSeries(series.clone())); + trace!("Media entity created: {:?}", created_media); - inserted_series.push(series); - }, - Err(e) => { - error!("Failed to insert series: {:?}", e); - }, - } + if library_options.create_webp_thumbnails { + debug!("Attempting to create WEBP thumbnail"); + let thumbnail_path = image::generate_thumbnail(&created_media.id, &path_str)?; + debug!("Created WEBP thumbnail: {:?}", thumbnail_path); } - inserted_series + debug!("Media for {} created successfully", path_str); + + Ok(created_media) } pub async fn insert_series_batch( @@ -209,31 +202,35 @@ pub async fn insert_series_batch( entries: Vec, library_id: String, ) -> CoreResult> { - let series_creates = entries.into_iter().map(|entry| { - let path = entry.path(); + let series_creates = entries + .into_iter() + .map(|entry| { + let path = entry.path(); - // TODO: figure out how to do this in the safest way possible... - let name = path - .file_name() - .unwrap_or_default() - .to_str() - .unwrap_or_default() - .to_string(); + let file_name = path + .file_name() + .and_then(|file_name| file_name.to_str().map(String::from)); + let path_str = path.to_str().map(String::from); - // TODO: change this to a Result, then filter map on the iterator and - // log the Err values... - ctx.db.series().create( - name, - path.to_str().unwrap_or_default().to_string(), - vec![series::library::connect(library::id::equals( - library_id.clone(), - ))], - ) - }); + debug!( + file_name, + path_str, "Parsing series name and path from file" + ); - let inserted_series = ctx.db._batch(series_creates).await?; + (file_name, path_str) + }) + .filter_map(|result| match result { + (Some(file_name), Some(path_str)) => Some(ctx.db.series().create( + file_name, + path_str, + vec![series::library::connect(library::id::equals( + library_id.clone(), + ))], + )), + _ => None, + }); - Ok(inserted_series) + Ok(ctx.db._batch(series_creates).await?) } pub async fn mark_media_missing( @@ -255,66 +252,123 @@ pub async fn batch_media_operations( ctx: &Ctx, operations: Vec, library_options: &LibraryOptions, -) -> Result, ScanError> { - // Note: this won't work if I add any other operations... - let (create_operations, mark_missing_operations): (Vec<_>, Vec<_>) = - operations.into_iter().partition(|operation| { - matches!(operation, BatchScanOperation::CreateMedia { .. }) - }); - - let media_creates = create_operations - .into_iter() - .map(|operation| { - match operation { - BatchScanOperation::CreateMedia { path, series_id } => { - // let result = insert_media(&ctx, &path, series_id, &library_options).await; - get_tentative_media(&path, series_id, library_options) +) -> CoreResult> { + let media_dao = MediaDaoImpl::new(ctx.db.clone()); + + let (media_creates, media_updates, missing_paths) = + operations + .into_iter() + .fold( + (vec![], vec![], vec![]), + |mut acc, operation| match operation { + BatchScanOperation::CreateMedia { path, series_id } => { + let build_result = Media::build_with_options( + &path, + MediaBuilderOptions { + series_id, + library_options: library_options.clone(), + }, + ); + + if let Ok(media) = build_result { + acc.0.push(media); + } else { + error!( + ?build_result, + "Error occurred trying to build media entity", + ); + } + + acc + }, + BatchScanOperation::UpdateMedia(outdated_media) => { + warn!( + ?outdated_media, + "Stump currently has minimal support for updating media. This will be improved in the future.", + ); + let build_result = Media::build_with_options( + Path::new(&outdated_media.path), + MediaBuilderOptions { + series_id: outdated_media.series_id.clone(), + library_options: library_options.clone(), + }, + ); + + if let Ok(newer_media) = build_result { + acc.1.push(outdated_media.resolve_changes(&newer_media)); + } else { + error!( + ?build_result, + "Error occurred trying to build media entity for update", + ); + } + + acc + }, + BatchScanOperation::MarkMediaMissing { path } => { + acc.2.push(path); + acc + }, }, - _ => unreachable!(), - } - }) - .filter_map(|res| match res { - Ok(entry) => Some(entry.into_action(ctx)), - Err(e) => { - error!("Failed to create media: {:?}", e); + ); - None - }, - }); - - let missing_paths = mark_missing_operations - .into_iter() - .map(|operation| match operation { - BatchScanOperation::MarkMediaMissing { path } => path, - _ => unreachable!(), - }) - .collect::>(); - - let result = mark_media_missing(ctx, missing_paths).await; + trace!( + media_creates_len = media_creates.len(), + media_updates_len = media_updates.len(), + missing_paths_len = missing_paths.len(), + "Partitioned batch operations", + ); - if let Err(err) = result { - error!("Failed to mark media as MISSING: {:?}", err); + let update_result = media_dao.update_many(media_updates).await; + if let Err(err) = update_result { + error!(query_error = ?err, "Error occurred trying to update media entities"); } else { - debug!("Marked {} media as MISSING", result.unwrap()); + debug!( + updated_count = update_result.unwrap().len(), + "Updated media entities" + ) } - Ok(ctx.db._batch(media_creates).await?) + let marked_missing_result = mark_media_missing(ctx, missing_paths).await; + if let Err(err) = marked_missing_result { + error!( + query_error = ?err, + "Error occurred trying to mark media as missing", + ); + } + + let inserted_media = media_dao.insert_many(media_creates).await?; + debug!( + inserted_media_len = inserted_media.len(), + "Inserted new media entities", + ); + + // FIXME: make return generic (not literally) + Ok(inserted_media) } -// TODO: error handling, i.e don't unwrap lol -// TODO: is it better practice to make this async? pub fn populate_glob_builder(builder: &mut GlobSetBuilder, paths: &[PathBuf]) { for path in paths { - // read the lines of the file, and add each line as a glob pattern in the builder - let file = File::open(path).unwrap(); - - for line in BufReader::new(file).lines() { - if let Err(e) = line { - error!("Failed to read line from file: {:?}", e); - continue; + let open_result = File::open(path); + if let Ok(file) = open_result { + // read the lines of the file, and add each line as a glob pattern in the builder + for line in BufReader::new(file).lines() { + if let Err(e) = line { + error!( + error = ?e, + "Error occurred trying to read line from glob file", + ); + continue; + } + + builder.add(Glob::new(&line.unwrap()).unwrap()); } - - builder.add(Glob::new(&line.unwrap()).unwrap()); + } else { + error!( + error = ?open_result.err(), + ?path, + "Failed to open file", + ); } } } diff --git a/core/src/fs/traits.rs b/core/src/fs/traits.rs new file mode 100644 index 000000000..0c0f154d3 --- /dev/null +++ b/core/src/fs/traits.rs @@ -0,0 +1,161 @@ +use std::{ffi::OsStr, path::Path}; +use tracing::error; +use walkdir::WalkDir; + +use crate::prelude::ContentType; + +use super::constants::is_accepted_cover_name; + +pub trait OsStrUtils { + fn try_to_string(&self) -> Option; +} + +impl OsStrUtils for OsStr { + fn try_to_string(&self) -> Option { + self.to_str().map(|str| str.to_string()) + } +} + +pub struct FileParts { + file_name: String, + file_stem: String, + extension: String, +} + +pub trait PathUtils { + fn file_parts(&self) -> FileParts; + fn infer_kind(&self) -> std::io::Result>; + fn is_hidden_file(&self) -> bool; + fn should_ignore(&self) -> bool; + fn is_supported(&self) -> bool; + fn is_img(&self) -> bool; + fn is_thumbnail_img(&self) -> bool; + fn dir_has_media(&self) -> bool; + fn dir_has_media_deep(&self) -> bool; +} + +impl PathUtils for Path { + fn file_parts(&self) -> FileParts { + let file_name = self + .file_name() + .and_then(|os_str| os_str.try_to_string()) + .unwrap_or_default(); + let file_stem = self + .file_stem() + .and_then(|os_str| os_str.try_to_string()) + .unwrap_or_default(); + let extension = self + .extension() + .and_then(|os_str| os_str.try_to_string()) + .unwrap_or_default(); + + FileParts { + file_name, + file_stem, + extension, + } + } + + /// Returns the result of `infer::get_from_path`. + fn infer_kind(&self) -> std::io::Result> { + infer::get_from_path(self) + } + + /// Returns true if the file is hidden (i.e. starts with a dot). + fn is_hidden_file(&self) -> bool { + let FileParts { file_name, .. } = self.file_parts(); + + file_name.starts_with('.') + } + + /// Returns true if the file is a supported media file. This is a strict check when + /// infer can determine the file type, and a loose extension-based check when infer cannot. + fn is_supported(&self) -> bool { + let content_type = ContentType::from_path(self); + content_type != ContentType::UNKNOWN && !content_type.is_image() + } + + /// Returns true when the scanner should not persist the file to the database. + /// First checks if the file is hidden (i.e. starts with a dot), then checks if + /// the file is supported by Stump. + // + // TODO: This will change in the future to allow for unsupported files to + // be added to the database with *minimal* functionality. + fn should_ignore(&self) -> bool { + if self.is_hidden_file() { + return true; + } + + !self.is_supported() + } + + /// Returns true if the file is an image. This is a strict check when infer + /// can determine the file type, and a loose extension-based check when infer cannot. + fn is_img(&self) -> bool { + if let Ok(Some(file_type)) = infer::get_from_path(self) { + return file_type.mime_type().starts_with("image/"); + } + + let FileParts { extension, .. } = self.file_parts(); + + extension.eq_ignore_ascii_case("jpg") + || extension.eq_ignore_ascii_case("png") + || extension.eq_ignore_ascii_case("jpeg") + } + + /// Returns true if the file is a thumbnail image. This calls the `is_img` function + /// from the same trait, and then checks if the file name is one of the following: + /// - cover + /// - thumbnail + /// - folder + /// + /// These will *potentially* be reserved filenames in the future... Not sure + /// if this functionality will be kept. + fn is_thumbnail_img(&self) -> bool { + if !self.is_img() { + return false; + } + + let FileParts { file_stem, .. } = self.file_parts(); + + is_accepted_cover_name(&file_stem) + } + + /// Returns true if the directory has any media files in it. This is a shallow + /// check, and will not check subdirectories. + fn dir_has_media(&self) -> bool { + if !self.is_dir() { + return false; + } + + let items = std::fs::read_dir(self); + if items.is_err() { + error!( + error = ?items.unwrap_err(), + path = ?self, + "IOError: failed to read directory" + ); + return false; + } + + items + .unwrap() + .filter_map(|item| item.ok()) + .filter(|item| item.path() != self) + .any(|f| !f.path().should_ignore()) + } + + /// Returns true if the directory has any media files in it. This is a deep + /// check, and will check *all* subdirectories. + fn dir_has_media_deep(&self) -> bool { + if !self.is_dir() { + return false; + } + + WalkDir::new(self) + .into_iter() + .filter_map(|item| item.ok()) + .filter(|item| item.path() != self) + .any(|f| !f.path().should_ignore()) + } +} diff --git a/core/src/job/jobs.rs b/core/src/job/jobs.rs index 5d0e55828..d8f658d2d 100644 --- a/core/src/job/jobs.rs +++ b/core/src/job/jobs.rs @@ -1,8 +1,6 @@ use super::{Job, RunnerCtx}; use crate::{ - fs::scanner::library_scanner::scan, - job::JobUpdate, - types::{models::library::LibraryScanMode, CoreResult}, + db::models::LibraryScanMode, fs::scanner::scan, job::JobUpdate, prelude::CoreResult, }; use tracing::info; diff --git a/core/src/job/mod.rs b/core/src/job/mod.rs index e7f96f769..fd9bfcbce 100644 --- a/core/src/job/mod.rs +++ b/core/src/job/mod.rs @@ -3,6 +3,7 @@ pub mod pool; pub mod runner; pub use jobs::*; +use utoipa::ToSchema; use std::{fmt::Debug, num::TryFromIntError}; @@ -11,11 +12,10 @@ use specta::Type; // use tracing::error; use crate::{ - config::context::Ctx, event::CoreEvent, job::runner::RunnerCtx, + prelude::{errors::CoreError, CoreResult, Ctx}, prisma::{self}, - types::{errors::CoreError, CoreResult}, }; #[async_trait::async_trait] @@ -102,7 +102,7 @@ pub enum JobEvent { Failed, } -#[derive(Clone, Serialize, Deserialize, Debug, Type)] +#[derive(Clone, Serialize, Deserialize, Debug, Type, ToSchema)] pub enum JobStatus { #[serde(rename = "RUNNING")] Running, @@ -212,7 +212,7 @@ impl JobUpdate { } } -#[derive(Clone, Serialize, Deserialize, Debug, Type)] +#[derive(Clone, Serialize, Deserialize, Debug, Type, ToSchema)] pub struct JobReport { /// This will actually refer to the job runner id pub id: Option, diff --git a/core/src/job/pool.rs b/core/src/job/pool.rs index 9247b8bbe..f5bbb9e66 100644 --- a/core/src/job/pool.rs +++ b/core/src/job/pool.rs @@ -8,10 +8,9 @@ use tracing::error; use super::{persist_job_cancelled, runner::Runner, Job, JobReport}; use crate::{ - config::context::Ctx, event::{CoreEvent, InternalCoreTask}, + prelude::{CoreError, CoreResult, Ctx}, prisma::job, - types::{CoreError, CoreResult}, }; // Note: this is 12 hours diff --git a/core/src/job/runner.rs b/core/src/job/runner.rs index bcef85e65..fe5d4d448 100644 --- a/core/src/job/runner.rs +++ b/core/src/job/runner.rs @@ -5,9 +5,8 @@ use tokio::{self, sync::Mutex}; use tracing::error; use crate::{ - config::context::Ctx, event::CoreEvent, - types::{CoreError, CoreResult}, + prelude::{CoreError, CoreResult, Ctx}, }; use super::{persist_new_job, pool::JobPool, Job, JobUpdate, JobWrapper}; diff --git a/core/src/lib.rs b/core/src/lib.rs index cb2091484..d0c7c3602 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -10,19 +10,14 @@ pub mod fs; pub mod job; pub mod opds; -// TODO: I don't really want this to be pub. I think the only way that is possible is if I -// made ALL the DB operations pub, interfacing with the prisma client directly. This way, -// the server invokes those functions, rather than building those queries. I don't see a nice, -// neat way to do this that won't give me a migraine lol. +pub mod prelude; pub mod prisma; -pub mod types; -use config::context::Ctx; use config::env::StumpEnvironment; use config::logging::STUMP_SHADOW_TEXT; use event::{event_manager::EventManager, InternalCoreTask}; +use prelude::{CoreError, CoreResult, Ctx}; use tokio::sync::mpsc::unbounded_channel; -use types::{errors::CoreError, CoreResult}; /// The [`StumpCore`] struct is the main entry point for any server-side Stump /// applications. It is responsible for managing incoming tasks ([`InternalCoreTask`]), diff --git a/core/src/opds/author.rs b/core/src/opds/author.rs index 16fdf4f68..e2f4e7c89 100644 --- a/core/src/opds/author.rs +++ b/core/src/opds/author.rs @@ -1,6 +1,6 @@ use xml::EventWriter; -use crate::types::CoreResult; +use crate::prelude::CoreResult; use super::util; diff --git a/core/src/opds/entry.rs b/core/src/opds/entry.rs index 8e0e21372..d32cbf509 100644 --- a/core/src/opds/entry.rs +++ b/core/src/opds/entry.rs @@ -3,7 +3,7 @@ use prisma_client_rust::chrono::{self, FixedOffset}; use urlencoding::encode; use xml::{writer::XmlEvent, EventWriter}; -use crate::types::CoreResult; +use crate::prelude::CoreResult; use crate::{ opds::link::OpdsStreamLink, prisma::{library, media, series}, diff --git a/core/src/opds/feed.rs b/core/src/opds/feed.rs index ea5b9b375..dd5d425d0 100644 --- a/core/src/opds/feed.rs +++ b/core/src/opds/feed.rs @@ -1,7 +1,7 @@ use crate::{ opds::link::OpdsLink, + prelude::errors::CoreError, prisma::{library, series}, - types::errors::CoreError, }; use prisma_client_rust::chrono; use tracing::warn; diff --git a/core/src/opds/link.rs b/core/src/opds/link.rs index 75c2850cb..3fb5f394e 100644 --- a/core/src/opds/link.rs +++ b/core/src/opds/link.rs @@ -1,6 +1,6 @@ use xml::{writer::XmlEvent, EventWriter}; -use crate::types::CoreResult; +use crate::prelude::CoreResult; use super::util::OpdsEnumStr; diff --git a/core/src/opds/opensearch.rs b/core/src/opds/opensearch.rs index 66bddc025..b83ab6cbb 100644 --- a/core/src/opds/opensearch.rs +++ b/core/src/opds/opensearch.rs @@ -1,6 +1,6 @@ use xml::{writer::XmlEvent, EventWriter}; -use crate::types::CoreResult; +use crate::prelude::CoreResult; use super::{ link::OpdsLinkType, diff --git a/core/src/opds/util.rs b/core/src/opds/util.rs index 425074441..cd5e195ca 100644 --- a/core/src/opds/util.rs +++ b/core/src/opds/util.rs @@ -1,6 +1,6 @@ use xml::{writer::XmlEvent, EventWriter}; -use crate::types::CoreResult; +use crate::prelude::CoreResult; pub trait OpdsEnumStr { fn as_str(&self) -> &'static str; diff --git a/core/src/config/context.rs b/core/src/prelude/context.rs similarity index 95% rename from core/src/config/context.rs rename to core/src/prelude/context.rs index 636acb56f..1f4f28786 100644 --- a/core/src/config/context.rs +++ b/core/src/prelude/context.rs @@ -6,11 +6,10 @@ use tokio::sync::{ }; use crate::{ - db, + db::{self, models::Log}, event::{CoreEvent, InternalCoreTask}, job::Job, prisma, - types::models::log::TentativeLog, }; type InternalSender = UnboundedSender; @@ -156,21 +155,19 @@ impl Ctx { pub async fn handle_failure_event(&self, event: CoreEvent) { use prisma::log; - // TODO: maybe log::error! here? - self.emit_client_event(event.clone()); - let tentative_log = TentativeLog::from(event); + let log = Log::from(event); // FIXME: error handling here... let _ = self .db .log() .create( - tentative_log.message, + log.message, vec![ - log::job_id::set(tentative_log.job_id), - log::level::set(tentative_log.level.to_string()), + log::job_id::set(log.job_id), + log::level::set(log.level.to_string()), ], ) .exec() diff --git a/core/src/types/enums.rs b/core/src/prelude/enums.rs similarity index 86% rename from core/src/types/enums.rs rename to core/src/prelude/enums.rs index 4f8bac9df..f081efc6a 100644 --- a/core/src/types/enums.rs +++ b/core/src/prelude/enums.rs @@ -2,16 +2,18 @@ use std::{fmt, str::FromStr}; use serde::{Deserialize, Serialize}; use specta::Type; +use utoipa::ToSchema; -#[derive(Serialize, Deserialize, Type)] +#[derive(Serialize, Deserialize, Type, ToSchema, Default)] pub enum UserRole { #[serde(rename = "SERVER_OWNER")] ServerOwner, #[serde(rename = "MEMBER")] + #[default] Member, } -#[derive(Serialize, Deserialize, Type)] +#[derive(Serialize, Deserialize, Type, ToSchema)] pub enum LayoutMode { #[serde(rename = "GRID")] Grid, @@ -19,7 +21,7 @@ pub enum LayoutMode { List, } -#[derive(Debug, Deserialize, Serialize, Type, Clone, Copy)] +#[derive(Debug, Deserialize, Serialize, Type, ToSchema, Clone, Copy)] pub enum FileStatus { #[serde(rename = "UNKNOWN")] Unknown, @@ -33,6 +35,12 @@ pub enum FileStatus { Missing, } +impl Default for FileStatus { + fn default() -> Self { + Self::Ready + } +} + impl fmt::Display for FileStatus { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -60,12 +68,6 @@ impl FromStr for FileStatus { } } -impl Default for UserRole { - fn default() -> Self { - UserRole::Member - } -} - impl From for String { fn from(role: UserRole) -> String { match role { diff --git a/core/src/types/errors.rs b/core/src/prelude/errors.rs similarity index 96% rename from core/src/types/errors.rs rename to core/src/prelude/errors.rs index 1a1296d61..f1bcc2dae 100644 --- a/core/src/types/errors.rs +++ b/core/src/prelude/errors.rs @@ -8,6 +8,10 @@ use zip::result::ZipError; pub enum CoreError { #[error("Failed to initialize Stump core: {0}")] InitializationError(String), + #[error( + "Attempted to initialize StumpCore with a config dir that does not exist: {0}" + )] + ConfigDirDoesNotExist(String), #[error("Query error: {0}")] QueryError(#[from] prisma_client_rust::queries::QueryError), #[error("Invalid query error: {0}")] diff --git a/core/src/types/models/list_directory.rs b/core/src/prelude/fs/list_directory.rs similarity index 80% rename from core/src/types/models/list_directory.rs rename to core/src/prelude/fs/list_directory.rs index a7d76beac..b30923b72 100644 --- a/core/src/types/models/list_directory.rs +++ b/core/src/prelude/fs/list_directory.rs @@ -1,7 +1,8 @@ use serde::{Deserialize, Serialize}; use specta::Type; +use utoipa::ToSchema; -#[derive(Debug, Clone, Deserialize, Serialize, Type)] +#[derive(Debug, Clone, Deserialize, Serialize, Type, ToSchema)] pub struct DirectoryListingInput { pub path: Option, } @@ -14,13 +15,13 @@ impl Default for DirectoryListingInput { } } -#[derive(Debug, Clone, Deserialize, Serialize, Type)] +#[derive(Debug, Clone, Deserialize, Serialize, Type, ToSchema)] pub struct DirectoryListing { pub parent: Option, pub files: Vec, } -#[derive(Debug, Clone, Deserialize, Serialize, Type)] +#[derive(Debug, Clone, Deserialize, Serialize, Type, ToSchema)] pub struct DirectoryListingFile { pub is_directory: bool, pub name: String, diff --git a/core/src/prelude/fs/media_file.rs b/core/src/prelude/fs/media_file.rs new file mode 100644 index 000000000..38ba10d9b --- /dev/null +++ b/core/src/prelude/fs/media_file.rs @@ -0,0 +1,45 @@ +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; +use specta::Type; + +pub struct ProcessedMediaFile { + pub thumbnail_path: Option, + pub path: PathBuf, + pub checksum: Option, + pub metadata: Option, + pub pages: i32, +} + +// Derived from ComicInfo.xml +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Type, Default)] +pub struct MediaMetadata { + #[serde(rename = "Series")] + pub series: Option, + #[serde(rename = "Number")] + pub number: Option, + #[serde(rename = "Web")] + pub web: Option, + #[serde(rename = "Summary")] + pub summary: Option, + #[serde(rename = "Publisher")] + pub publisher: Option, + #[serde(rename = "Genre")] + pub genre: Option, + #[serde(rename = "PageCount")] + pub page_count: Option, +} + +// impl MediaMetadata { +// pub fn default() -> Self { +// Self { +// series: None, +// number: None, +// web: None, +// summary: None, +// publisher: None, +// genre: None, +// page_count: None, +// } +// } +// } diff --git a/core/src/prelude/fs/mod.rs b/core/src/prelude/fs/mod.rs new file mode 100644 index 000000000..ac3fcda85 --- /dev/null +++ b/core/src/prelude/fs/mod.rs @@ -0,0 +1,5 @@ +pub mod list_directory; +pub mod media_file; + +pub use list_directory::*; +pub use media_file::*; diff --git a/core/src/types/mod.rs b/core/src/prelude/mod.rs similarity index 83% rename from core/src/types/mod.rs rename to core/src/prelude/mod.rs index ef15f0a9d..f8b688e6b 100644 --- a/core/src/types/mod.rs +++ b/core/src/prelude/mod.rs @@ -1,13 +1,15 @@ +pub mod context; pub mod enums; pub mod errors; -pub mod models; +pub mod fs; pub mod server; -pub use errors::CoreError; - pub type CoreResult = Result; -pub use models::*; +pub use context::*; +pub use enums::*; +pub use errors::*; +pub use fs::*; pub use server::*; #[allow(unused_imports)] @@ -16,25 +18,23 @@ mod tests { use specta::ts_export; - use crate::{event::*, job::*}; - - use super::{ - enums::*, - errors::*, - inputs::*, - models::{ - epub::*, library::*, list_directory::*, log::*, media::*, read_progress::*, - series::*, tag::*, user::*, readinglist::* + use crate::{ + db::models::{ + epub::*, library::*, log::*, media::*, read_progress::*, reading_list::*, + series::*, tag::*, user::*, }, - server::*, + event::*, + job::*, }; + use super::{enums::*, errors::*, fs::*, inputs::*, server::*}; + #[test] #[ignore] fn codegen() -> Result<(), Box> { let mut file = File::create( PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../common/client/src/types") + .join("../packages/types") .join("core.ts"), )?; @@ -44,6 +44,7 @@ mod tests { file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; file.write_all( format!("{}\n\n", ts_export::()?).as_bytes(), )?; @@ -94,8 +95,11 @@ mod tests { file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; // Note: this will essentially be Partial... - file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; + file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; // file.write_all(format!("{}\n\n", ts_export::()?).as_bytes())?; // TODO: figure this out Ok(()) diff --git a/core/src/prelude/server/http.rs b/core/src/prelude/server/http.rs new file mode 100644 index 000000000..707fe7300 --- /dev/null +++ b/core/src/prelude/server/http.rs @@ -0,0 +1,226 @@ +use std::path::Path; + +use serde::Serialize; +use tracing::{debug, warn}; + +/// [`ContentType`] is an enum that represents the HTTP content type. This is a smaller +/// subset of the full list of content types, mostly focusing on types supported by Stump. +#[allow(non_camel_case_types)] +#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)] +pub enum ContentType { + XHTML, + XML, + HTML, + PDF, + EPUB_ZIP, + ZIP, + COMIC_ZIP, + RAR, + COMIC_RAR, + PNG, + JPEG, + WEBP, + GIF, + UNKNOWN, +} + +fn temporary_content_workarounds(extension: &str) -> ContentType { + if extension == "opf" || extension == "ncx" { + return ContentType::XML; + } + + ContentType::UNKNOWN +} + +fn infer_mime(path: &Path) -> Option { + match infer::get_from_path(path) { + Ok(result) => { + debug!(?path, ?result, "Infer result for path"); + result.map(|infer_type| infer_type.mime_type().to_string()) + }, + Err(e) => { + warn!(error = ?e, ?path, "Unable to infer mime for file",); + None + }, + } +} + +impl ContentType { + /// Infer the MIME type of a file extension. + /// + /// ### Examples + /// ```rust + /// use stump_core::types::server::http::ContentType; + /// + /// let content_type = ContentType::from_extension("png"); + /// assert_eq!(content_type, Some(ContentType::PNG)); + /// ``` + pub fn from_extension(extension: &str) -> ContentType { + match extension.to_lowercase().as_str() { + "xhtml" => ContentType::XHTML, + "xml" => ContentType::XML, + "html" => ContentType::HTML, + "pdf" => ContentType::PDF, + "epub" => ContentType::EPUB_ZIP, + "zip" => ContentType::ZIP, + "cbz" => ContentType::COMIC_ZIP, + "rar" => ContentType::RAR, + "cbr" => ContentType::COMIC_RAR, + "png" => ContentType::PNG, + "jpg" => ContentType::JPEG, + "jpeg" => ContentType::JPEG, + "webp" => ContentType::WEBP, + "gif" => ContentType::GIF, + _ => temporary_content_workarounds(extension), + } + } + + /// Infer the MIME type of a file using the [infer] crate. If the MIME type cannot be inferred, + /// then the file extension is used to determine the content type. + /// + /// ### Examples + /// ```rust + /// use stump_core::types::server::http::ContentType; + /// + /// let content_type = ContentType::from_file("test.png"); + /// assert_eq!(content_type, ContentType::PNG); + /// ``` + pub fn from_file(file_path: &str) -> ContentType { + let path = Path::new(file_path); + ContentType::from_path(path) + } + + /// Infer the MIME type of a [Path] using the [infer] crate. If the MIME type cannot be inferred, + /// then the extension of the path is used to determine the content type. + /// + /// ### Examples + /// ```rust + /// use stump_core::types::server::http::ContentType; + /// use std::path::Path; + /// + /// let path = Path::new("test.png"); + /// let content_type = ContentType::from_path(path); + /// assert_eq!(content_type, ContentType::PNG); + /// ``` + pub fn from_path(path: &Path) -> ContentType { + infer_mime(path) + .map(|mime| ContentType::from(mime.as_str())) + .unwrap_or_else(|| { + ContentType::from_extension( + path.extension() + .unwrap_or_default() + .to_str() + .unwrap_or_default(), + ) + }) + } + + /// Returns the string representation of the MIME type. + pub fn mime_type(&self) -> String { + self.to_string() + } + + /// Returns true if the content type is an image. + /// + /// ## Examples + /// ```rust + /// use stump_core::types::server::http::ContentType; + /// + /// let content_type = ContentType::PNG; + /// assert!(content_type.is_image()); + /// + /// let content_type = ContentType::XHTML; + /// assert!(!content_type.is_image()); + /// ``` + pub fn is_image(&self) -> bool { + self.to_string().starts_with("image") + } + + /// Returns true if the content type is a ZIP archive. + /// + /// ## Examples + /// + /// ```rust + /// use stump_core::types::server::http::ContentType; + /// + /// let content_type = ContentType::ZIP; + /// assert!(content_type.is_zip()); + /// ``` + pub fn is_zip(&self) -> bool { + self == &ContentType::ZIP || self == &ContentType::COMIC_ZIP + } + + /// Returns true if the content type is a RAR archive. + /// + /// ## Examples + /// + /// ```rust + /// use stump_core::types::server::http::ContentType; + /// + /// let content_type = ContentType::RAR; + /// assert!(content_type.is_rar()); + /// ``` + pub fn is_rar(&self) -> bool { + self == &ContentType::RAR || self == &ContentType::COMIC_RAR + } + + /// Returns true if the content type is an EPUB archive. + /// + /// ## Examples + /// + /// ```rust + /// use stump_core::types::server::http::ContentType; + /// + /// let content_type = ContentType::EPUB_ZIP; + /// assert!(content_type.is_epub()); + /// ``` + pub fn is_epub(&self) -> bool { + self == &ContentType::EPUB_ZIP + } +} + +impl From<&str> for ContentType { + /// Returns the content type from the string. + /// + /// NOTE: It is assumed that the string is a valid representation of a content type. + /// **Do not** use this method to parse a file path or extension. + fn from(s: &str) -> Self { + match s.to_lowercase().as_str() { + "application/xhtml+xml" => ContentType::XHTML, + "application/xml" => ContentType::XML, + "text/html" => ContentType::HTML, + "application/pdf" => ContentType::PDF, + "application/epub+zip" => ContentType::EPUB_ZIP, + "application/zip" => ContentType::ZIP, + "application/vnd.comicbook+zip" => ContentType::COMIC_ZIP, + "application/vnd.rar" => ContentType::RAR, + "application/vnd.comicbook-rar" => ContentType::COMIC_RAR, + "image/png" => ContentType::PNG, + "image/jpeg" => ContentType::JPEG, + "image/webp" => ContentType::WEBP, + "image/gif" => ContentType::GIF, + _ => ContentType::UNKNOWN, + } + } +} + +impl std::fmt::Display for ContentType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ContentType::XHTML => write!(f, "application/xhtml+xml"), + ContentType::XML => write!(f, "application/xml"), + ContentType::HTML => write!(f, "text/html"), + ContentType::PDF => write!(f, "application/pdf"), + ContentType::EPUB_ZIP => write!(f, "application/epub+zip"), + ContentType::ZIP => write!(f, "application/zip"), + ContentType::COMIC_ZIP => write!(f, "application/vnd.comicbook+zip"), + ContentType::RAR => write!(f, "application/vnd.rar"), + ContentType::COMIC_RAR => write!(f, "application/vnd.comicbook-rar"), + ContentType::PNG => write!(f, "image/png"), + ContentType::JPEG => write!(f, "image/jpeg"), + ContentType::WEBP => write!(f, "image/webp"), + ContentType::GIF => write!(f, "image/gif"), + ContentType::UNKNOWN => write!(f, "unknown"), + } + } +} diff --git a/core/src/types/server/inputs.rs b/core/src/prelude/server/inputs.rs similarity index 69% rename from core/src/types/server/inputs.rs rename to core/src/prelude/server/inputs.rs index 20c64f6c0..c7b40b5c9 100644 --- a/core/src/types/server/inputs.rs +++ b/core/src/prelude/server/inputs.rs @@ -1,12 +1,10 @@ use serde::{Deserialize, Serialize}; use specta::Type; +use utoipa::ToSchema; -use crate::types::{ - library::{LibraryOptions, LibraryScanMode}, - tag::Tag, -}; +use crate::db::models::{LibraryOptions, LibraryScanMode, Tag}; -#[derive(Debug, Clone, Deserialize, Type)] +#[derive(Debug, Clone, Deserialize, Type, ToSchema)] pub struct UserPreferencesUpdate { pub id: String, pub locale: String, @@ -21,18 +19,24 @@ pub struct DecodedCredentials { pub password: String, } -#[derive(Deserialize, Type)] +#[derive(Deserialize, Type, ToSchema)] pub struct LoginOrRegisterArgs { pub username: String, pub password: String, } -#[derive(Serialize, Type)] +#[derive(Deserialize, Type, ToSchema)] +pub struct UpdateUserArgs { + pub username: String, + pub password: Option, +} + +#[derive(Serialize, Type, ToSchema)] pub struct ClaimResponse { pub is_claimed: bool, } -#[derive(Deserialize, Debug, Type)] +#[derive(Deserialize, Debug, Type, ToSchema)] pub struct CreateLibraryArgs { /// The name of the library to create. pub name: String, @@ -48,7 +52,7 @@ pub struct CreateLibraryArgs { pub library_options: Option, } -#[derive(Deserialize, Debug, Type)] +#[derive(Deserialize, Debug, Type, ToSchema)] pub struct UpdateLibraryArgs { pub id: String, /// The updated name of the library. @@ -66,3 +70,19 @@ pub struct UpdateLibraryArgs { /// Optional flag to indicate how the library should be automatically scanned after update. Default is `BATCHED`. pub scan_mode: Option, } + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct ScanQueryParam { + pub scan_mode: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type, ToSchema)] +pub struct CreateReadingList { + pub id: String, + pub media_ids: Vec, +} + +#[derive(Deserialize, Type, ToSchema)] +pub struct CreateTags { + pub tags: Vec, +} diff --git a/core/src/types/server/mod.rs b/core/src/prelude/server/mod.rs similarity index 80% rename from core/src/types/server/mod.rs rename to core/src/prelude/server/mod.rs index 48e23de95..b51a14c8c 100644 --- a/core/src/types/server/mod.rs +++ b/core/src/prelude/server/mod.rs @@ -10,8 +10,9 @@ pub use http::*; pub use inputs::*; pub use pageable::*; pub use query::*; +use utoipa::ToSchema; -#[derive(Serialize, Deserialize, Type)] +#[derive(Serialize, Deserialize, Type, ToSchema)] pub struct StumpVersion { pub semver: String, pub rev: Option, diff --git a/core/src/prelude/server/pageable.rs b/core/src/prelude/server/pageable.rs new file mode 100644 index 000000000..6f777434f --- /dev/null +++ b/core/src/prelude/server/pageable.rs @@ -0,0 +1,451 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; +use tracing::trace; +use utoipa::ToSchema; + +use crate::{ + db::models::{Library, Media, Series}, + prelude::DirectoryListing, +}; + +#[derive( + Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq, Type, ToSchema, +)] +pub struct PageQuery { + pub zero_based: Option, + pub page: Option, + pub page_size: Option, +} + +#[derive( + Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq, Type, ToSchema, +)] +pub struct CursorQuery { + pub cursor: Option, + pub limit: Option, +} + +#[derive(Default, Debug, Deserialize, Serialize, Type, ToSchema)] +pub struct PaginationQuery { + pub zero_based: Option, + pub page: Option, + pub page_size: Option, + pub cursor: Option, + pub limit: Option, +} + +impl PaginationQuery { + pub fn get(self) -> Pagination { + Pagination::from(self) + } +} + +impl From for Pagination { + fn from(p: PaginationQuery) -> Self { + if p.cursor.is_some() || p.limit.is_some() { + Pagination::Cursor(CursorQuery { + cursor: p.cursor, + limit: p.limit.map(|val| val.into()), + }) + } else if p.page.is_some() || p.page_size.is_some() || p.zero_based.is_some() { + Pagination::Page(PageQuery { + page: p.page, + page_size: p.page_size, + zero_based: p.zero_based, + }) + } else { + Pagination::None + } + } +} + +// FIXME: , ToSchema, not working... +#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq, Type)] +#[serde(untagged)] +pub enum Pagination { + #[default] + None, + Page(PageQuery), + Cursor(CursorQuery), +} + +impl Pagination { + /// Returns true if pagination is None + pub fn is_unpaged(&self) -> bool { + matches!(self, Pagination::None) + } + + /// Returns true if pagination is Page(..) + pub fn is_paged(&self) -> bool { + matches!(self, Pagination::Page(..)) + } + + /// Returns true if pagination is Cursor(..) + pub fn is_cursor(&self) -> bool { + matches!(self, Pagination::Cursor(..)) + } +} + +impl PageQuery { + /// Returns a tuple of (skip, take) for use in Prisma queries. + pub fn get_skip_take(&self) -> (i64, i64) { + let zero_based = self.zero_based.unwrap_or(false); + let page_size = self.page_size.unwrap_or(20); + let default_page = u32::from(!zero_based); + let page = self.page.unwrap_or(default_page); + + let start = if zero_based { + page * page_size + } else { + (page - 1) * page_size + } as i64; + + let take = page_size as i64; + + (start, take) + } + + pub fn page_params(self) -> PageParams { + let zero_based = self.zero_based.unwrap_or(false); + + PageParams { + zero_based, + page: self.page.unwrap_or_else(|| u32::from(!zero_based)), + page_size: self.page_size.unwrap_or(20), + } + } +} + +pub struct PageBounds { + pub skip: i64, + pub take: i64, +} + +#[derive(Debug, Serialize, Clone, Type, ToSchema)] +pub struct PageParams { + pub zero_based: bool, + pub page: u32, + pub page_size: u32, +} + +impl Default for PageParams { + fn default() -> Self { + PageParams { + zero_based: false, + page: 0, + page_size: 20, + } + } +} + +impl PageParams { + /// Returns a tuple of (skip, take) for use in Prisma queries. + pub fn get_skip_take(&self) -> (i64, i64) { + let start = if self.zero_based { + self.page * self.page_size + } else { + (self.page - 1) * self.page_size + } as i64; + + // let end = start + self.page_size; + let take = self.page_size as i64; + + (start, take) + } + + pub fn get_page_bounds(&self) -> PageBounds { + let (skip, take) = self.get_skip_take(); + + PageBounds { skip, take } + } +} + +impl From> for PageParams { + fn from(req_params: Option) -> Self { + match req_params { + Some(params) => { + let zero_based = params.zero_based.unwrap_or(false); + let page_size = params.page_size.unwrap_or(20); + + let default_page = u32::from(!zero_based); + + let page = params.page.unwrap_or(default_page); + + PageParams { + page, + page_size, + zero_based, + } + }, + None => PageParams::default(), + } + } +} + +#[derive(Serialize)] +pub struct PageLinks { + /// The current request URL. E.g. http://example.com/api/v1/users?page=2 + #[serde(rename = "self")] + pub itself: String, + /// The start URL, relative to current paginated request URL. E.g. http://example.com/api/v1/users?page=0 + pub start: String, + /// The prev URL, relative to current paginated request URL. E.g. http://example.com/api/v1/users?page=1 + pub prev: Option, + /// The next URL, relative to current paginated request URL. E.g. http://example.com/api/v1/users?page=3 + pub next: Option, +} + +#[derive(Serialize, Type, ToSchema)] +pub struct PageInfo { + /// The number of pages available. + pub total_pages: u32, + /// The current page, zero-indexed. + pub current_page: u32, + /// The number of elements per page. + pub page_size: u32, + /// The offset of the current page. E.g. if current page is 1, and pageSize is 10, the offset is 20. + pub page_offset: u32, + /// Whether or not the page is zero-indexed. + pub zero_based: bool, +} + +impl PageInfo { + pub fn new(page_params: PageParams, total_pages: u32) -> Self { + let current_page = page_params.page; + let page_size = page_params.page_size; + let zero_based = page_params.zero_based; + + PageInfo { + total_pages, + current_page, + page_size, + page_offset: current_page * page_size, + zero_based, + } + } +} + +#[derive(Default, Debug, Deserialize, Serialize, PartialEq, Eq, Type, ToSchema)] +pub struct CursorInfo { + cursor: Option, + limit: Option, +} + +impl From for CursorInfo { + fn from(cursor_query: CursorQuery) -> Self { + Self { + cursor: cursor_query.cursor, + limit: cursor_query.limit, + } + } +} + +#[derive(Serialize, Type, ToSchema)] +// OK, this is SO annoying... +#[aliases(PageableDirectoryListing = Pageable)] +pub struct Pageable { + /// The target data being returned. + pub data: T, + /// The pagination information (if paginated). + pub _page: Option, + /// The cursor information (if cursor-baesd paginated). + pub _cursor: Option, +} + +// NOTE: this is an infuriating workaround for getting Pageable> to work with utoipa +#[derive(Serialize, Type, ToSchema)] +#[aliases(PageableLibraries = PageableArray, PageableSeries = PageableArray, PageableMedia = PageableArray)] +pub struct PageableArray { + /// The target data being returned. + pub data: Vec, + /// The pagination information (if paginated). + pub _page: Option, + /// The cursor information (if cursor-baesd paginated). + pub _cursor: Option, +} + +impl Pageable { + pub fn unpaged(data: T) -> Self { + Pageable { + data, + _page: None, + _cursor: None, + } + } + + pub fn page_paginated(data: T, page_info: PageInfo) -> Self { + Self::new(data, Some(page_info), None) + } + + pub fn cursor_paginated(data: T, cursor_info: CursorInfo) -> Self { + Self::new(data, None, Some(cursor_info)) + } + + pub fn new( + data: T, + page_info: Option, + cursor_info: Option, + ) -> Self { + Pageable { + data, + _page: page_info, + _cursor: cursor_info, + } + } + + /// Generates a Pageable instance using an explicitly provided count and page params. This is useful for + /// when the data provided is not the full set available, but rather a subset of the data (e.g. a query with + /// a limit). + pub fn with_count(data: T, db_count: i64, page_params: PageParams) -> Self { + let total_pages = (db_count as f32 / page_params.page_size as f32).ceil() as u32; + + Pageable::page_paginated(data, PageInfo::new(page_params, total_pages)) + } +} + +impl From> for Pageable> +where + T: Serialize + Clone, +{ + fn from(vec: Vec) -> Pageable> { + Pageable::unpaged(vec) + } +} + +impl From<(Vec, PageParams)> for Pageable> +where + T: Serialize + Clone, +{ + fn from(tuple: (Vec, PageParams)) -> Pageable> { + let (mut data, page_params) = tuple; + + let total_pages = + (data.len() as f32 / page_params.page_size as f32).ceil() as u32; + + let start = match page_params.zero_based { + true => page_params.page * page_params.page_size, + false => (page_params.page - 1) * page_params.page_size, + }; + + // let start = page_params.page * page_params.page_size; + let end = start + page_params.page_size; + + // println!("len:{}, start: {}, end: {}", data.len(), start, end); + + if start > data.len() as u32 { + data = vec![]; + } else if end < data.len() as u32 { + data = data + .get((start as usize)..(end as usize)) + .ok_or("Invalid page") + .unwrap() + .to_vec(); + } else { + data = data + .get((start as usize)..) + .ok_or("Invalid page") + .unwrap() + .to_vec(); + } + + Pageable::page_paginated(data, PageInfo::new(page_params, total_pages)) + } +} + +impl From<(Vec, Option)> for Pageable> +where + T: Serialize + Clone, +{ + fn from(tuple: (Vec, Option)) -> Pageable> { + (tuple.0, PageParams::from(tuple.1)).into() + } +} + +// Note: this is used when you have to query the database for the total number of pages. +impl From<(Vec, i64, PageParams)> for Pageable> +where + T: Serialize + Clone, +{ + fn from(tuple: (Vec, i64, PageParams)) -> Pageable> { + let (data, db_total, page_params) = tuple; + + let total_pages = (db_total as f32 / page_params.page_size as f32).ceil() as u32; + + Pageable::page_paginated(data, PageInfo::new(page_params, total_pages)) + } +} + +// Note: this is used when you have to query the database for the total number of pages. +impl From<(Vec, i64, Pagination)> for Pageable> +where + T: Serialize + Clone, +{ + fn from(tuple: (Vec, i64, Pagination)) -> Pageable> { + let (data, db_total, pagination) = tuple; + + match pagination { + Pagination::Page(page_query) => { + let page_params = page_query.page_params(); + let total_pages = + (db_total as f32 / page_params.page_size as f32).ceil() as u32; + Pageable::page_paginated(data, PageInfo::new(page_params, total_pages)) + }, + Pagination::Cursor(cursor_query) => { + Pageable::cursor_paginated(data, CursorInfo::from(cursor_query)) + }, + _ => Pageable::unpaged(data), + } + } +} + +impl From<(DirectoryListing, u32, u32)> for Pageable { + fn from(tuple: (DirectoryListing, u32, u32)) -> Pageable { + let (data, page, page_size) = tuple; + + let total_pages = (data.files.len() as f32 / page_size as f32).ceil() as u32; + // directory listing will always be zero-based. + let start = (page - 1) * page_size; + let end = start + page_size; + + let mut truncated_files = data.files; + + if start > truncated_files.len() as u32 { + truncated_files = vec![]; + } else if end < truncated_files.len() as u32 { + truncated_files = truncated_files + .get((start as usize)..(end as usize)) + .ok_or("Invalid page") + .unwrap() + .to_vec(); + } else { + truncated_files = truncated_files + .get((start as usize)..) + .ok_or("Invalid page") + .unwrap() + .to_vec(); + } + + trace!( + "{} total pages of size {}. Returning truncated data of size {}.", + total_pages, + page_size, + truncated_files.len() + ); + + let truncated_data = DirectoryListing { + parent: data.parent, + files: truncated_files, + }; + + Pageable::page_paginated( + truncated_data, + PageInfo { + total_pages, + current_page: page, + page_size, + page_offset: page * page_size, + zero_based: false, + }, + ) + } +} diff --git a/core/src/types/server/query.rs b/core/src/prelude/server/query.rs similarity index 60% rename from core/src/types/server/query.rs rename to core/src/prelude/server/query.rs index 80e641662..1358bafe4 100644 --- a/core/src/types/server/query.rs +++ b/core/src/prelude/server/query.rs @@ -1,28 +1,21 @@ -use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use specta::Type; - -use prisma_client_rust::{query_core::Selection, FindMany, PrismaValue, SerializedWhere}; +use utoipa::ToSchema; use crate::{ - prisma::{media, series}, - types::{errors::CoreError, server::pageable::PageParams}, + prelude::errors::CoreError, + prisma::{library, media, series}, }; -#[derive(Debug, Serialize, Deserialize, Clone, Type)] +#[derive(Debug, Default, Serialize, Deserialize, Clone, Type, ToSchema)] pub enum Direction { #[serde(rename = "asc")] Asc, #[serde(rename = "desc")] + #[default] Desc, } -impl Default for Direction { - fn default() -> Self { - Direction::Asc - } -} - impl From for prisma_client_rust::Direction { fn from(direction: Direction) -> prisma_client_rust::Direction { match direction { @@ -33,7 +26,8 @@ impl From for prisma_client_rust::Direction { } /// Model used in media API to alter sorting/ordering of queried media -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize, Type, ToSchema)] +#[serde(default)] pub struct QueryOrder { /// The field to order by. Defaults to 'name' pub order_by: String, @@ -50,15 +44,6 @@ impl Default for QueryOrder { } } -impl From for QueryOrder { - fn from(params: PageParams) -> Self { - QueryOrder { - order_by: params.order_by, - direction: params.direction, - } - } -} - impl TryInto for QueryOrder { type Error = CoreError; @@ -85,6 +70,26 @@ impl TryInto for QueryOrder { } } +impl TryInto for QueryOrder { + type Error = CoreError; + + fn try_into(self) -> Result { + let dir: prisma_client_rust::Direction = self.direction.into(); + + Ok(match self.order_by.to_lowercase().as_str() { + "name" => library::name::order(dir), + "path" => library::path::order(dir), + "status" => library::status::order(dir), + _ => { + return Err(CoreError::InvalidQuery(format!( + "You cannot order library by {:?}", + self.order_by + ))) + }, + }) + } +} + impl TryInto for QueryOrder { type Error = CoreError; @@ -108,28 +113,28 @@ impl TryInto for QueryOrder { } } -pub trait FindManyTrait { - fn paginated(self, page_params: PageParams) -> Self; -} - -impl FindManyTrait - for FindMany<'_, Where, With, OrderBy, Cursor, Set, Data> -where - Where: Into, - With: Into, - OrderBy: Into<(String, PrismaValue)>, - Cursor: Into, - Set: Into<(String, PrismaValue)>, - Data: DeserializeOwned, -{ - fn paginated(self, page_params: PageParams) -> Self { - let skip = match page_params.zero_based { - true => page_params.page * page_params.page_size, - false => (page_params.page - 1) * page_params.page_size, - } as i64; - - let take = page_params.page_size as i64; - - self.skip(skip).take(take) - } -} +// pub trait FindManyTrait { +// fn paginated(self, page_params: PageParams) -> Self; +// } + +// impl FindManyTrait +// for FindMany<'_, Where, With, OrderBy, Cursor, Set, Data> +// where +// Where: Into, +// With: Into, +// OrderBy: Into<(String, PrismaValue)>, +// Cursor: Into, +// Set: Into<(String, PrismaValue)>, +// Data: DeserializeOwned, +// { +// fn paginated(self, page_params: PageParams) -> Self { +// let skip = match page_params.zero_based { +// true => page_params.page * page_params.page_size, +// false => (page_params.page - 1) * page_params.page_size, +// } as i64; + +// let take = page_params.page_size as i64; + +// self.skip(skip).take(take) +// } +// } diff --git a/core/src/types/models/media.rs b/core/src/types/models/media.rs deleted file mode 100644 index a0743cc0c..000000000 --- a/core/src/types/models/media.rs +++ /dev/null @@ -1,165 +0,0 @@ -use std::{path::PathBuf, str::FromStr}; - -use serde::{Deserialize, Serialize}; -use specta::Type; - -use crate::{config::context::Ctx, prisma, types::enums::FileStatus}; - -use super::{read_progress::ReadProgress, series::Series, tag::Tag}; - -#[derive(Debug, Clone, Deserialize, Serialize, Type)] -pub struct Media { - pub id: String, - /// The name of the media. ex: "The Amazing Spider-Man (2018) #69" - pub name: String, - /// The description of the media. ex: "Spidey and his superspy sister, Teresa Parker, dig to uncover THE CHAMELEON CONSPIRACY." - pub description: Option, - /// The size of the media in bytes. - pub size: i32, - /// The file extension of the media. ex: "cbz" - pub extension: String, - /// The number of pages in the media. ex: "69" - pub pages: i32, - // pub updated_at: DateTime, - pub updated_at: String, - /// The checksum hash of the file contents. Used to ensure only one instance of a file in the database. - pub checksum: Option, - /// The path of the media. ex: "/home/user/media/comics/The Amazing Spider-Man (2018) #69.cbz" - pub path: String, - /// The status of the media - pub status: FileStatus, - /// The ID of the series this media belongs to. - pub series_id: String, - // The series this media belongs to. Will be `None` only if the relation is not loaded. - pub series: Option, - /// The read progresses of the media. Will be `None` only if the relation is not loaded. - pub read_progresses: Option>, - /// The current page of the media, computed from `read_progresses`. Will be `None` only - /// if the `read_progresses` relation is not loaded. - pub current_page: Option, - /// The user assigned tags for the media. ex: ["comic", "spiderman"]. Will be `None` only if the relation is not loaded. - pub tags: Option>, - // pub status: String, -} - -// Note: used internally... -pub struct TentativeMedia { - pub name: String, - pub description: Option, - pub size: i32, - pub extension: String, - pub pages: i32, - pub checksum: Option, - pub path: String, - pub series_id: String, -} - -impl TentativeMedia { - pub fn into_action(self, ctx: &Ctx) -> prisma::media::Create { - ctx.db.media().create( - self.name, - self.size, - self.extension, - self.pages, - self.path, - vec![ - prisma::media::checksum::set(self.checksum), - prisma::media::description::set(self.description), - prisma::media::series::connect(prisma::series::id::equals( - self.series_id, - )), - ], - ) - } -} - -impl From for Media { - fn from(data: prisma::media::Data) -> Media { - let series = match data.series() { - Ok(series) => Some(series.unwrap().to_owned().into()), - Err(_e) => None, - }; - - let (read_progresses, current_page) = match data.read_progresses() { - Ok(read_progresses) => { - let progress = read_progresses - .iter() - .map(|rp| rp.to_owned().into()) - .collect::>(); - - // Note: ugh. - if let Some(p) = progress.first().cloned() { - (Some(progress), Some(p.page)) - } else { - (Some(progress), None) - } - }, - Err(_e) => (None, None), - }; - - let tags = match data.tags() { - Ok(tags) => Some(tags.iter().map(|tag| tag.to_owned().into()).collect()), - Err(_e) => None, - }; - - Media { - id: data.id, - name: data.name, - description: data.description, - size: data.size, - extension: data.extension, - pages: data.pages, - updated_at: data.updated_at.to_string(), - checksum: data.checksum, - path: data.path, - status: FileStatus::from_str(&data.status).unwrap_or(FileStatus::Error), - series_id: data.series_id.unwrap(), - series, - read_progresses, - current_page, - tags, - } - } -} - -// Derived from ComicInfo.xml -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Type, Default)] - -pub struct MediaMetadata { - #[serde(rename = "Series")] - pub series: Option, - #[serde(rename = "Number")] - pub number: Option, - #[serde(rename = "Web")] - pub web: Option, - #[serde(rename = "Summary")] - pub summary: Option, - #[serde(rename = "Publisher")] - pub publisher: Option, - #[serde(rename = "Genre")] - pub genre: Option, - #[serde(rename = "PageCount")] - pub page_count: Option, -} - -// impl MediaMetadata { -// pub fn default() -> Self { -// Self { -// series: None, -// number: None, -// web: None, -// summary: None, -// publisher: None, -// genre: None, -// page_count: None, -// } -// } -// } - -pub struct ProcessedMediaFile { - pub thumbnail_path: Option, - pub path: PathBuf, - pub checksum: Option, - pub metadata: Option, - pub pages: i32, -} diff --git a/core/src/types/models/readinglist.rs b/core/src/types/models/readinglist.rs deleted file mode 100644 index 783088aac..000000000 --- a/core/src/types/models/readinglist.rs +++ /dev/null @@ -1,29 +0,0 @@ -use serde::{Deserialize, Serialize}; -use specta::Type; - -use crate::prisma::{self}; - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -pub struct ReadingList { - pub id: String, - pub name: String, - pub creating_user_id: String, - pub description: Option, -} - -impl From for ReadingList { - fn from(data: prisma::reading_list::Data) -> ReadingList { - ReadingList { - id: data.id, - name: data.name, - creating_user_id: data.creating_user_id, - description: data.description, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -pub struct CreateReadingList { - pub id: String, - pub media_ids: Vec -} \ No newline at end of file diff --git a/core/src/types/server/http.rs b/core/src/types/server/http.rs deleted file mode 100644 index 42fa1fe82..000000000 --- a/core/src/types/server/http.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::path::Path; - -use serde::Serialize; - -use crate::fs::media_file::infer_mime_from_path; - -/// [`ContentType`] is an enum that represents the HTTP content type. This is a smaller -/// subset of the full list of content types, mostly focusing on types supported by Stump. -#[allow(non_camel_case_types)] -#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)] -pub enum ContentType { - XHTML, - XML, - HTML, - PDF, - EPUB_ZIP, - ZIP, - COMIC_ZIP, - RAR, - COMIC_RAR, - PNG, - JPEG, - WEBP, - GIF, - UNKNOWN, -} - -impl ContentType { - /// Returns the content type from the file extension. - /// - /// ## Examples - /// ```rust - /// use stump_core::types::server::http::ContentType; - /// - /// let content_type = ContentType::from_extension("png"); - /// assert_eq!(content_type, Some(ContentType::PNG)); - /// ``` - pub fn from_extension(extension: &str) -> Option { - match extension.to_lowercase().as_str() { - "xhtml" => Some(ContentType::XHTML), - "xml" => Some(ContentType::XML), - "html" => Some(ContentType::HTML), - "pdf" => Some(ContentType::PDF), - "epub" => Some(ContentType::EPUB_ZIP), - "zip" => Some(ContentType::ZIP), - "cbz" => Some(ContentType::COMIC_ZIP), - "rar" => Some(ContentType::RAR), - "cbr" => Some(ContentType::COMIC_RAR), - "png" => Some(ContentType::PNG), - "jpg" => Some(ContentType::JPEG), - "jpeg" => Some(ContentType::JPEG), - "webp" => Some(ContentType::WEBP), - "gif" => Some(ContentType::GIF), - _ => None, - } - } - - pub fn from_infer(path: &Path) -> ContentType { - infer_mime_from_path(path) - .map(|mime| ContentType::from(mime.as_str())) - .unwrap_or_else(|| { - ContentType::from_extension( - path.extension() - .unwrap_or_default() - .to_str() - .unwrap_or_default(), - ) - .unwrap_or(ContentType::UNKNOWN) - }) - } - - /// Returns true if the content type is an image. - /// - /// # Examples - /// ```rust - /// use stump_core::types::server::http::ContentType; - /// - /// let content_type = ContentType::PNG; - /// assert!(content_type.is_image()); - /// - /// let content_type = ContentType::XHTML; - /// assert!(!content_type.is_image()); - /// ``` - pub fn is_image(&self) -> bool { - self.to_string().starts_with("image") - } -} - -impl From<&str> for ContentType { - fn from(s: &str) -> Self { - match s.to_lowercase().as_str() { - "application/xhtml+xml" => ContentType::XHTML, - "application/xml" => ContentType::XML, - "text/html" => ContentType::HTML, - "application/pdf" => ContentType::PDF, - "application/epub+zip" => ContentType::EPUB_ZIP, - "application/zip" => ContentType::ZIP, - "application/vnd.comicbook+zip" => ContentType::COMIC_ZIP, - "application/vnd.rar" => ContentType::RAR, - "application/vnd.comicbook-rar" => ContentType::COMIC_RAR, - "image/png" => ContentType::PNG, - "image/jpeg" => ContentType::JPEG, - "image/webp" => ContentType::WEBP, - "image/gif" => ContentType::GIF, - _ => ContentType::UNKNOWN, - } - } -} - -impl std::fmt::Display for ContentType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ContentType::XHTML => write!(f, "application/xhtml+xml"), - ContentType::XML => write!(f, "application/xml"), - ContentType::HTML => write!(f, "text/html"), - ContentType::PDF => write!(f, "application/pdf"), - ContentType::EPUB_ZIP => write!(f, "application/epub+zip"), - ContentType::ZIP => write!(f, "application/zip"), - ContentType::COMIC_ZIP => write!(f, "application/vnd.comicbook+zip"), - ContentType::RAR => write!(f, "application/vnd.rar"), - ContentType::COMIC_RAR => write!(f, "application/vnd.comicbook-rar"), - ContentType::PNG => write!(f, "image/png"), - ContentType::JPEG => write!(f, "image/jpeg"), - ContentType::WEBP => write!(f, "image/webp"), - ContentType::GIF => write!(f, "image/gif"), - ContentType::UNKNOWN => write!(f, "unknown"), - } - } -} diff --git a/core/src/types/server/pageable.rs b/core/src/types/server/pageable.rs deleted file mode 100644 index 1b0fe81a6..000000000 --- a/core/src/types/server/pageable.rs +++ /dev/null @@ -1,257 +0,0 @@ -use serde::{Deserialize, Serialize}; -use specta::Type; -use tracing::trace; - -use crate::types::DirectoryListing; - -use super::Direction; - -#[derive(Debug, Deserialize, Serialize, Type)] -pub struct PagedRequestParams { - pub unpaged: Option, - pub zero_based: Option, - pub page: Option, - pub page_size: Option, - pub order_by: Option, - pub direction: Option, -} - -#[derive(Debug, Serialize, Clone, Type)] -pub struct PageParams { - pub zero_based: bool, - pub page: u32, - pub page_size: u32, - pub order_by: String, - pub direction: Direction, -} - -impl Default for PageParams { - fn default() -> Self { - PageParams { - zero_based: false, - page: 0, - page_size: 20, - order_by: "name".to_string(), - direction: Direction::Asc, - } - } -} - -impl From> for PageParams { - fn from(req_params: Option) -> Self { - match req_params { - Some(params) => { - let zero_based = params.zero_based.unwrap_or(false); - let page_size = params.page_size.unwrap_or(20); - - let default_page = if zero_based { 0 } else { 1 }; - - let page = params.page.unwrap_or(default_page); - - PageParams { - page, - page_size, - zero_based, - order_by: params.order_by.unwrap_or_else(|| "name".to_string()), - direction: params.direction.unwrap_or_default(), - } - }, - None => PageParams::default(), - } - } -} - -#[derive(Serialize)] -pub struct PageLinks { - /// The current request URL. E.g. http://example.com/api/v1/users?page=2 - #[serde(rename = "self")] - pub itself: String, - /// The start URL, relative to current paginated request URL. E.g. http://example.com/api/v1/users?page=0 - pub start: String, - /// The prev URL, relative to current paginated request URL. E.g. http://example.com/api/v1/users?page=1 - pub prev: Option, - /// The next URL, relative to current paginated request URL. E.g. http://example.com/api/v1/users?page=3 - pub next: Option, -} - -#[derive(Serialize, Type)] -pub struct PageInfo { - /// The number of pages available. - pub total_pages: u32, - /// The current page, zero-indexed. - pub current_page: u32, - /// The number of elements per page. - pub page_size: u32, - /// The offset of the current page. E.g. if current page is 1, and pageSize is 10, the offset is 20. - pub page_offset: u32, - /// Whether or not the page is zero-indexed. - pub zero_based: bool, -} - -impl PageInfo { - pub fn new(page_params: PageParams, total_pages: u32) -> Self { - let current_page = page_params.page; - let page_size = page_params.page_size; - let zero_based = page_params.zero_based; - - PageInfo { - total_pages, - current_page, - page_size, - page_offset: current_page * page_size, - zero_based, - } - } -} - -#[derive(Serialize, Type)] -pub struct Pageable { - /// The target data being returned. - pub data: T, - /// The pagination information (if paginated). - pub _page: Option, - // FIXME: removing for now. - // /// The links to other pages (if paginated). - // pub _links: Option, -} - -impl Pageable { - pub fn unpaged(data: T) -> Self { - Pageable { - data, - _page: None, - // _links: None, - } - } - - pub fn new(data: T, page_info: PageInfo) -> Self { - Pageable { - data, - _page: Some(page_info), - } - } -} - -impl From> for Pageable> -where - T: Serialize + Clone, -{ - fn from(vec: Vec) -> Pageable> { - Pageable::unpaged(vec) - } -} - -impl From<(Vec, PageParams)> for Pageable> -where - T: Serialize + Clone, -{ - fn from(tuple: (Vec, PageParams)) -> Pageable> { - let (mut data, page_params) = tuple; - - let total_pages = - (data.len() as f32 / page_params.page_size as f32).ceil() as u32; - - let start = match page_params.zero_based { - true => page_params.page * page_params.page_size, - false => (page_params.page - 1) * page_params.page_size, - }; - - // let start = page_params.page * page_params.page_size; - let end = start + page_params.page_size; - - // println!("len:{}, start: {}, end: {}", data.len(), start, end); - - if start > data.len() as u32 { - data = vec![]; - } else if end < data.len() as u32 { - data = data - .get((start as usize)..(end as usize)) - .ok_or("Invalid page") - .unwrap() - .to_vec(); - } else { - data = data - .get((start as usize)..) - .ok_or("Invalid page") - .unwrap() - .to_vec(); - } - - Pageable::new(data, PageInfo::new(page_params, total_pages)) - } -} - -impl From<(Vec, Option)> for Pageable> -where - T: Serialize + Clone, -{ - fn from(tuple: (Vec, Option)) -> Pageable> { - (tuple.0, PageParams::from(tuple.1)).into() - } -} - -// Note: this is used when you have to query the database for the total number of pages. -impl From<(Vec, i64, PageParams)> for Pageable> -where - T: Serialize + Clone, -{ - fn from(tuple: (Vec, i64, PageParams)) -> Pageable> { - let (data, db_total, page_params) = tuple; - - let total_pages = (db_total as f32 / page_params.page_size as f32).ceil() as u32; - - Pageable::new(data, PageInfo::new(page_params, total_pages)) - } -} - -impl From<(DirectoryListing, u32, u32)> for Pageable { - fn from(tuple: (DirectoryListing, u32, u32)) -> Pageable { - let (data, page, page_size) = tuple; - - let total_pages = (data.files.len() as f32 / page_size as f32).ceil() as u32; - // directory listing will always be zero-based. - let start = (page - 1) * page_size; - let end = start + page_size; - - let mut truncated_files = data.files; - - if start > truncated_files.len() as u32 { - truncated_files = vec![]; - } else if end < truncated_files.len() as u32 { - truncated_files = truncated_files - .get((start as usize)..(end as usize)) - .ok_or("Invalid page") - .unwrap() - .to_vec(); - } else { - truncated_files = truncated_files - .get((start as usize)..) - .ok_or("Invalid page") - .unwrap() - .to_vec(); - } - - trace!( - "{} total pages of size {}. Returning truncated data of size {}.", - total_pages, - page_size, - truncated_files.len() - ); - - let truncated_data = DirectoryListing { - parent: data.parent, - files: truncated_files, - }; - - Pageable::new( - truncated_data, - PageInfo { - total_pages, - current_page: page, - page_size, - page_offset: page * page_size, - zero_based: false, - }, - ) - } -} diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..39ee94057 --- /dev/null +++ b/flake.lock @@ -0,0 +1,92 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1672428209, + "narHash": "sha256-eejhqkDz2cb2vc5VeaWphJz8UXNuoNoM8/Op8eWv2tQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "293a28df6d7ff3dec1e61e37cc4ee6e6c0fb0847", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1665296151, + "narHash": "sha256-uOB0oxqxN9K7XGF1hcnY+PQnlQJ+3bP2vCn/+Ru/bbc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "14ccaaedd95a488dd7ae142757884d8e125b3363", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1672453260, + "narHash": "sha256-ruR2xo30Vn7kY2hAgg2Z2xrCvNePxck6mgR5a8u+zow=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "176b6fd3dd3d7cea8d22ab1131364a050228d94c", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..919737ac9 --- /dev/null +++ b/flake.nix @@ -0,0 +1,76 @@ +{ + inputs = { + nixpkgs.url = "nixpkgs"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay.url = "github:oxalica/rust-overlay"; + }; + + outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { + inherit system overlays; + }; + + libraries = with pkgs;[ + webkitgtk + gtk3 + cairo + gdk-pixbuf + glib + dbus + openssl_3 + ]; + + packages = with pkgs; [ + # node + nodePackages.pnpm + nodejs + + # rust + rustfmt + clippy + rustc + cargo + cargo-deny + cargo-edit + cargo-watch + + # Tauri deps + curl + wget + pkg-config + dbus + openssl_3 + glib + gtk3 + libsoup + webkitgtk + + # avoid openssl linking error when local git version isn't compatible with openssl_3 + git + ]; + in + { + devShell = pkgs.mkShell { + + buildInputs = packages ++ [( + # Needed for rust-analyzer + pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" ]; + } + )]; + + # Needed for rust-analyzer + RUST_SRC_PATH = "${pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" ]; + }}/lib/rustlib/src/rust/library"; + + shellHook = + '' + export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH + ''; + }; + }); +} diff --git a/package.json b/package.json index a8faa658d..0afa3348e 100644 --- a/package.json +++ b/package.json @@ -1,52 +1,66 @@ { - "name": "@stump/monorepo", - "version": "0.0.0", - "repository": "https://github.com/aaronleopold/stump.git", - "author": "Aaron Leopold ", - "license": "MIT", - "scripts": { - "prepare": "husky install", - "setup": "pnpm i && pnpm web build && pnpm core run setup", - "checks": "pnpm -r check", - "clippy": "cargo clippy -- -D warnings", - "tests": "pnpm -r test", - "start:web": "pnpm run server start", - "dev:server": "cargo watch --ignore common -x 'run --manifest-path=apps/server/Cargo.toml --package stump_server'", - "dev:web": "concurrently -n server,web -c green.bold,blue.bold \"pnpm dev:server\" \"pnpm web dev\"", - "dev:desktop": "concurrently -n server,desktop -c green.bold,blue.bold \"pnpm run server dev\" \"pnpm desktop dev\"", - "core": "pnpm --filter @stump/core --", - "prisma": "pnpm core prisma", - "codegen": "pnpm -r codegen", - "web": "pnpm --filter @stump/web --", - "server": "pnpm --filter @stump/server --", - "desktop": "pnpm --filter @stump/desktop --", - "interface": "pnpm --filter @stump/interface --", - "client": "pnpm --filter @stump/client --", - "build:server": "pnpm run server build", - "build:web": "pnpm web build && pnpm build:server", - "build:desktop": "pnpm desktop build", - "build:docker": "docker buildx build --push --platform=linux/arm64/v8,linux/amd64 -t aaronleopold/stump-preview:latest .", - "cache:docker": "docker buildx build --platform=linux/arm64/v8,linux/amd64 -t aaronleopold/stump-preview:latest .", - "cache:docker-arm": "docker buildx build --platform=linux/arm64/v8 -t aaronleopold/stump-preview:latest .", - "build:docker-amd": "docker buildx build --push --platform=linux/amd64 -t aaronleopold/stump-preview:latest ." - }, - "devDependencies": { - "concurrently": "^7.4.0", - "cpy-cli": "^4.2.0", - "husky": "^8.0.1", - "lint-staged": "^13.0.3", - "move-cli": "2.0.0", - "prettier": "^2.7.1", - "trash-cli": "^5.0.0" - }, - "lint-staged": { - "*.{js,jsx,ts,tsx,md}": [ - "prettier --write" - ], - ".rs": [ - "cargo fmt --manifest-path=core/Cargo.toml --", - "cargo fmt --manifest-path=apps/server/Cargo.toml --", - "cargo fmt --manifest-path=apps/desktop/src-tauri/Cargo.toml --" - ] - } -} \ No newline at end of file + "name": "@stump/monorepo", + "version": "0.0.0", + "repository": "https://github.com/aaronleopold/stump.git", + "author": "Aaron Leopold ", + "license": "MIT", + "scripts": { + "prepare": "husky install", + "setup": "pnpm i && pnpm web build && pnpm core run setup", + "lint": "eslint --ext .ts,.tsx,.cts,.mts,.js,.jsx,.cjs,.mjs --fix --report-unused-disable-directives --no-error-on-unmatched-pattern --exit-on-fatal-error --ignore-path .gitignore .", + "client": "pnpm --filter @stump/client --", + "desktop": "pnpm --filter @stump/desktop --", + "interface": "pnpm --filter @stump/interface --", + "web": "pnpm --filter @stump/web --", + "server": "pnpm --filter @stump/server --", + "dev:desktop": "concurrently -n server,desktop -c green.bold,blue.bold \"pnpm run server dev\" \"pnpm desktop dev\"", + "core": "pnpm --filter @stump/core --", + "prisma": "pnpm core prisma", + "codegen": "pnpm -r codegen", + "build:server": "pnpm run server build", + "build:web": "pnpm web build && pnpm build:server", + "build:desktop": "pnpm desktop build", + "moon": "moon --color --log trace" + }, + "devDependencies": { + "@babel/core": "^7.20.12", + "@moonrepo/cli": "^0.21.4", + "@typescript-eslint/eslint-plugin": "^5.52.0", + "@typescript-eslint/parser": "^5.52.0", + "babel-preset-moon": "^1.1.4", + "concurrently": "^7.6.0", + "cpy-cli": "^4.2.0", + "eslint": "^8.34.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-simple-import-sort": "^8.0.0", + "eslint-plugin-sort-keys-fix": "^1.1.2", + "husky": "^8.0.3", + "lint-staged": "^13.1.2", + "move-cli": "2.0.0", + "prettier": "^2.8.4", + "prettier-eslint": "^15.0.1", + "trash-cli": "^5.0.0", + "tsconfig-moon": "^1.2.1", + "typescript": "^4.9.5" + }, + "lint-staged": { + "*.{js,jsx,ts,tsx,md}": [ + "prettier --write" + ], + ".rs": [ + "cargo fmt --manifest-path=core/Cargo.toml --", + "cargo fmt --manifest-path=apps/server/Cargo.toml --", + "cargo fmt --manifest-path=apps/desktop/src-tauri/Cargo.toml --" + ] + }, + "packageManager": "pnpm@7.18.2", + "engines": { + "node": "18.12.0" + }, + "resolutions": { + "esbuild": "^0.15.13" + } +} diff --git a/packages/api/package.json b/packages/api/package.json new file mode 100644 index 000000000..34f6154a9 --- /dev/null +++ b/packages/api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@stump/api", + "version": "0.0.0", + "description": "", + "main": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./types": "./src/types.ts" + }, + "license": "MIT", + "devDependencies": { + "typescript": "^4.9.5" + }, + "dependencies": { + "@stump/types": "workspace:*", + "axios": "^1.3.3" + } +} diff --git a/packages/api/src/auth.ts b/packages/api/src/auth.ts new file mode 100644 index 000000000..a7aaae391 --- /dev/null +++ b/packages/api/src/auth.ts @@ -0,0 +1,22 @@ +import type { LoginOrRegisterArgs, User } from '@stump/types' + +import { API } from '.' +import { ApiResult } from './types' + +// TODO: types + +export function me(): Promise> { + return API.get('/auth/me') +} + +export function login(input: LoginOrRegisterArgs): Promise> { + return API.post('/auth/login', input) +} + +export function register(payload: LoginOrRegisterArgs) { + return API.post('/auth/register', payload) +} + +export function logout(): Promise> { + return API.post('/auth/logout') +} diff --git a/packages/api/src/axios.ts b/packages/api/src/axios.ts new file mode 100644 index 000000000..dff8696ad --- /dev/null +++ b/packages/api/src/axios.ts @@ -0,0 +1,49 @@ +import axios, { AxiosInstance } from 'axios' + +export let API: AxiosInstance + +// TODO: make not bad +export function initializeApi(baseUrl: string, version: string) { + let correctedUrl = baseUrl + + // remove trailing slash + if (correctedUrl.endsWith('/')) { + correctedUrl = correctedUrl.slice(0, -1) + } + + const isValid = correctedUrl.endsWith(`/api/${version}`) + const hasApiPiece = !isValid && correctedUrl.endsWith('/api') + + if (!isValid && !hasApiPiece) { + correctedUrl += `/api/${version}` + } else if (hasApiPiece) { + correctedUrl += `/${version}` + } + + // remove all double slashes AFTER the initial http:// or https:// or whatever + correctedUrl = correctedUrl.replace(/([^:]\/)\/+/g, '$1') + + API = axios.create({ + baseURL: correctedUrl, + withCredentials: true, + }) +} + +export function apiIsInitialized() { + return !!API +} + +// TODO: make not bad +export function isUrl(url: string) { + return url.startsWith('http://') || url.startsWith('https://') +} + +export async function checkUrl(url: string, version = 'v1') { + if (!isUrl(url)) { + return false + } + + const res = await fetch(`${url}/api/${version}/ping`).catch((err) => err) + + return res.status === 200 +} diff --git a/packages/api/src/config.ts b/packages/api/src/config.ts new file mode 100644 index 000000000..0d1c18f43 --- /dev/null +++ b/packages/api/src/config.ts @@ -0,0 +1,12 @@ +import type { ClaimResponse } from '@stump/types' + +import { API } from '.' +import { ApiResult } from './types' + +export function ping() { + return API.get('/ping') +} + +export async function checkIsClaimed(): Promise> { + return API.get('/claim') +} diff --git a/common/client/src/api/epub.ts b/packages/api/src/epub.ts similarity index 53% rename from common/client/src/api/epub.ts rename to packages/api/src/epub.ts index f7360b71b..073e568a3 100644 --- a/common/client/src/api/epub.ts +++ b/packages/api/src/epub.ts @@ -1,20 +1,22 @@ -import type { ApiResult, Epub } from '../types'; -import { API } from '.'; +import type { Epub } from '@stump/types' + +import { API } from '.' +import { ApiResult } from './types' export function getEpubBaseUrl(id: string): string { - return `${API.getUri()}/epub/${id}`; + return `${API.getUri()}/epub/${id}` } export function getEpubById(id: string): Promise> { - return API.get(`/epub/${id}`); + return API.get(`/epub/${id}`) } // This returns raw epub data (e.g. HTML, XHTML, CSS, etc.) // TODO: type this?? export function getEpubResource(payload: { - id: string; - root?: string; - resourceId: string; -}): Promise> { - return API.get(`/epub/${payload.id}/${payload.root ?? 'META-INF'}/${payload.resourceId}`); + id: string + root?: string + resourceId: string +}): Promise> { + return API.get(`/epub/${payload.id}/${payload.root ?? 'META-INF'}/${payload.resourceId}`) } diff --git a/packages/api/src/filesystem.ts b/packages/api/src/filesystem.ts new file mode 100644 index 000000000..b1cf13f5c --- /dev/null +++ b/packages/api/src/filesystem.ts @@ -0,0 +1,18 @@ +import type { DirectoryListing, DirectoryListingInput, Pageable } from '@stump/types' + +import { API } from '.' +import { ApiResult } from './types' + +interface ListDirectoryFnInput extends DirectoryListingInput { + page?: number +} + +export function listDirectory( + input?: ListDirectoryFnInput, +): Promise>> { + if (input?.page != null) { + return API.post(`/filesystem?page=${input.page}`, input) + } + + return API.post('/filesystem', input) +} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts new file mode 100644 index 000000000..9a50d714e --- /dev/null +++ b/packages/api/src/index.ts @@ -0,0 +1,15 @@ +export * from './auth' +export * from './axios' +export * from './config' +export * from './epub' +export * from './filesystem' +export * from './job' +export * from './library' +export * from './log' +export * from './media' +export * from './series' +export * from './server' +export * from './tag' +export * from './types' +export * from './user' +export * from './utils' diff --git a/packages/api/src/job.ts b/packages/api/src/job.ts new file mode 100644 index 000000000..daed4326f --- /dev/null +++ b/packages/api/src/job.ts @@ -0,0 +1,13 @@ +import type { JobReport } from '@stump/types' + +import { API } from '.' +import { ApiResult } from './types' + +export function getJobs(): Promise> { + return API.get('/jobs') +} + +// TODO: type this +export function cancelJob(id: string): Promise> { + return API.delete(`/jobs/${id}/cancel`) +} diff --git a/common/client/src/api/library.ts b/packages/api/src/library.ts similarity index 72% rename from common/client/src/api/library.ts rename to packages/api/src/library.ts index f254a1715..ef86853e7 100644 --- a/common/client/src/api/library.ts +++ b/packages/api/src/library.ts @@ -1,25 +1,25 @@ import type { - ApiResult, CreateLibraryArgs, - UpdateLibraryArgs, LibrariesStats, Library, - PageableApiResult, - Series, LibraryScanMode, -} from '../types'; -import { API } from '.'; + Series, + UpdateLibraryArgs, +} from '@stump/types' + +import { API } from '.' +import { ApiResult, PageableApiResult } from './types' export function getLibraries(): Promise> { - return API.get('/libraries?unpaged=true'); + return API.get('/libraries?unpaged=true') } export function getLibrariesStats(): Promise> { - return API.get('/libraries/stats'); + return API.get('/libraries/stats') } export function getLibraryById(id: string): Promise> { - return API.get(`/libraries/${id}`); + return API.get(`/libraries/${id}`) } export function getLibrarySeries( @@ -28,28 +28,28 @@ export function getLibrarySeries( params?: string, ): Promise> { if (params) { - return API.get(`/libraries/${id}/series?page=${page}&${params}`); + return API.get(`/libraries/${id}/series?page=${page}&${params}`) } - return API.get(`/libraries/${id}/series?page=${page}`); + return API.get(`/libraries/${id}/series?page=${page}`) } // FIXME: type this lol // TODO: narrow mode type to exclude NONE // TODO: fix function signature to work with react-query export function scanLibary(id: string, mode?: LibraryScanMode): Promise> { - return API.get(`/libraries/${id}/scan?scan_mode=${mode ?? 'BATCHED'}`); + return API.get(`/libraries/${id}/scan?scan_mode=${mode ?? 'BATCHED'}`) } // TODO: type this export function deleteLibrary(id: string) { - return API.delete(`/libraries/${id}`); + return API.delete(`/libraries/${id}`) } export function createLibrary(payload: CreateLibraryArgs): Promise> { - return API.post('/libraries', payload); + return API.post('/libraries', payload) } export function editLibrary(payload: UpdateLibraryArgs): Promise> { - return API.put(`/libraries/${payload.id}`, payload); + return API.put(`/libraries/${payload.id}`, payload) } diff --git a/packages/api/src/log.ts b/packages/api/src/log.ts new file mode 100644 index 000000000..3e7773494 --- /dev/null +++ b/packages/api/src/log.ts @@ -0,0 +1,12 @@ +import type { LogMetadata } from '@stump/types' + +import { API } from '.' +import { ApiResult } from './types' + +export function getLogFileMeta(): Promise> { + return API.get('/logs') +} + +export function clearLogFile() { + return API.delete('/logs') +} diff --git a/packages/api/src/media.ts b/packages/api/src/media.ts new file mode 100644 index 000000000..22072f114 --- /dev/null +++ b/packages/api/src/media.ts @@ -0,0 +1,56 @@ +import type { Media, ReadProgress } from '@stump/types' + +import { API } from './index' +import { ApiResult, PageableApiResult } from './types' +import { urlWithParams } from './utils' + +type GetMediaById = ApiResult + +export function getMedia(params?: URLSearchParams): Promise> { + return API.get(urlWithParams('/media', params)) +} + +export function getPaginatedMedia(page: number): Promise> { + return API.get(`/media?page=${page}`) +} + +export function getMediaById(id: string): Promise { + return API.get(`/media/${id}?load_series=true`) +} + +// TODO: I see myself using this pattern a lot, so I should make a helper/wrapper for it... +export function getRecentlyAddedMedia( + page: number, + params?: URLSearchParams, +): Promise> { + if (params) { + params.set('page', page.toString()) + return API.get(urlWithParams('/media/recently-added', params)) + } + + return API.get(`/media/recently-added?page=${page}`) +} + +export function getInProgressMedia( + page: number, + params?: URLSearchParams, +): Promise> { + if (params) { + params.set('page', page.toString()) + return API.get(urlWithParams('/media/keep-reading', params)) + } + + return API.get(`/media/keep-reading?page=${page}`) +} + +export function getMediaThumbnail(id: string): string { + return `${API.getUri()}/media/${id}/thumbnail` +} + +export function getMediaPage(id: string, page: number): string { + return `${API.getUri()}/media/${id}/page/${page}` +} + +export function updateMediaProgress(id: string, page: number): Promise { + return API.put(`/media/${id}/progress/${page}`) +} diff --git a/packages/api/src/series.ts b/packages/api/src/series.ts new file mode 100644 index 000000000..dea433238 --- /dev/null +++ b/packages/api/src/series.ts @@ -0,0 +1,57 @@ +import type { Media, Series } from '@stump/types' + +import { API, getMedia } from '.' +import { ApiResult, PageableApiResult } from './types' + +export function getSeriesById(id: string): Promise> { + return API.get(`/series/${id}`) +} + +export function getSeriesMedia( + id: string, + page: number, + params?: string, +): Promise> { + if (params) { + return API.get(`/series/${id}/media?page=${page}&${params}`) + } + + return API.get(`/series/${id}/media?page=${page}`) +} + +export function getRecentlyAddedSeries( + page: number, + params?: URLSearchParams, +): Promise> { + if (params) { + params.set('page', page.toString()) + return API.get(`/series/recently-added?${params.toString()}`) + } + + return API.get(`/series/recently-added?page=${page}`) +} + +export function getNextInSeries(id: string): Promise> { + return API.get(`/series/${id}/media/next`) +} + +/** Returns a list of media within a series, ordered after the cursor. + * Default limit is 25. + * */ +export function getNextMediaInSeries( + series_id: string, + media_id: string, + limit = 25, +): Promise> { + return getMedia( + new URLSearchParams({ + cursor: media_id, + limit: limit.toString(), + series_id, + }), + ) +} + +export function getSeriesThumbnail(id: string): string { + return `${API.getUri()}/series/${id}/thumbnail` +} diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts new file mode 100644 index 000000000..96da33c94 --- /dev/null +++ b/packages/api/src/server.ts @@ -0,0 +1,8 @@ +import type { StumpVersion } from '@stump/types' + +import { API } from '.' +import { ApiResult } from './types' + +export function getStumpVersion(): Promise> { + return API.post('/version') +} diff --git a/packages/api/src/tag.ts b/packages/api/src/tag.ts new file mode 100644 index 000000000..60fb272c3 --- /dev/null +++ b/packages/api/src/tag.ts @@ -0,0 +1,12 @@ +import type { Tag } from '@stump/types' + +import { API } from '.' +import { ApiResult } from './types' + +export function getAllTags(): Promise> { + return API.get('/tags') +} + +export function createTags(tags: string[]): Promise> { + return API.post('/tags', { tags }) +} diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts new file mode 100644 index 000000000..365057f70 --- /dev/null +++ b/packages/api/src/types.ts @@ -0,0 +1,4 @@ +import { ApiError, Pageable } from '@stump/types' + +export type ApiResult = import('axios').AxiosResponse> +export type PageableApiResult = ApiResult> diff --git a/packages/api/src/user.ts b/packages/api/src/user.ts new file mode 100644 index 000000000..8b447d600 --- /dev/null +++ b/packages/api/src/user.ts @@ -0,0 +1,19 @@ +import type { UpdateUserArgs, User, UserPreferences } from '@stump/types' + +import { API } from '.' +import { ApiResult } from './types' + +export function getUserPreferences(userId: string): Promise> { + return API.get(`/users/${userId}/preferences`) +} + +export function updateUserPreferences( + userId: string, + preferences: UserPreferences, +): Promise> { + return API.put(`/users/${userId}/preferences`, preferences) +} + +export function updateUser(userId: string, params: UpdateUserArgs): Promise> { + return API.put(`/users/${userId}`, params) +} diff --git a/packages/api/src/utils.ts b/packages/api/src/utils.ts new file mode 100644 index 000000000..faa5d4f8a --- /dev/null +++ b/packages/api/src/utils.ts @@ -0,0 +1,31 @@ +/** Formats a string with UrlSearchParams */ +export const urlWithParams = (url: string, params?: URLSearchParams) => { + const paramString = params?.toString() + if (paramString?.length) { + return `${url}?${paramString}` + } + return url +} + +/** Convert an object to `UrlSearchParams`. Will work for deeply nested objects, as well. */ +export const toUrlParams = ( + obj: T, + params = new URLSearchParams(), + prefix?: string, +) => { + Object.entries(obj).forEach(([key, value]) => { + if (value !== null && typeof value === 'object') { + if (prefix) { + toUrlParams(value, params, `${prefix}_${key}`) + } else { + toUrlParams(value, params, key) + } + } else if (prefix) { + params.append(`${prefix}_${key}`, value) + } else { + params.append(key, value) + } + }) + + return params +} diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json new file mode 100644 index 000000000..34f4cfbcb --- /dev/null +++ b/packages/api/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "skipLibCheck": true, + "outDir": "../../.moon/cache/types/packages/api", + "paths": { + "@stump/types": [ + "../types/index.ts" + ], + "@stump/types/*": [ + "../types/*" + ] + } + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules" + ], + "references": [ + { + "path": "../types" + } + ] +} diff --git a/common/client/README.md b/packages/client/README.md similarity index 100% rename from common/client/README.md rename to packages/client/README.md diff --git a/packages/client/package.json b/packages/client/package.json new file mode 100644 index 000000000..6fb1b3164 --- /dev/null +++ b/packages/client/package.json @@ -0,0 +1,27 @@ +{ + "name": "@stump/client", + "version": "0.0.0", + "private": true, + "main": "src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@stump/api": "workspace:*", + "@stump/types": "workspace:*", + "@tanstack/react-query": "^4.20.4", + "axios": "^1.3.3", + "immer": "^9.0.19", + "react-use-websocket": "^4.3.1", + "zustand": "^4.3.3" + }, + "devDependencies": { + "@types/axios": "^0.14.0", + "@types/node": "^18.14.0", + "@types/react": "^18.0.28", + "react": "^18.2.0", + "prettier": "^2.8.4", + "tsconfig": "*", + "typescript": "^4.9.5" + } +} diff --git a/packages/client/src/Provider.tsx b/packages/client/src/Provider.tsx new file mode 100644 index 000000000..7494017a5 --- /dev/null +++ b/packages/client/src/Provider.tsx @@ -0,0 +1,84 @@ +import { JobUpdate } from '@stump/types' +import { ReactElement, useState } from 'react' + +import { queryClient, QueryClientProvider } from './client' +import { + ActiveJobContext, + QueryClientContext, + StumpClientContext, + StumpClientContextProps, +} from './context' + +type Props = { + children: ReactElement +} & StumpClientContextProps +export function StumpClientContextProvider({ children, onRedirect }: Props) { + // lol this is so scuffed + return ( + + + {children} + + + ) +} + +export function JobContextProvider({ children }: { children: ReactElement }) { + const [jobs, setJobs] = useState>({}) + + function addJob(newJob: JobUpdate) { + const job = jobs[newJob.runner_id] + + if (job) { + updateJob(newJob) + } else { + setJobs((jobs) => ({ + ...jobs, + [newJob.runner_id]: newJob, + })) + } + } + + function updateJob(jobUpdate: JobUpdate) { + const job = jobs[jobUpdate.runner_id] + + if (!job || !Object.keys(jobs).length) { + addJob(jobUpdate) + return + } + + const { current_task, message, task_count } = jobUpdate + const updatedJob = { + ...job, + current_task, + message, + task_count, + } + + setJobs((jobs) => ({ + ...jobs, + [jobUpdate.runner_id]: updatedJob, + })) + } + + function removeJob(jobId: string) { + setJobs((jobs) => { + const newJobs = { ...jobs } + delete newJobs[jobId] + return newJobs + }) + } + + return ( + + {children} + + ) +} diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts new file mode 100644 index 000000000..cffd53b86 --- /dev/null +++ b/packages/client/src/client.ts @@ -0,0 +1,196 @@ +import { ApiResult } from '@stump/api' +import { Pageable } from '@stump/types' +import { + MutationFunction, + MutationKey, + QueryClient, + QueryFunction, + QueryKey, + useInfiniteQuery, + UseInfiniteQueryOptions, + useMutation as useReactMutation, + UseMutationOptions, + useQuery as useReactQuery, + UseQueryOptions, +} from '@tanstack/react-query' +import { isAxiosError } from 'axios' +import { useMemo } from 'react' + +import { QueryClientContext, useClientContext } from './context' +import { useUserStore } from './index' + +export * from './queries' +export { QueryClientProvider } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: false, + suspense: true, + }, + }, +}) + +export type QueryOptions< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = Omit< + UseQueryOptions, + 'queryKey' | 'queryFn' | 'context' +> +export function useQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + queryKey: TQueryKey, + queryFn: QueryFunction, + options?: QueryOptions, +) { + const { onRedirect } = useClientContext() || {} + const { setUser } = useUserStore((store) => ({ + setUser: store.setUser, + })) + const { onError, ...restOptions } = options || {} + return useReactQuery(queryKey, queryFn, { + context: QueryClientContext, + onError: (err) => { + if (isAxiosError(err) && err.response?.status === 401) { + setUser(null) + onRedirect?.('/auth') + } + onError?.(err) + }, + ...restOptions, + }) +} + +// FIXME: not very happy with the types here for infinite queries, a little more +// tedious than normal query wrappers +export type InfiniteQueryOptions< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = Omit< + UseInfiniteQueryOptions, + 'queryKey' | 'queryFn' | 'context' +> +export function useInfinitePagedQuery( + queryKey: TQueryKey, + queryFn: (page: number, searchParams?: URLSearchParams) => Promise>>, + searchParams = new URLSearchParams(), + options?: { + onError?: (err: unknown) => void + }, +) { + const { onRedirect } = useClientContext() || {} + const { setUser } = useUserStore((store) => ({ + setUser: store.setUser, + })) + const { onError } = options || {} + const { + data: pageData, + fetchNextPage, + hasNextPage, + isFetching, + isFetchingNextPage, + isLoading, + ...rest + } = useInfiniteQuery(queryKey, (ctx) => queryFn(ctx.pageParam || 1, searchParams), { + context: QueryClientContext, + getNextPageParam: (res) => { + const lastGroup = res.data + if (lastGroup._page) { + const currentPage = lastGroup._page.current_page + const totalPages = lastGroup._page.total_pages + + if (currentPage < totalPages) { + return lastGroup._page?.current_page + 1 + } + } + + return undefined + }, + keepPreviousData: true, + onError: (err) => { + if (isAxiosError(err) && err.response?.status === 401) { + setUser(null) + onRedirect?.('/auth') + } + onError?.(err) + }, + }) + + const data = + pageData?.pages.flatMap((res) => { + const pageable = res.data + return pageable.data + }) ?? [] + + return { + data, + fetchMore: fetchNextPage, + hasMore: hasNextPage, + isFetching: isFetching || isFetchingNextPage, + isFetchingNextPage, + isLoading: isLoading, + ...rest, + } +} + +// FIXME: make this independent of the paged query, API now supports `_cursor` param +export function useCursorQuery( + cursor: string, + queryKey: TQueryKey, + queryFn: (page: number, searchParams?: URLSearchParams) => Promise>>, + searchParams = new URLSearchParams(), +) { + const params = useMemo(() => { + const params = new URLSearchParams(searchParams) + params.set('cursor', cursor) + return params + }, [cursor, searchParams]) + + return useInfinitePagedQuery( + queryKey, + (page, searchParams) => queryFn(page, searchParams), + params, + ) +} + +export function useMutation< + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown, +>( + mutationKey: MutationKey, + mutationFn?: MutationFunction, + options?: Omit< + UseMutationOptions, + 'mutationKey' | 'mutationFn' | 'context' + >, +) { + const { onRedirect } = useClientContext() || {} + const { setUser } = useUserStore((store) => ({ + setUser: store.setUser, + })) + const { onError, ...restOptions } = options || {} + + return useReactMutation(mutationKey, mutationFn, { + context: QueryClientContext, + onError: (err, variables, context) => { + if (isAxiosError(err) && err.response?.status === 401) { + setUser(null) + onRedirect?.('/auth') + } + onError?.(err, variables, context) + }, + ...restOptions, + }) +} diff --git a/packages/client/src/context.ts b/packages/client/src/context.ts new file mode 100644 index 000000000..9aec2c305 --- /dev/null +++ b/packages/client/src/context.ts @@ -0,0 +1,36 @@ +import { JobUpdate } from '@stump/types' +import { QueryClient } from '@tanstack/react-query' +import { createContext, useContext } from 'react' + +export const AppPropsContext = createContext(null) +export const QueryClientContext = createContext(undefined) + +export type StumpClientContextProps = { + onRedirect?: (url: string) => void +} +export const StumpClientContext = createContext(undefined) + +export type Platform = 'browser' | 'macOS' | 'windows' | 'linux' | 'unknown' + +export interface AppProps { + platform: Platform + baseUrl?: string + demoMode?: boolean + + setBaseUrl?: (baseUrl: string) => void + setUseDiscordPresence?: (connect: boolean) => void + setDiscordPresence?: (status?: string, details?: string) => void +} + +export interface JobContext { + activeJobs: Record + + addJob(job: JobUpdate): void + updateJob(job: JobUpdate): void + removeJob(runnerId: string): void +} +export const ActiveJobContext = createContext(null) + +export const useAppProps = () => useContext(AppPropsContext) +export const useJobContext = () => useContext(ActiveJobContext) +export const useClientContext = () => useContext(StumpClientContext) diff --git a/packages/client/src/hooks/index.ts b/packages/client/src/hooks/index.ts new file mode 100644 index 000000000..82f0f2158 --- /dev/null +++ b/packages/client/src/hooks/index.ts @@ -0,0 +1,4 @@ +export * from './useCoreEvent' +export * from './useLayoutMode' +export * from './useStumpSse' +export * from './useStumpWs' diff --git a/packages/client/src/hooks/useCoreEvent.ts b/packages/client/src/hooks/useCoreEvent.ts new file mode 100644 index 000000000..3045759d9 --- /dev/null +++ b/packages/client/src/hooks/useCoreEvent.ts @@ -0,0 +1,70 @@ +import type { CoreEvent } from '@stump/types' + +import { useJobContext } from '../context' +import { core_event_triggers, invalidateQueries } from '../invalidate' +import { useStumpSse } from './useStumpSse' + +interface UseCoreEventHandlerParams { + onJobComplete?: (jobId: string) => void + onJobFailed?: (err: { runner_id: string; message: string }) => void +} + +export function useCoreEventHandler({ + onJobComplete, + onJobFailed, +}: UseCoreEventHandlerParams = {}) { + const context = useJobContext() + + if (!context) { + throw new Error('useCoreEventHandler must be used within a JobContext') + } + + const { addJob, updateJob, removeJob } = context + + function handleCoreEvent(event: CoreEvent) { + const { key, data } = event + + switch (key) { + case 'JobStarted': + addJob(data) + break + case 'JobProgress': + // FIXME: Testing with a test library containing over 10k cbz files, there are so + // many updates that around 2000k it just dies. I have implemented a check to + // in this store function where if the task_count is greater than 1000, it will + // only update the store every 50 tasks. This is a temporary fix. The UI is still pretty + // slow when this happens, but is usable. A better solution needs to be found. + updateJob(data) + break + case 'JobComplete': + setTimeout(() => { + removeJob(data) + invalidateQueries({ keys: core_event_triggers[key].keys }) + onJobComplete?.(data) + }, 750) + break + case 'JobFailed': + onJobFailed?.(data) + removeJob(data.runner_id) + invalidateQueries({ keys: core_event_triggers[key].keys }) + + break + case 'CreatedMedia': + case 'CreatedMediaBatch': + case 'CreatedSeries': + // I set a timeout here to give the backend a little time to analyze at least + // one of the books in a new series before triggering a refetch. This is to + // prevent the series/media cards from being displayed before there is an image ready. + setTimeout(() => { + invalidateQueries({ keys: core_event_triggers[key].keys }) + }, 250) + break + default: + console.warn('Unknown JobEvent', data) + console.debug(data) + break + } + } + + useStumpSse({ onEvent: handleCoreEvent }) +} diff --git a/packages/client/src/hooks/useCounter.ts b/packages/client/src/hooks/useCounter.ts new file mode 100644 index 000000000..9bcaefd0c --- /dev/null +++ b/packages/client/src/hooks/useCounter.ts @@ -0,0 +1,17 @@ +import { useMemo, useState } from 'react' + +export function useCounter(initialValue = 0) { + const [count, setCount] = useState(initialValue) + + const actions = useMemo( + () => ({ + decrement: () => setCount(count - 1), + increment: () => setCount(count + 1), + reset: () => setCount(initialValue), + set: (value: number) => setCount(value), + }), + [initialValue, count], + ) + + return [count, actions] as const +} diff --git a/common/client/src/hooks/useLayoutMode.ts b/packages/client/src/hooks/useLayoutMode.ts similarity index 53% rename from common/client/src/hooks/useLayoutMode.ts rename to packages/client/src/hooks/useLayoutMode.ts index ef845e378..a1158b419 100644 --- a/common/client/src/hooks/useLayoutMode.ts +++ b/packages/client/src/hooks/useLayoutMode.ts @@ -1,50 +1,55 @@ -import type { LayoutMode } from '../types'; -import { useMemo } from 'react'; -import { useUserPreferences } from '../queries'; -import { useUserStore } from '../stores'; +import type { LayoutMode } from '@stump/types' +import { useMemo } from 'react' -export type LayoutEntity = 'LIBRARY' | 'SERIES'; +import { useUserPreferences } from '../queries' +import { useUserStore } from '../stores' -const DEFAULT_LAYOUT_MODE: LayoutMode = 'GRID'; +export type LayoutEntity = 'LIBRARY' | 'SERIES' + +const DEFAULT_LAYOUT_MODE: LayoutMode = 'GRID' // TODO: add callbacks for error? export function useLayoutMode(entity: LayoutEntity) { - const { user, userPreferences, setUserPreferences } = useUserStore(); + const { user, userPreferences, setUserPreferences } = useUserStore((state) => ({ + setUserPreferences: state.setUserPreferences, + user: state.user, + userPreferences: state.userPreferences, + })) const { updateUserPreferences } = useUserPreferences(user?.id, { - onUpdated: setUserPreferences, enableFetchPreferences: !user, - }); + onUpdated: setUserPreferences, + }) async function updateLayoutMode(mode: LayoutMode, onError?: (err: unknown) => void) { if (userPreferences) { - let key = entity === 'LIBRARY' ? 'library_layout_mode' : 'series_layout_mode'; + const key = entity === 'LIBRARY' ? 'library_layout_mode' : 'series_layout_mode' updateUserPreferences({ ...userPreferences, [key]: mode, }).catch((err) => { - onError?.(err); - }); + onError?.(err) + }) } } // TODO: update function for changing layout mode const layoutMode = useMemo(() => { if (!userPreferences) { - return DEFAULT_LAYOUT_MODE; + return DEFAULT_LAYOUT_MODE } switch (entity) { case 'LIBRARY': - return userPreferences.library_layout_mode || DEFAULT_LAYOUT_MODE; + return userPreferences.library_layout_mode || DEFAULT_LAYOUT_MODE case 'SERIES': - return userPreferences.series_layout_mode || DEFAULT_LAYOUT_MODE; + return userPreferences.series_layout_mode || DEFAULT_LAYOUT_MODE default: - console.warn('Unknown layout entity', entity); - return DEFAULT_LAYOUT_MODE; + console.warn('Unknown layout entity', entity) + return DEFAULT_LAYOUT_MODE } - }, [entity, userPreferences]); + }, [entity, userPreferences]) - return { layoutMode, updateLayoutMode }; + return { layoutMode, updateLayoutMode } } diff --git a/packages/client/src/hooks/useStumpSse.ts b/packages/client/src/hooks/useStumpSse.ts new file mode 100644 index 000000000..62c8bf0c3 --- /dev/null +++ b/packages/client/src/hooks/useStumpSse.ts @@ -0,0 +1,111 @@ +import { API } from '@stump/api' +import type { CoreEvent } from '@stump/types' +import { useEffect, useMemo } from 'react' + +import { useStumpStore } from '../stores' + +interface SseOptions { + onOpen?: (event: Event) => void + onClose?: (event?: Event) => void + onMessage?: (event: MessageEvent) => void + onError?: (event: Event) => void +} + +let sse: EventSource + +// this is a little meh +function useSse(url: string, sseOptions: SseOptions = {}) { + const { onOpen, onClose, onMessage } = sseOptions + + function initEventSource() { + sse = new EventSource(url, { + withCredentials: true, + }) + + sse.onmessage = (e) => { + // console.log('EVENT', e); + onMessage?.(e) + } + + sse.onerror = (event) => { + console.error('EventSource error event:', event) + + sse?.close() + + setTimeout(() => { + initEventSource() + + if (sse?.readyState !== EventSource.OPEN) { + onClose?.(event) + return + } + }, 5000) + } + + sse.onopen = (e) => { + onOpen?.(e) + } + } + + useEffect( + () => { + initEventSource() + + return () => { + sse?.close() + } + }, + + // eslint-disable-next-line react-hooks/exhaustive-deps + [url], + ) + + return { + readyState: sse?.readyState, + } +} + +interface Props { + onEvent(event: CoreEvent): void +} + +export function useStumpSse({ onEvent }: Props) { + const URI = API?.getUri() + + const setConnected = useStumpStore(({ setConnected }) => setConnected) + + const eventSourceUrl = useMemo(() => { + let url = URI + // remove /api(/) from end of url + url = url.replace(/\/api(\/v\d)?$/, '') + + return `${url}/sse` + }, [URI]) + + function handleMessage(e: MessageEvent) { + if ('data' in e && typeof e.data === 'string') { + try { + const event = JSON.parse(e.data) + onEvent(event) + } catch (err) { + console.error(err) + } + } else { + console.warn('Unrecognized message event:', e) + } + } + + const { readyState } = useSse(eventSourceUrl, { + onClose: () => { + setConnected(false) + }, + onMessage: handleMessage, + onOpen: () => { + setConnected(true) + }, + }) + + return { + readyState, + } +} diff --git a/packages/client/src/hooks/useStumpWs.ts b/packages/client/src/hooks/useStumpWs.ts new file mode 100644 index 000000000..a5417bacc --- /dev/null +++ b/packages/client/src/hooks/useStumpWs.ts @@ -0,0 +1,57 @@ +import { API } from '@stump/api' +import type { CoreEvent } from '@stump/types' +import { useMemo } from 'react' +import useWebSocket, { ReadyState } from 'react-use-websocket' + +import { useStumpStore } from '../stores' + +interface Props { + onEvent(event: CoreEvent): void +} + +export function useStumpWs({ onEvent }: Props) { + const URI = API?.getUri() + const { setConnected } = useStumpStore(({ setConnected }) => ({ setConnected })) + + const socketUrl = useMemo(() => { + let url = URI + // remove http(s):// from url, and replace with ws(s):// + url = url.replace(/^http(s?):\/\//, 'ws$1://') + // remove /api(/) from end of url + url = url.replace(/\/api\/?$/, '') + + return `${url}/ws` + }, [URI]) + + function handleWsMessage(e: MessageEvent) { + if ('data' in e && typeof e.data === 'string') { + try { + const event = JSON.parse(e.data) + onEvent(event) + } catch (err) { + console.error(err) + } + } else { + console.warn('Unrecognized message event:', e) + } + } + + function handleOpen() { + setConnected(true) + } + + function handleClose() { + setConnected(false) + } + + const { readyState } = useWebSocket(socketUrl, { + onClose: handleClose, + onMessage: handleWsMessage, + onOpen: handleOpen, + }) + + return { readyState } +} + +// Re-export the ready state enum so consumer of client can use it if needed +export { ReadyState } diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts new file mode 100644 index 000000000..54ffdaf47 --- /dev/null +++ b/packages/client/src/index.ts @@ -0,0 +1,5 @@ +export * from './client' +export * from './context' +export * from './hooks' +export * from './Provider' +export * from './stores' diff --git a/packages/client/src/invalidate.ts b/packages/client/src/invalidate.ts new file mode 100644 index 000000000..1e6233d97 --- /dev/null +++ b/packages/client/src/invalidate.ts @@ -0,0 +1,75 @@ +import { CoreEvent } from '@stump/types' +import { QueryKey } from '@tanstack/react-query' + +import { queryClient } from './index' +import { QUERY_KEYS } from './query_keys' + +type CoreEventTrigger = CoreEvent['key'] +type InvalidateFnArgs = { + exact?: boolean +} & ( + | { + keys: string[] + } + | { + queryKey: QueryKey + } +) + +const { library, series, media, job } = QUERY_KEYS + +// TODO: this is still rather verbose, but it's a start +export const core_event_triggers = { + CreateEntityFailed: { + keys: [job.get], + }, + CreatedMedia: { + keys: [library.get_by_id, library.stats, series.get, media.recently_added], + }, + CreatedMediaBatch: { + keys: [library.get_by_id, library.stats, series.get, media.recently_added], + }, + CreatedSeries: { + keys: [library.get_by_id, library.stats, series.get, media.recently_added], + }, + CreatedSeriesBatch: { + keys: [library.get_by_id, library.stats, series.get, media.recently_added], + }, + JobComplete: { + keys: [ + library.get_by_id, + library.stats, + job.get, + series.get, + series.recently_added, + media.recently_added, + ], + }, + JobFailed: { + keys: [job.get], + }, + JobProgress: { + keys: [job.get], + }, + JobStarted: { + keys: [job.get], + }, +} satisfies Record + +export async function invalidateQueries({ exact, ...args }: InvalidateFnArgs) { + if ('keys' in args) { + const predicate = (query: { queryKey: QueryKey }, compare: string) => { + const key = (query.queryKey[0] as string) || '' + return exact ? key === compare : key.startsWith(compare) + } + return Promise.all( + args.keys.map((key) => + queryClient.invalidateQueries({ + predicate: (query) => predicate(query, key), + }), + ), + ) + } else { + return queryClient.invalidateQueries({ exact, queryKey: args.queryKey }) + } +} diff --git a/packages/client/src/queries/auth.ts b/packages/client/src/queries/auth.ts new file mode 100644 index 000000000..c8118fa9a --- /dev/null +++ b/packages/client/src/queries/auth.ts @@ -0,0 +1,74 @@ +import { checkIsClaimed, login, me, register } from '@stump/api' +import type { User } from '@stump/types' +import { useEffect, useState } from 'react' + +import { queryClient, useMutation, useQuery } from '../client' +import { ClientQueryParams, QueryCallbacks } from '.' + +export interface AuthQueryOptions extends QueryCallbacks { + disabled?: boolean + enabled?: boolean +} + +export function useAuthQuery(options: AuthQueryOptions = {}) { + const { data, error, isLoading, isFetching, isRefetching } = useQuery(['getViewer'], me, { + enabled: options?.enabled, + onError(err) { + options.onError?.(err) + }, + onSuccess(res) { + options.onSuccess?.(res.data) + }, + useErrorBoundary: false, + }) + + return { + error, + isLoading: isLoading || isFetching || isRefetching, + user: data, + } +} + +export function useLoginOrRegister({ onSuccess, onError }: ClientQueryParams) { + const [isClaimed, setIsClaimed] = useState(true) + + const { data: claimCheck, isLoading: isCheckingClaimed } = useQuery( + ['checkIsClaimed'], + checkIsClaimed, + ) + + useEffect(() => { + if (claimCheck?.data && !claimCheck.data.is_claimed) { + setIsClaimed(false) + } + }, [claimCheck]) + + const { isLoading: isLoggingIn, mutateAsync: loginUser } = useMutation(['loginUser'], login, { + onError: (err) => { + onError?.(err) + }, + onSuccess: (res) => { + if (!res.data) { + onError?.(res) + } else { + queryClient.invalidateQueries(['getLibraries']) + + onSuccess?.(res.data) + } + }, + }) + + const { isLoading: isRegistering, mutateAsync: registerUser } = useMutation( + ['registerUser'], + register, + ) + + return { + isCheckingClaimed, + isClaimed, + isLoggingIn, + isRegistering, + loginUser, + registerUser, + } +} diff --git a/common/client/src/queries/epub.ts b/packages/client/src/queries/epub.ts similarity index 52% rename from common/client/src/queries/epub.ts rename to packages/client/src/queries/epub.ts index fae261864..29a32605b 100644 --- a/common/client/src/queries/epub.ts +++ b/packages/client/src/queries/epub.ts @@ -1,16 +1,16 @@ -import type { Epub, EpubContent } from '../types'; -import { useQuery } from '@tanstack/react-query'; -import { useMemo, useState } from 'react'; -import { getEpubBaseUrl, getEpubById } from '../api/epub'; -import { StumpQueryContext } from '../context'; +import { getEpubBaseUrl, getEpubById } from '@stump/api' +import type { Epub, EpubContent } from '@stump/types' +import { useMemo, useState } from 'react' + +import { useQuery } from '../client' export interface EpubOptions { // loc is the epubcfi, comes from the query param ?loc=epubcfi(..) - loc: string | null; + loc: string | null } export interface EpubActions { - currentResource(): EpubContent | undefined; + currentResource(): EpubContent | undefined // hasNext(): boolean; // hasPrev(): boolean; // next(): void; @@ -18,10 +18,10 @@ export interface EpubActions { } export interface UseEpubReturn { - epub: Epub; - isFetchingBook: boolean; - actions: EpubActions; - correctHtmlUrls: (html: string) => string; + epub: Epub + isFetchingBook: boolean + actions: EpubActions + correctHtmlUrls: (html: string) => string } // TODO: I need to decide how to navigate epub streaming. I can go the cheap route and @@ -30,70 +30,83 @@ export interface UseEpubReturn { // - client-side might be easier, but I'd rather not have heavier client-side computations for *large* epub files // I can use epubcfi to navigate, but that makes me want to throw up lol i mean just look at this syntax: // epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/3:10) -> wtf is that lmao -export function useEpub(id: string, options?: EpubOptions) { - const [chapter, setChapter] = useState(2); - const { isLoading: isFetchingBook, data: epub } = useQuery(['getEpubById', id], { - queryFn: () => getEpubById(id).then((res) => res.data), - context: StumpQueryContext, - }); +// FIXME: use options + +export function useEpub(id: string, _options?: EpubOptions, enabled?: boolean) { + const [chapter] = useState(2) + + const { isLoading: isFetchingBook, data: epub } = useQuery( + ['getEpubById', id], + () => getEpubById(id).then((res) => res.data), + { + enabled, + }, + ) const actions = useMemo( () => ({ currentResource() { - return epub?.toc.find((item) => item.play_order === chapter); + return epub?.toc.find((item) => item.play_order === chapter) + }, + hasNext() { + // TODO: make me + }, + hasPrevious() { + // TODO: make me + }, + next() { + // TODO: make me + }, + previous() { + // TODO: make me }, - hasNext() {}, - hasPrevious() {}, - next() {}, - previous() {}, }), - [epub], - ); + [epub, chapter], + ) function correctHtmlUrls(html: string): string { // replace all src attributes with `{epubBaseURl}/{root}/{src}` // replace all href attributes with `{epubBaseURl}/{root}/{href}` - let corrected = html; + let corrected = html - const invalidSources = corrected.match(/src="[^"]+"/g); + const invalidSources = corrected.match(/src="[^"]+"/g) invalidSources?.forEach((entry) => { - const src = entry.replace('src="', `src="${getEpubBaseUrl(id)}/${epub?.root_base ?? ''}/`); - corrected = corrected.replace(entry, src); - }); + const src = entry.replace('src="', `src="${getEpubBaseUrl(id)}/${epub?.root_base ?? ''}/`) + corrected = corrected.replace(entry, src) + }) - const invlalidHrefs = corrected.match(/href="[^"]+"/g); + const invlalidHrefs = corrected.match(/href="[^"]+"/g) invlalidHrefs?.forEach((entry) => { - const href = entry.replace('href="', `href="${getEpubBaseUrl(id)}/${epub?.root_base ?? ''}/`); - corrected = corrected.replace(entry, href); - }); + const href = entry.replace('href="', `href="${getEpubBaseUrl(id)}/${epub?.root_base ?? ''}/`) + corrected = corrected.replace(entry, href) + }) - return corrected; + return corrected } return { - isFetchingBook, - epub, actions, correctHtmlUrls, - } as UseEpubReturn; + epub, + isFetchingBook, + } as UseEpubReturn } -export function useEpubLazy(id: string, options?: EpubOptions) { +// FIXME: use options +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useEpubLazy(id: string, _options?: EpubOptions) { const { data: epub, isLoading, isRefetching, isFetching, - } = useQuery(['getEpubById', id], { - queryFn: () => getEpubById(id).then((res) => res.data), - context: StumpQueryContext, - }); + } = useQuery(['getEpubById', id], () => getEpubById(id).then((res) => res.data)) return { - isLoading: isLoading || isRefetching || isFetching, epub, - }; + isLoading: isLoading || isRefetching || isFetching, + } } diff --git a/common/client/src/queries/filesystem.ts b/packages/client/src/queries/filesystem.ts similarity index 58% rename from common/client/src/queries/filesystem.ts rename to packages/client/src/queries/filesystem.ts index a9aaab19f..02fcc6852 100644 --- a/common/client/src/queries/filesystem.ts +++ b/packages/client/src/queries/filesystem.ts @@ -1,16 +1,14 @@ -import type { DirectoryListing } from '../types'; -import { AxiosError } from 'axios'; -import { useMemo, useState } from 'react'; +import { listDirectory } from '@stump/api' +import type { DirectoryListing } from '@stump/types' +import { AxiosError } from 'axios' +import { useMemo, useState } from 'react' -import { useQuery } from '@tanstack/react-query'; - -import { listDirectory } from '../api/filesystem'; -import { StumpQueryContext } from '../context'; +import { useQuery } from '../client' export interface DirectoryListingQueryParams { - enabled: boolean; - startingPath?: string; - page?: number; + enabled: boolean + startingPath?: string + page?: number } export function useDirectoryListing({ @@ -18,61 +16,61 @@ export function useDirectoryListing({ startingPath, page = 1, }: DirectoryListingQueryParams) { - const [path, setPath] = useState(startingPath || null); + const [path, setPath] = useState(startingPath || null) - const [directoryListing, setDirectoryListing] = useState(); + const [directoryListing, setDirectoryListing] = useState() const { isLoading, error } = useQuery( ['listDirectory', path, page], - () => listDirectory({ path, page }), + () => listDirectory({ page, path }), { // Do not run query until `enabled` aka modal is opened. enabled, - suspense: false, + keepPreviousData: true, onSuccess(res) { - setDirectoryListing(res.data.data); + setDirectoryListing(res.data.data) }, - context: StumpQueryContext, + suspense: false, }, - ); + ) const actions = useMemo( () => ({ goBack() { if (directoryListing?.parent) { - setPath(directoryListing.parent); + setPath(directoryListing.parent) } }, onSelect(directory: string) { - setPath(directory); + setPath(directory) }, }), [directoryListing], - ); + ) const errorMessage = useMemo(() => { - let err = error as AxiosError; + const err = error as AxiosError if (err?.response?.data) { if (err.response.status === 404) { - return 'Directory not found'; + return 'Directory not found' } else { - return err.response.data as string; + return err.response.data as string } } - return null; - }, [error]); + return null + }, [error]) return { - isLoading, + directories: directoryListing?.files.filter((f) => f.is_directory) ?? [], + entries: directoryListing?.files ?? [], error, errorMessage, - path, - entries: directoryListing?.files ?? [], + isLoading, parent: directoryListing?.parent, - directories: directoryListing?.files.filter((f) => f.is_directory) ?? [], + path, // files: directoryListing?.files.filter((f) => !f.isDirectory) ?? [], ...actions, - }; + } } diff --git a/packages/client/src/queries/index.ts b/packages/client/src/queries/index.ts new file mode 100644 index 000000000..1a41e50d8 --- /dev/null +++ b/packages/client/src/queries/index.ts @@ -0,0 +1,38 @@ +import type { ApiResult } from '@stump/api' + +export * from './auth' +export * from './epub' +export * from './filesystem' +export * from './job' +export * from './library' +export * from './media' +export * from './series' +export * from './server' +export * from './tag' +export * from './user' + +export interface QueryCallbacks { + onSuccess?: (data?: T | null) => void + onError?: (data: unknown) => void +} + +export interface CreateCallbacks { + onCreated?: (data: T) => void + onCreateFailed?: (res: ApiResult) => void + onError?: (data: unknown) => void +} + +export interface UpdateCallbacks { + onUpdated?: (data: T) => void + onUpdateFailed?: (res: ApiResult) => void + onError?: (data: unknown) => void +} + +export interface DeleteCallbacks { + onDeleted?: (data: T) => void + onDeleteFailed?: (res: ApiResult) => void + onError?: (data: unknown) => void +} + +export type MutationCallbacks = CreateCallbacks & UpdateCallbacks & DeleteCallbacks +export type ClientQueryParams = QueryCallbacks & MutationCallbacks diff --git a/packages/client/src/queries/job.ts b/packages/client/src/queries/job.ts new file mode 100644 index 000000000..245656b07 --- /dev/null +++ b/packages/client/src/queries/job.ts @@ -0,0 +1,19 @@ +import { getJobs } from '@stump/api' +import type { JobReport } from '@stump/types' + +import { useQuery } from '../client' +import { QueryCallbacks } from '.' + +export function useJobReport({ onSuccess, onError }: QueryCallbacks = {}) { + const { + data: jobReports, + isLoading, + isRefetching, + isFetching, + } = useQuery(['getJobReports'], () => getJobs().then((res) => res.data), { + onError, + onSuccess, + }) + + return { isLoading: isLoading || isRefetching || isFetching, jobReports } +} diff --git a/packages/client/src/queries/library.ts b/packages/client/src/queries/library.ts new file mode 100644 index 000000000..1c54eed01 --- /dev/null +++ b/packages/client/src/queries/library.ts @@ -0,0 +1,169 @@ +import { + createLibrary, + deleteLibrary, + editLibrary, + getLibraries, + getLibrariesStats, + getLibraryById, + getLibrarySeries, + scanLibary, +} from '@stump/api' +import type { Library, PageInfo } from '@stump/types' +import { AxiosError } from 'axios' +import { useMemo } from 'react' + +import { useMutation, useQuery } from '../client' +import { invalidateQueries } from '../invalidate' +import { QUERY_KEYS } from '../query_keys' +import { useQueryParamStore } from '../stores' +import type { ClientQueryParams, QueryCallbacks } from '.' + +const LIBRARY_KEYS = QUERY_KEYS.library + +export const refreshUseLibrary = (id: string) => + invalidateQueries({ exact: true, queryKey: [LIBRARY_KEYS.get_by_id, id] }) + +export function useLibrary(id: string, { onError }: QueryCallbacks = {}) { + const { isLoading, data: library } = useQuery( + [LIBRARY_KEYS.get_by_id, id], + () => getLibraryById(id).then((res) => res.data), + { + onError, + }, + ) + + return { isLoading, library } +} + +export interface UseLibrariesReturn { + libraries: Library[] + pageData?: PageInfo +} + +export function useLibraries() { + const { data, ...rest } = useQuery([LIBRARY_KEYS.get], getLibraries, { + // Send all non-401 errors to the error page + useErrorBoundary: (err: AxiosError) => !err || (err.response?.status ?? 500) !== 401, + }) + + const { libraries, pageData } = useMemo(() => { + if (data?.data) { + return { + libraries: data.data.data, + pageData: data.data._page, + } + } + + return { libraries: [] } + }, [data]) + + return { + libraries, + pageData, + ...rest, + } +} + +export function useLibrarySeries(libraryId: string, page = 1) { + const { getQueryString, ...paramsStore } = useQueryParamStore((state) => state) + + const { isLoading, isFetching, isPreviousData, data } = useQuery( + [LIBRARY_KEYS.series, page, libraryId, paramsStore], + () => + getLibrarySeries(libraryId, page, getQueryString()).then(({ data }) => ({ + pageData: data._page, + series: data.data, + })), + { + keepPreviousData: true, + }, + ) + + const { series, pageData } = data ?? {} + + return { isFetching, isLoading, isPreviousData, pageData, series } +} + +export function useLibraryStats() { + const { + data: libraryStats, + isLoading, + isRefetching, + isFetching, + } = useQuery([LIBRARY_KEYS.stats], () => getLibrariesStats().then((data) => data.data), {}) + + return { isLoading: isLoading || isRefetching || isFetching, libraryStats } +} + +export function useScanLibrary({ onError }: ClientQueryParams = {}) { + const { mutate: scan, mutateAsync: scanAsync } = useMutation([LIBRARY_KEYS.scan], scanLibary, { + onError, + }) + + return { scan, scanAsync } +} + +export function useLibraryMutation({ + onCreated, + onUpdated, + onDeleted, + onCreateFailed, + onUpdateFailed, + onError, +}: ClientQueryParams = {}) { + const { isLoading: createIsLoading, mutateAsync: createLibraryAsync } = useMutation( + ['createLibrary'], + createLibrary, + { + onError, + onSuccess: (res) => { + if (!res.data) { + onCreateFailed?.(res) + } else { + invalidateQueries({ + keys: [LIBRARY_KEYS.get, LIBRARY_KEYS.stats, QUERY_KEYS.job.get], + }) + onCreated?.(res.data) + // onClose(); + } + }, + }, + ) + + const { isLoading: editIsLoading, mutateAsync: editLibraryAsync } = useMutation( + ['editLibrary'], + editLibrary, + { + onError, + onSuccess: (res) => { + if (!res.data) { + console.warn('Update failed:', res) + onUpdateFailed?.(res) + } else { + invalidateQueries({ + keys: [LIBRARY_KEYS.get, LIBRARY_KEYS.stats, QUERY_KEYS.job.get], + }) + onUpdated?.(res.data) + } + }, + }, + ) + + const { mutateAsync: deleteLibraryAsync } = useMutation(['deleteLibrary'], deleteLibrary, { + async onSuccess(res) { + await invalidateQueries({ + keys: [LIBRARY_KEYS.get, LIBRARY_KEYS.stats], + }) + + onDeleted?.(res.data) + }, + }) + + return { + createIsLoading, + createLibraryAsync, + deleteLibraryAsync, + editIsLoading, + editLibraryAsync, + } +} diff --git a/packages/client/src/queries/media.ts b/packages/client/src/queries/media.ts new file mode 100644 index 000000000..b08dbf075 --- /dev/null +++ b/packages/client/src/queries/media.ts @@ -0,0 +1,85 @@ +import { + getInProgressMedia, + getMedia, + getMediaById, + getRecentlyAddedMedia, + updateMediaProgress, +} from '@stump/api' +import type { Media, ReadProgress } from '@stump/types' +import { useMemo } from 'react' + +import { QueryOptions, useCursorQuery, useInfinitePagedQuery, useMutation } from '../client' +import { queryClient, useQuery } from '../client' +import { QUERY_KEYS } from '../query_keys' + +const MEDIA_KEYS = QUERY_KEYS.media + +export const prefetchMedia = async (id: string) => { + await queryClient.prefetchQuery([MEDIA_KEYS.get_by_id, id], () => getMediaById(id), { + staleTime: 10 * 1000, + }) +} + +export function useMediaById(id: string, { onError }: QueryOptions = {}) { + const { data, isLoading, isFetching, isRefetching } = useQuery( + [MEDIA_KEYS.get_by_id, id], + () => getMediaById(id), + { + keepPreviousData: false, + onError(err) { + console.error(err) + onError?.(err) + }, + }, + ) + + return { isLoading: isLoading || isFetching || isRefetching, media: data?.data } +} + +/** Hook for fetching media after a cursor, within a series */ +export function useMediaCursor(afterId: string, seriesId: string) { + const searchParams = useMemo(() => { + return new URLSearchParams({ cursor: afterId, series_id: seriesId }) + }, [afterId, seriesId]) + const { data: media, ...rest } = useCursorQuery( + afterId, + [MEDIA_KEYS.get_with_cursor, afterId], + () => getMedia(searchParams), + ) + + return { media, ...rest } +} + +export function useMediaMutation(id: string, options: QueryOptions = {}) { + const { + mutate: updateReadProgress, + mutateAsync: updateReadProgressAsync, + isLoading, + } = useMutation(['updateReadProgress'], (page: number) => updateMediaProgress(id, page), { + // context: StumpQueryContext, + onError(err) { + options.onError?.(err) + }, + onSuccess(data) { + options.onSuccess?.(data) + }, + }) + + return { isLoading, updateReadProgress, updateReadProgressAsync } +} + +export function useRecentlyAddedMedia() { + return useInfinitePagedQuery( + [MEDIA_KEYS.recently_added], + getRecentlyAddedMedia, + new URLSearchParams('page_size=10'), + ) +} + +export function useContinueReading() { + return useInfinitePagedQuery( + [MEDIA_KEYS.in_progress], + getInProgressMedia, + new URLSearchParams('page_size=10'), + ) +} diff --git a/packages/client/src/queries/series.ts b/packages/client/src/queries/series.ts new file mode 100644 index 000000000..2236b1ad0 --- /dev/null +++ b/packages/client/src/queries/series.ts @@ -0,0 +1,103 @@ +import { getNextInSeries, getRecentlyAddedSeries, getSeriesById, getSeriesMedia } from '@stump/api' +import type { Media, Series } from '@stump/types' +import { Axios, isAxiosError } from 'axios' + +import { queryClient, useInfinitePagedQuery, useQuery } from '../client' +import { QUERY_KEYS } from '../query_keys' +import { useQueryParamStore } from '../stores' +import { QueryCallbacks } from '.' + +const SERIES_KEYS = QUERY_KEYS.series + +export const prefetchSeries = async (id: string) => { + await queryClient.prefetchQuery([SERIES_KEYS.get_by_id, id], () => getSeriesById(id), { + staleTime: 10 * 1000, + }) +} + +export function useSeries(id: string, options: QueryCallbacks = {}) { + const { + isLoading, + isFetching, + isRefetching, + data: series, + } = useQuery( + [SERIES_KEYS.get_by_id, id], + () => getSeriesById(id).then(({ data }) => data), + options, + ) + + return { isLoading: isLoading || isFetching || isRefetching, series } +} + +export function useSeriesMedia(seriesId: string, page = 1) { + const { getQueryString, ...paramsStore } = useQueryParamStore((state) => state) + + const { isLoading, isFetching, isRefetching, isPreviousData, data } = useQuery( + [SERIES_KEYS.media, page, seriesId, paramsStore], + () => + getSeriesMedia(seriesId, page, getQueryString()).then(({ data }) => ({ + media: data.data, + pageData: data._page, + })), + { + // context: StumpQueryContext, + keepPreviousData: true, + }, + ) + + const { media, pageData } = data ?? {} + + return { + isLoading: isLoading || isFetching || isRefetching, + isPreviousData, + media, + pageData, + } +} + +export function useRecentlyAddedSeries() { + return useInfinitePagedQuery( + [SERIES_KEYS.recently_added], + getRecentlyAddedSeries, + new URLSearchParams('page_size=50'), + ) +} + +// export function useInfinite() { +// const { +// status, +// data: pageData, +// error, +// isFetching, +// isFetchingNextPage, +// fetchNextPage, +// hasNextPage, +// } = useInfiniteQuery( +// ['getRecentlyAddedSeries'], +// (ctx) => getRecentlyAddedSeries(ctx.pageParam, new URLSearchParams('page_size=50')), +// { +// getNextPageParam: (_lastGroup, groups) => groups.length, +// }, +// ); + +// const data = pageData ? pageData.pages.flatMap((res) => res.data.data) : []; + +// return { +// data, +// }; +// } + +export function useUpNextInSeries(id: string, options: QueryCallbacks = {}) { + const { + data: media, + isLoading, + isFetching, + isRefetching, + } = useQuery([SERIES_KEYS.up_next, id], () => getNextInSeries(id).then((res) => res.data), { + ...options, + useErrorBoundary: false, + }) + + return { isLoading: isLoading || isFetching || isRefetching, media } +} diff --git a/packages/client/src/queries/server.ts b/packages/client/src/queries/server.ts new file mode 100644 index 000000000..23eb6114a --- /dev/null +++ b/packages/client/src/queries/server.ts @@ -0,0 +1,17 @@ +import { getStumpVersion } from '@stump/api' + +import { useQuery } from '../client' + +export function useStumpVersion() { + const { data: version } = useQuery( + ['stumpVersion'], + () => getStumpVersion().then((res) => res.data), + { + onError(err) { + console.error('Failed to fetch Stump API version:', err) + }, + }, + ) + + return version +} diff --git a/packages/client/src/queries/tag.ts b/packages/client/src/queries/tag.ts new file mode 100644 index 000000000..14c7a05b3 --- /dev/null +++ b/packages/client/src/queries/tag.ts @@ -0,0 +1,70 @@ +import { ApiResult, createTags, getAllTags } from '@stump/api' +import type { Tag } from '@stump/types' +import { AxiosError } from 'axios' +import { useMemo } from 'react' + +import { queryClient, useMutation, useQuery } from '../client' + +export interface UseTagsConfig { + onQuerySuccess?: (res: ApiResult) => void + onQueryError?: (err: AxiosError) => void + onCreateSuccess?: (res: ApiResult) => void + onCreateError?: (err: AxiosError) => void +} + +export interface TagOption { + label: string + value: string +} + +export function useTags({ + onQuerySuccess, + onQueryError, + onCreateSuccess, + onCreateError, +}: UseTagsConfig = {}) { + const { data, isLoading, refetch } = useQuery(['getAllTags'], getAllTags, { + onError: onQueryError, + onSuccess: onQuerySuccess, + suspense: false, + }) + + const { + mutate, + mutateAsync, + isLoading: isCreating, + } = useMutation(['createTags'], createTags, { + onError: onCreateError, + onSuccess(res) { + onCreateSuccess?.(res) + + queryClient.refetchQueries(['getAllTags']) + }, + }) + + const { tags, options } = useMemo(() => { + if (data && data.data) { + const tagOptions = data.data?.map( + (tag) => + ({ + label: tag.name, + value: tag.name, + } as TagOption), + ) + + return { options: tagOptions, tags: data.data } + } + + return { options: [], tags: [] } + }, [data]) + + return { + createTags: mutate, + createTagsAsync: mutateAsync, + isCreating, + isLoading, + options, + refetch, + tags, + } +} diff --git a/common/client/src/queries/user.ts b/packages/client/src/queries/user.ts similarity index 69% rename from common/client/src/queries/user.ts rename to packages/client/src/queries/user.ts index 458bebf98..536556ed3 100644 --- a/common/client/src/queries/user.ts +++ b/packages/client/src/queries/user.ts @@ -1,11 +1,11 @@ -import type { UserPreferences } from '../types'; -import { useMutation, useQuery } from '@tanstack/react-query'; -import { ClientQueryParams } from '.'; -import { getUserPreferences, updateUserPreferences as updateUserPreferencesFn } from '../api/user'; -import { StumpQueryContext } from '../context'; +import { getUserPreferences, updateUserPreferences as updateUserPreferencesFn } from '@stump/api' +import type { UserPreferences } from '@stump/types' + +import { useMutation, useQuery } from '../client' +import { ClientQueryParams } from '.' interface UseUserPreferencesParams extends ClientQueryParams { - enableFetchPreferences?: boolean; + enableFetchPreferences?: boolean } export function useUserPreferences( @@ -19,24 +19,22 @@ export function useUserPreferences( isRefetching, } = useQuery(['getUserPreferences', id], () => getUserPreferences(id!).then((res) => res.data), { enabled: enableFetchPreferences && !!id, - context: StumpQueryContext, - }); + }) const { mutateAsync: updateUserPreferences, isLoading: isUpdating } = useMutation( ['updateUserPreferences', id], (preferences: UserPreferences) => updateUserPreferencesFn(id!, preferences), { onSuccess(res) { - onUpdated?.(res.data); + onUpdated?.(res.data) }, - context: StumpQueryContext, }, - ); + ) return { isLoadingPreferences: isLoading || isFetching || isRefetching, - userPreferences, - updateUserPreferences, isUpdating, - }; + updateUserPreferences, + userPreferences, + } } diff --git a/packages/client/src/query_keys.ts b/packages/client/src/query_keys.ts new file mode 100644 index 000000000..b68fd03d4 --- /dev/null +++ b/packages/client/src/query_keys.ts @@ -0,0 +1,34 @@ +export const QUERY_KEYS = { + job: { + _group_prefix: 'job', + get: 'job.get', + get_by_id: 'job.get_by_id', + }, + library: { + _group_prefix: 'library', + create: 'library.create', + delete: 'library.delete', + get: 'library.get', + get_by_id: 'library.get_by_id', + scan: 'library.scan', + series: 'library.series', + stats: 'library.stats', + update: 'library.update', + }, + media: { + _group_prefix: 'media', + get: 'media.get', + get_by_id: 'media.get_by_id', + get_with_cursor: 'media.get_with_cursor', + in_progress: 'media.in_progress', + recently_added: 'media.recently_added', + }, + series: { + _group_prefix: 'series', + get: 'series.get', + get_by_id: 'series.get_by_id', + media: 'series.media', + recently_added: 'series.recently_added', + up_next: 'series.up_next', + }, +} diff --git a/packages/client/src/stores/index.ts b/packages/client/src/stores/index.ts new file mode 100644 index 000000000..b2d4d6dc1 --- /dev/null +++ b/packages/client/src/stores/index.ts @@ -0,0 +1,10 @@ +export * from './useJobStore' +export * from './useQueryParamStore' +export * from './useStumpStore' +export * from './useTopBarStore' +export * from './useUserStore' + +export interface StoreBase> { + reset(): void + set(changes: Partial): void +} diff --git a/common/client/src/stores/useJobStore.ts b/packages/client/src/stores/useJobStore.ts similarity index 62% rename from common/client/src/stores/useJobStore.ts rename to packages/client/src/stores/useJobStore.ts index 80ad914a7..4fca250e6 100644 --- a/common/client/src/stores/useJobStore.ts +++ b/packages/client/src/stores/useJobStore.ts @@ -1,60 +1,81 @@ -import type { JobUpdate } from '../types'; -import create from 'zustand'; -import { devtools } from 'zustand/middleware'; -import { produce } from 'immer'; -import { StoreBase } from '.'; +import type { JobUpdate } from '@stump/types' +import { produce } from 'immer' +import { create } from 'zustand' +import { devtools } from 'zustand/middleware' + +import { StoreBase } from '.' export const LARGE_JOB_THRESHOLDS = { 1000: 50, - 5000: 100, 10000: 150, -}; + 5000: 100, +} interface JobStore extends StoreBase { - jobs: Record; - addJob(job: JobUpdate): void; - updateJob(job: JobUpdate): void; - completeJob(runnerId: string): void; + jobs: Record + addJob(job: JobUpdate): void + updateJob(job: JobUpdate): void + completeJob(runnerId: string): void } export const useJobStore = create()( devtools((set, get) => ({ - jobs: {}, - addJob(newJob: JobUpdate) { - let job = get().jobs[newJob.runner_id]; + const job = get().jobs[newJob.runner_id] if (job) { - get().updateJob(newJob); + get().updateJob(newJob) } else { set((store) => produce(store, (draft) => { draft.jobs = { ...store.jobs, [newJob.runner_id]: newJob, - }; + } }), - ); + ) } }, + + // TODO: delete job? will be in DB so not really needed anymore + completeJob(runnerId: string) { + const job = get().jobs[runnerId] + + if (job) { + set((store) => + produce(store, (draft) => { + draft.jobs[runnerId]!.status = 'COMPLETED' + }), + ) + } + }, + + jobs: {}, + + reset() { + set(() => ({})) + }, + set(changes) { + set((state) => ({ ...state, ...changes })) + }, updateJob(jobUpdate: JobUpdate) { - let jobs = get().jobs; + const jobs = get().jobs - let job = jobs[jobUpdate.runner_id]; + const job = jobs[jobUpdate.runner_id] if (!job || !Object.keys(jobs).length) { - get().addJob(jobUpdate); + get().addJob(jobUpdate) - return; + return } - const { current_task, message, task_count } = jobUpdate; + const { current_task, message, task_count } = jobUpdate // if the task_count is greater than 1000, update the store every 50 tasks // otherwise, update the store as is - let curr = Number(current_task); - let isDifferentCount = task_count !== job.task_count; - let isLargeJob = task_count > 1000; + // const curr = Number(current_task); + // const isDifferentCount = task_count !== job.task_count; + // const isLargeJob = task_count > 1000; // if (isLargeJob && !isDifferentCount) { // // get the threshold based on the closest key in the LARGE_JOB_THRESHOLDS object // let threshold_key = Object.keys(LARGE_JOB_THRESHOLDS).reduce((prev, curr) => { @@ -77,33 +98,14 @@ export const useJobStore = create()( set((store) => produce(store, (draft) => { - draft.jobs[jobUpdate.runner_id].current_task = current_task; - draft.jobs[jobUpdate.runner_id].message = message; + draft.jobs[jobUpdate.runner_id]!.current_task = current_task + draft.jobs[jobUpdate.runner_id]!.message = message if (task_count !== store.jobs[jobUpdate.runner_id]?.task_count) { - draft.jobs[jobUpdate.runner_id].task_count = task_count; + draft.jobs[jobUpdate.runner_id]!.task_count = task_count } }), - ); - }, - - // TODO: delete job? will be in DB so not really needed anymore - completeJob(runnerId: string) { - const job = get().jobs[runnerId]; - - if (job) { - set((store) => - produce(store, (draft) => { - draft.jobs[runnerId].status = 'COMPLETED'; - }), - ); - } - }, - reset() { - set(() => ({})); - }, - set(changes) { - set((state) => ({ ...state, ...changes })); + ) }, })), -); +) diff --git a/packages/client/src/stores/useLayoutStore.ts b/packages/client/src/stores/useLayoutStore.ts new file mode 100644 index 000000000..695c8c501 --- /dev/null +++ b/packages/client/src/stores/useLayoutStore.ts @@ -0,0 +1,22 @@ +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' + +import { StoreBase } from '.' + +type LayoutStore = StoreBase + +export const useLayoutStore = create()( + devtools( + persist( + (set) => ({ + reset() { + set(() => ({})) + }, + set(changes) { + set((state) => ({ ...state, ...changes })) + }, + }), + { name: 'stump-layout-store' }, + ), + ), +) diff --git a/packages/client/src/stores/useQueryParamStore.ts b/packages/client/src/stores/useQueryParamStore.ts new file mode 100644 index 000000000..65ed50238 --- /dev/null +++ b/packages/client/src/stores/useQueryParamStore.ts @@ -0,0 +1,84 @@ +import type { Direction, PageParams, QueryOrder } from '@stump/types' +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' + +import { StoreBase } from '.' + +export const DEFAULT_ORDER_BY = 'name' +export const DEFAULT_ORDER_DIRECTION = 'asc' +export const DEFAULT_PAGE_SIZE = 20 + +// TODO: search? +export interface QueryParamStore + extends Partial, + StoreBase { + setZeroBased: (zeroBased?: boolean) => void + setPageSize: (pageSize?: number) => void + setOrderBy: (orderBy?: string) => void + setDirection: (direction?: Direction) => void + + getQueryString: () => string +} + +const defaultValues: Partial = { + direction: 'asc', + // zeroBased: false, + // pageSize: 20, + order_by: 'name', +} + +export const useQueryParamStore = create()( + devtools( + persist( + (set, get) => ({ + ...defaultValues, + + getQueryString() { + let params = '' + + for (const [key, value] of Object.entries(get())) { + if (value != undefined && typeof value !== 'function' && typeof value !== 'object') { + params += `${key}=${value}&` + } + } + + // remote trailing & if present + if (params.endsWith('&')) { + return params.slice(0, -1) + } + + return params + }, + reset() { + set(() => ({})) + }, + set(changes) { + set((state) => ({ ...state, ...changes })) + }, + setDirection(direction) { + set((store) => ({ ...store, direction })) + }, + + setOrderBy(orderBy) { + set((store) => ({ ...store, order_by: orderBy })) + }, + + setPageSize(pageSize) { + set((store) => ({ ...store, page_zize: pageSize })) + }, + setZeroBased(zeroBased) { + set((store) => ({ ...store, zero_based: zeroBased })) + }, + }), + { + getStorage: () => sessionStorage, + name: 'stump-query-param-store', + partialize(store) { + return { + direction: store.direction, + } + }, + }, + ), + ), +) diff --git a/common/client/src/stores/useStumpStore.ts b/packages/client/src/stores/useStumpStore.ts similarity index 59% rename from common/client/src/stores/useStumpStore.ts rename to packages/client/src/stores/useStumpStore.ts index 1f1aabde1..3a660f904 100644 --- a/common/client/src/stores/useStumpStore.ts +++ b/packages/client/src/stores/useStumpStore.ts @@ -1,12 +1,13 @@ -import create from 'zustand'; -import { devtools, persist } from 'zustand/middleware'; -import { StoreBase } from '.'; +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' + +import { StoreBase } from '.' interface StumpStore extends StoreBase { - baseUrl?: string; - connected: boolean; - setBaseUrl(baseUrl: string): void; - setConnected(connected: boolean): void; + baseUrl?: string + connected: boolean + setBaseUrl(baseUrl: string): void + setConnected(connected: boolean): void } export const useStumpStore = create()( @@ -18,35 +19,35 @@ export const useStumpStore = create()( persist( (set) => ({ connected: false, + reset() { + set(() => ({})) + }, + set(changes) { + set((state) => ({ ...state, ...changes })) + }, setBaseUrl(baseUrl: string) { - let adjustedBaseUrl = baseUrl; + let adjustedBaseUrl = baseUrl if (baseUrl.endsWith('/')) { - adjustedBaseUrl = baseUrl.slice(0, -1); + adjustedBaseUrl = baseUrl.slice(0, -1) } if (!baseUrl.endsWith('/api')) { - adjustedBaseUrl += '/api'; + adjustedBaseUrl += '/api' } - set({ baseUrl: adjustedBaseUrl }); + set({ baseUrl: adjustedBaseUrl }) }, setConnected(connected: boolean) { - set({ connected }); - }, - reset() { - set(() => ({})); - }, - set(changes) { - set((state) => ({ ...state, ...changes })); + set({ connected }) }, }), { name: 'stump-config-store', partialize(state) { - return { baseUrl: state.baseUrl }; + return { baseUrl: state.baseUrl } }, }, ), ), -); +) diff --git a/packages/client/src/stores/useTopBarStore.ts b/packages/client/src/stores/useTopBarStore.ts new file mode 100644 index 000000000..3d5f16a62 --- /dev/null +++ b/packages/client/src/stores/useTopBarStore.ts @@ -0,0 +1,34 @@ +import { create } from 'zustand' +import { devtools } from 'zustand/middleware' + +import { StoreBase } from '.' + +export interface TopBarStore extends StoreBase { + title?: string + backwardsUrl?: string | number + forwardsUrl?: string | number + + setTitle(title?: string): void + setBackwardsUrl(backwardsUrl?: string | number): void + setForwardsUrl(forwardsUrl?: string | number): void +} + +export const useTopBarStore = create()( + devtools((set) => ({ + reset() { + set(() => ({})) + }, + set(changes) { + set((state) => ({ ...state, ...changes })) + }, + setBackwardsUrl(backwardsUrl) { + set((store) => ({ ...store, backwardsUrl })) + }, + setForwardsUrl(forwardsUrl) { + set((store) => ({ ...store, forwardsUrl })) + }, + setTitle(title) { + set((store) => ({ ...store, title })) + }, + })), +) diff --git a/common/client/src/stores/useUserStore.ts b/packages/client/src/stores/useUserStore.ts similarity index 56% rename from common/client/src/stores/useUserStore.ts rename to packages/client/src/stores/useUserStore.ts index 43415c4f7..a2e32bfb8 100644 --- a/common/client/src/stores/useUserStore.ts +++ b/packages/client/src/stores/useUserStore.ts @@ -1,42 +1,46 @@ -import type { User, UserPreferences } from '../types'; -import create from 'zustand'; -import { devtools, persist } from 'zustand/middleware'; -import { produce } from 'immer'; -import { StoreBase } from '.'; +import type { User, UserPreferences } from '@stump/types' +import { produce } from 'immer' +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' +import { StoreBase } from '.' + +// TODO: isServerOwner computed value +// https://github.com/cmlarsen/zustand-middleware-computed-state interface UserStore extends StoreBase { - user?: User | null; - userPreferences?: UserPreferences | null; + user?: User | null + userPreferences?: UserPreferences | null - setUser: (user?: User | null) => void; - setUserPreferences: (userPreferences: UserPreferences | null) => void; + setUser: (user?: User | null) => void + setUserPreferences: (userPreferences: UserPreferences | null) => void } +// TODO: consider renaming to useAuth export const useUserStore = create()( devtools( persist( (set, get) => ({ + reset() { + set(() => ({})) + }, + set(changes) { + set((state) => ({ ...state, ...changes })) + }, setUser(user?: User | null) { set((state) => produce(state, (draft) => { - draft.user = user; + draft.user = user }), - ); + ) - get().setUserPreferences(user?.user_preferences ?? null); + get().setUserPreferences(user?.user_preferences ?? null) }, setUserPreferences(userPreferences: UserPreferences | null) { set((state) => produce(state, (draft) => { - draft.userPreferences = userPreferences; + draft.userPreferences = userPreferences }), - ); - }, - reset() { - set(() => ({})); - }, - set(changes) { - set((state) => ({ ...state, ...changes })); + ) }, }), { @@ -47,9 +51,9 @@ export const useUserStore = create()( // happening, I think the locale should be preserved (which is in userPreferences). return { userPreferences: store.userPreferences, - }; + } }, }, ), ), -); +) diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json new file mode 100644 index 000000000..04652b7f6 --- /dev/null +++ b/packages/client/tsconfig.json @@ -0,0 +1,36 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "skipLibCheck": true, + "outDir": "../../.moon/cache/types/packages/client", + "paths": { + "@stump/api": [ + "../api/src/index.ts" + ], + "@stump/api/*": [ + "../api/src/*" + ], + "@stump/types": [ + "../types/index.ts" + ], + "@stump/types/*": [ + "../types/*" + ] + } + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules" + ], + "references": [ + { + "path": "../api" + }, + { + "path": "../types" + } + ] +} diff --git a/packages/components/package.json b/packages/components/package.json new file mode 100644 index 000000000..cc2a052fc --- /dev/null +++ b/packages/components/package.json @@ -0,0 +1,9 @@ +{ + "name": "@stump/components", + "version": "0.0.0", + "devDependencies": { + "@tailwindcss/typography": "^0.5.9", + "tailwind-scrollbar-hide": "^1.1.7", + "typescript": "^4.9.5" + } +} diff --git a/common/config/tailwind.js b/packages/components/tailwind.js similarity index 85% rename from common/config/tailwind.js rename to packages/components/tailwind.js index 248cb25aa..03c3bd5e0 100644 --- a/common/config/tailwind.js +++ b/packages/components/tailwind.js @@ -1,16 +1,16 @@ const brand = { - DEFAULT: '#C48259', - 50: '#F4E8E0', 100: '#EFDDD1', 200: '#E4C6B3', 300: '#D9AF95', 400: '#CF9977', + 50: '#F4E8E0', 500: '#C48259', 600: '#A9663C', 700: '#7F4D2D', 800: '#56341F', 900: '#2D1B10', -}; + DEFAULT: '#C48259', +} /** * @@ -19,20 +19,24 @@ const brand = { */ module.exports = function (app) { let config = { - darkMode: 'class', // content: ['./**/*.{js,ts,jsx,tsx}', './index.html'], content: [ - '../../common/*/src/**/*.{js,ts,jsx,tsx,html}', + '../../packages/*/src/**/*.{js,ts,jsx,tsx,html}', app ? `../../apps/${app}/src/**/*.{js,ts,jsx,tsx,html}` : `./src/**/*.{js,ts,jsx,tsx,html}`, ], + // NOTE: this allows me to sync tailwind dark mode with chakra-ui dark mode *yeet* + // so happy I found this! + darkMode: ['class', '[data-theme="dark"]'], + plugins: [require('tailwind-scrollbar-hide'), require('@tailwindcss/typography')], theme: { extend: { colors: { + brand, gray: { - 50: '#F7FAFC', - 65: '#F3F7FA', - 75: '#F1F5F9', 100: '#EDF2F7', + 1000: '#0B0C11', + 1050: '#050507', + 1100: '#010101', 150: '#E8EDF4', 200: '#E2E8F0', 250: '#D8DFE9', @@ -40,21 +44,20 @@ module.exports = function (app) { 350: '#B7C3D1', 400: '#A0AEC0', 450: '#8B99AD', + 50: '#F7FAFC', 500: '#718096', 550: '#5F6C81', 600: '#4A5568', + 65: '#F3F7FA', 650: '#3D4759', 700: '#2D3748', + 75: '#F1F5F9', 750: '#212836', 800: '#1A202C', 850: '#191D28', 900: '#171923', 950: '#11121A', - 1000: '#0B0C11', - 1050: '#050507', - 1100: '#010101', }, - brand, }, ringColor: { DEFAULT: brand['500'], @@ -69,8 +72,7 @@ module.exports = function (app) { backgroundImage: ['dark'], }, }, - plugins: [require('tailwind-scrollbar-hide'), require('@tailwindcss/typography')], - }; + } - return config; -}; + return config +} diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json new file mode 100644 index 000000000..efff6c940 --- /dev/null +++ b/packages/components/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.options.json", + "include": [ + "**/*" + ], + "references": [], + "compilerOptions": { + "outDir": "../../.moon/cache/types/packages/components" + } +} diff --git a/common/interface/README.md b/packages/interface/README.md similarity index 100% rename from common/interface/README.md rename to packages/interface/README.md diff --git a/packages/interface/package.json b/packages/interface/package.json new file mode 100644 index 000000000..cf7c555cd --- /dev/null +++ b/packages/interface/package.json @@ -0,0 +1,69 @@ +{ + "name": "@stump/interface", + "version": "0.0.0", + "description": "", + "license": "MIT", + "private": true, + "main": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./assets/*": "./src/assets/*" + }, + "dependencies": { + "@chakra-ui/react": "^2.5.1", + "@emotion/react": "^11.10.6", + "@emotion/styled": "^11.10.6", + "@hookform/resolvers": "^2.9.11", + "@stump/api": "workspace:*", + "@stump/client": "workspace:*", + "@stump/types": "workspace:*", + "@tanstack/react-query": "^4.20.4", + "@tanstack/react-query-devtools": "^4.20.4", + "@tanstack/react-table": "^8.7.9", + "@tanstack/react-virtual": "3.0.0-beta.18", + "chakra-react-select": "^4.4.3", + "class-variance-authority": "^0.2.4", + "clsx": "^1.2.1", + "dayjs": "^1.11.7", + "epubjs": "^0.3.93", + "framer-motion": "^7.10.3", + "i18next": "^21.10.0", + "immer": "^9.0.19", + "nprogress": "^0.2.0", + "phosphor-react": "^1.4.1", + "pluralize": "^8.0.0", + "react-dom": "^18.2.0", + "react-error-boundary": "^3.1.4", + "react-helmet": "^6.1.0", + "react-hook-form": "^7.43.1", + "react-hot-toast": "^2.4.0", + "react-hotkeys-hook": "^3.4.7", + "react-i18next": "^11.18.6", + "react-router": "^6.8.1", + "react-router-dom": "^6.8.1", + "react-swipeable": "^7.0.0", + "rooks": "^7.4.3", + "use-count-up": "^3.0.1", + "zod": "^3.20.6", + "zustand": "^4.3.3" + }, + "devDependencies": { + "@types/node": "^18.14.0", + "@types/nprogress": "^0.2.0", + "@types/pluralize": "^0.0.29", + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.11", + "@types/react-helmet": "^6.1.6", + "@types/react-router-dom": "^5.3.3", + "@vitejs/plugin-react": "^2.2.0", + "typescript": "^4.9.5", + "vite": "^3.2.5" + }, + "pnpm": { + "peerDependencyRules": { + "ignoreMissing": [ + "@babel/core" + ] + } + } +} diff --git a/common/interface/public/assets/fallbacks/image-file.svg b/packages/interface/public/assets/fallbacks/image-file.svg similarity index 100% rename from common/interface/public/assets/fallbacks/image-file.svg rename to packages/interface/public/assets/fallbacks/image-file.svg diff --git a/common/interface/public/assets/favicon.ico b/packages/interface/public/assets/favicon.ico similarity index 100% rename from common/interface/public/assets/favicon.ico rename to packages/interface/public/assets/favicon.ico diff --git a/common/interface/public/assets/favicon.png b/packages/interface/public/assets/favicon.png similarity index 100% rename from common/interface/public/assets/favicon.png rename to packages/interface/public/assets/favicon.png diff --git a/common/interface/public/assets/icons/archive.svg b/packages/interface/public/assets/icons/archive.svg similarity index 100% rename from common/interface/public/assets/icons/archive.svg rename to packages/interface/public/assets/icons/archive.svg diff --git a/common/interface/public/assets/icons/epub.svg b/packages/interface/public/assets/icons/epub.svg similarity index 100% rename from common/interface/public/assets/icons/epub.svg rename to packages/interface/public/assets/icons/epub.svg diff --git a/common/interface/public/assets/icons/folder.png b/packages/interface/public/assets/icons/folder.png similarity index 100% rename from common/interface/public/assets/icons/folder.png rename to packages/interface/public/assets/icons/folder.png diff --git a/common/interface/public/assets/icons/pdf.svg b/packages/interface/public/assets/icons/pdf.svg similarity index 100% rename from common/interface/public/assets/icons/pdf.svg rename to packages/interface/public/assets/icons/pdf.svg diff --git a/common/interface/public/assets/stump-logo--irregular-lg.png b/packages/interface/public/assets/stump-logo--irregular-lg.png similarity index 100% rename from common/interface/public/assets/stump-logo--irregular-lg.png rename to packages/interface/public/assets/stump-logo--irregular-lg.png diff --git a/common/interface/public/assets/stump-logo--irregular-sm.png b/packages/interface/public/assets/stump-logo--irregular-sm.png similarity index 100% rename from common/interface/public/assets/stump-logo--irregular-sm.png rename to packages/interface/public/assets/stump-logo--irregular-sm.png diff --git a/common/interface/public/assets/stump-logo--irregular-xs.png b/packages/interface/public/assets/stump-logo--irregular-xs.png similarity index 100% rename from common/interface/public/assets/stump-logo--irregular-xs.png rename to packages/interface/public/assets/stump-logo--irregular-xs.png diff --git a/common/interface/public/assets/stump-logo--irregular.png b/packages/interface/public/assets/stump-logo--irregular.png similarity index 100% rename from common/interface/public/assets/stump-logo--irregular.png rename to packages/interface/public/assets/stump-logo--irregular.png diff --git a/common/interface/public/assets/stump-logo--square.png b/packages/interface/public/assets/stump-logo--square.png similarity index 100% rename from common/interface/public/assets/stump-logo--square.png rename to packages/interface/public/assets/stump-logo--square.png diff --git a/common/interface/public/favicon.ico b/packages/interface/public/favicon.ico similarity index 100% rename from common/interface/public/favicon.ico rename to packages/interface/public/favicon.ico diff --git a/packages/interface/src/App.tsx b/packages/interface/src/App.tsx new file mode 100644 index 000000000..7893e05f1 --- /dev/null +++ b/packages/interface/src/App.tsx @@ -0,0 +1,126 @@ +import './styles/index.css' + +import { ChakraProvider } from '@chakra-ui/react' +import { initializeApi } from '@stump/api' +import { + AppProps, + AppPropsContext, + JobContextProvider, + queryClient, + StumpClientContextProvider, + useStumpStore, + useTopBarStore, +} from '@stump/client' +import { defaultContext, QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { useEffect, useState } from 'react' +import { ErrorBoundary } from 'react-error-boundary' +import { Helmet } from 'react-helmet' +import { BrowserRouter, createSearchParams, useLocation, useNavigate } from 'react-router-dom' + +import { AppRouter } from './AppRouter' +import { chakraTheme } from './chakra' +import { ErrorFallback } from './components/ErrorFallback' +import Notifications from './components/Notifications' +import { API_VERSION } from './index' + +function RouterContainer(props: { appProps: AppProps }) { + const location = useLocation() + const navigate = useNavigate() + + const [mounted, setMounted] = useState(false) + const [appProps, setAppProps] = useState(props.appProps) + + const setTitle = useTopBarStore(({ setTitle }) => setTitle) + + const { baseUrl, setBaseUrl } = useStumpStore(({ baseUrl, setBaseUrl }) => ({ + baseUrl, + setBaseUrl, + })) + + useEffect( + () => { + if (!baseUrl && appProps.baseUrl) { + setBaseUrl(appProps.baseUrl) + } else if (baseUrl) { + initializeApi(baseUrl, API_VERSION) + + setAppProps((appProps) => ({ + ...appProps, + baseUrl, + })) + } + + setMounted(true) + }, + + // eslint-disable-next-line react-hooks/exhaustive-deps + [baseUrl], + ) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function handleHelmetChange(newState: any) { + if (Array.isArray(newState?.title) && newState.title.length > 0) { + if (newState.title.length > 1) { + setTitle(newState.title[newState.title.length - 1]) + } else { + setTitle(newState.title[0]) + } + } else if (typeof newState?.title === 'string') { + if (newState.title === 'Stump') { + setTitle('') + } else { + setTitle(newState.title) + } + } + } + + const handleRedirect = (url: string) => { + navigate({ + pathname: url, + search: createSearchParams({ + redirect: location.pathname, + }).toString(), + }) + } + + if (!mounted) { + // TODO: suspend + return null + } + + return ( + + + + Stump + + + + + + + ) +} + +export default function StumpInterface(props: AppProps) { + return ( + + + + {import.meta.env.MODE === 'development' && ( + + )} + + + + + + + + ) +} diff --git a/packages/interface/src/AppLayout.tsx b/packages/interface/src/AppLayout.tsx new file mode 100644 index 000000000..d01b06676 --- /dev/null +++ b/packages/interface/src/AppLayout.tsx @@ -0,0 +1,94 @@ +import { Box, Flex, useColorModeValue } from '@chakra-ui/react' +import { useAppProps, useAuthQuery, useCoreEventHandler, useUserStore } from '@stump/client' +import React, { useMemo } from 'react' +import { useHotkeys } from 'react-hotkeys-hook' +import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom' + +import CommandPalette from './components/CommandPalette' +import JobOverlay from './components/jobs/JobOverlay' +import Lazy from './components/Lazy' +import ServerStatusOverlay from './components/ServerStatusOverlay' +import Sidebar from './components/sidebar/Sidebar' +import TopBar from './components/topbar/TopBar' +import { AppContext } from './context' + +export function AppLayout() { + const appProps = useAppProps() + + const navigate = useNavigate() + const location = useLocation() + + const hideSidebar = useMemo(() => { + // hide sidebar when on /books/:id/pages/:page or /epub/ + // TODO: replace with single regex, I am lazy rn + return ( + location.pathname.match(/\/books\/.+\/pages\/.+/) || location.pathname.match(/\/epub\/.+/) + ) + }, [location]) + + useCoreEventHandler() + + const { storeUser, setUser } = useUserStore((state) => ({ + setUser: state.setUser, + storeUser: state.user, + })) + + // TODO: platform specific hotkeys + // TODO: cmd+shift+h for home + useHotkeys('ctrl+,, cmd+,', (e) => { + e.preventDefault() + navigate('/settings/general') + }) + + const { error } = useAuthQuery({ + enabled: !storeUser, + onSuccess: setUser, + }) + + const mainColor = useColorModeValue('gray.75', 'gray.900') + + // @ts-expect-error: FIXME: type error no good >:( + if (error?.code === 'ERR_NETWORK' && appProps?.platform !== 'browser') { + return + } + + if (!storeUser) { + return null + // throw new Error('User was not expected to be null') + } + + return ( + + }> + + { + // TODO: uncomment once I add custom menu on Tauri side + // if (appProps?.platform != 'browser') { + // e.preventDefault(); + // return false; + // } + + return true + }} + > + {!hideSidebar && } + + {!hideSidebar && } + }> + + + + + + {appProps?.platform !== 'browser' && } + {!location.pathname.match(/\/settings\/jobs/) && } + + + ) +} diff --git a/packages/interface/src/AppRouter.tsx b/packages/interface/src/AppRouter.tsx new file mode 100644 index 000000000..b9d2977a4 --- /dev/null +++ b/packages/interface/src/AppRouter.tsx @@ -0,0 +1,87 @@ +import { useAppProps } from '@stump/client' +import React from 'react' +import { Navigate } from 'react-router' +import { Route, Routes } from 'react-router-dom' + +import { AppLayout } from './AppLayout' + +// FIXME: this is really annoying +type LazyComponent = Promise<{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + default: React.ComponentType +}> + +// I'm still so annoyed at this lol +const lazily = (loader: () => unknown) => React.lazy(() => loader() as LazyComponent) +const Home = lazily(() => import('./pages/Home')) +const LibraryOverview = lazily(() => import('./pages/library/LibraryOverview')) +const LibraryFileExplorer = lazily(() => import('./pages/library/LibraryFileExplorer')) +const CreateLibrary = lazily(() => import('./pages/library/CreateLibrary')) +const SeriesOverview = lazily(() => import('./pages/SeriesOverview')) +const BookOverview = lazily(() => import('./pages/book/BookOverview')) +const ReadBook = lazily(() => import('./pages/book/ReadBook')) +const ReadEpub = lazily(() => import('./pages/book/ReadEpub')) +const SettingsLayout = lazily(() => import('./components/settings/SettingsLayout')) +const GeneralSettings = lazily(() => import('./pages/settings/GeneralSettings')) +const JobSettings = lazily(() => import('./pages/settings/JobSettings')) +const ServerSettings = lazily(() => import('./pages/settings/ServerSettings')) +const UserSettings = lazily(() => import('./pages/settings/UserSettings')) +const FourOhFour = lazily(() => import('./pages/FourOhFour')) +const ServerConnectionError = lazily(() => import('./pages/ServerConnectionError')) +const LoginOrClaim = lazily(() => import('./pages/LoginOrClaim')) +const OnBoarding = lazily(() => import('./pages/OnBoarding')) + +function OnBoardingRouter() { + return ( + + + } /> + + + ) +} + +export function AppRouter() { + const appProps = useAppProps() + + if (!appProps?.baseUrl) { + if (appProps?.platform === 'browser') { + throw new Error('Base URL is not set') + } + + return + } + + return ( + + }> + } /> + + } /> + } /> + } /> + + } /> + + } /> + } /> + } /> + + }> + } /> + } /> + } /> + } /> + } /> + {appProps?.platform !== 'browser' && Desktop!} />} + + + + } /> + {appProps?.platform !== 'browser' && ( + } /> + )} + } /> + + ) +} diff --git a/common/interface/src/chakra.ts b/packages/interface/src/chakra.ts similarity index 94% rename from common/interface/src/chakra.ts rename to packages/interface/src/chakra.ts index 466e08119..756b62bab 100644 --- a/common/interface/src/chakra.ts +++ b/packages/interface/src/chakra.ts @@ -1,29 +1,29 @@ -import { extendTheme } from '@chakra-ui/react'; +import { extendTheme } from '@chakra-ui/react' const config = { initialColorMode: 'dark', useSystemColorMode: false, -}; +} const colors = { brand: { - DEFAULT: '#C48259', - 50: '#F4E8E0', 100: '#EFDDD1', 200: '#E4C6B3', 300: '#D9AF95', 400: '#CF9977', + 50: '#F4E8E0', 500: '#C48259', 600: '#A9663C', 700: '#7F4D2D', 800: '#56341F', 900: '#2D1B10', + DEFAULT: '#C48259', }, gray: { - 50: '#F7FAFC', - 65: '#F3F7FA', - 75: '#F1F5F9', 100: '#EDF2F7', + 1000: '#0B0C11', + 1050: '#050507', + 1100: '#010101', 150: '#E8EDF4', 200: '#E2E8F0', 250: '#D8DFE9', @@ -31,31 +31,29 @@ const colors = { 350: '#B7C3D1', 400: '#A0AEC0', 450: '#8B99AD', + 50: '#F7FAFC', 500: '#718096', 550: '#5F6C81', 600: '#4A5568', + 65: '#F3F7FA', 650: '#3D4759', 700: '#2D3748', + 75: '#F1F5F9', 750: '#212836', 800: '#1A202C', 850: '#191D28', 900: '#171923', 950: '#11121A', - 1000: '#0B0C11', - 1050: '#050507', - 1100: '#010101', }, -}; +} const fonts = { - heading: 'Inter var, sans-serif', body: 'Inter var, sans-serif', -}; + heading: 'Inter var, sans-serif', +} export const chakraTheme = extendTheme({ - config, colors, - fonts, components: { Checkbox: { baseStyle: { @@ -67,4 +65,6 @@ export const chakraTheme = extendTheme({ }, }, }, -}); + config, + fonts, +}) diff --git a/common/interface/src/components/ApplicationVersion.tsx b/packages/interface/src/components/ApplicationVersion.tsx similarity index 69% rename from common/interface/src/components/ApplicationVersion.tsx rename to packages/interface/src/components/ApplicationVersion.tsx index c9379b970..5fd8bfe30 100644 --- a/common/interface/src/components/ApplicationVersion.tsx +++ b/packages/interface/src/components/ApplicationVersion.tsx @@ -1,8 +1,8 @@ -import { useStumpVersion } from '@stump/client'; -import { ArrowSquareOut } from 'phosphor-react'; +import { useStumpVersion } from '@stump/client' +import { ArrowSquareOut } from 'phosphor-react' export default function ApplicationVersion() { - const version = useStumpVersion(); + const version = useStumpVersion() return ( v{version?.semver} - ); + ) } diff --git a/packages/interface/src/components/Card.tsx b/packages/interface/src/components/Card.tsx new file mode 100644 index 000000000..a2ec8ed02 --- /dev/null +++ b/packages/interface/src/components/Card.tsx @@ -0,0 +1,109 @@ +import { Box, BoxProps } from '@chakra-ui/react' +import { cva, VariantProps } from 'class-variance-authority' +import clsx from 'clsx' +import { ComponentProps } from 'react' +import { Link } from 'react-router-dom' + +// FIXME: what's not to fix here lol just not a great variant pattern... +export const cardVariants = cva('relative shadow rounded-md', { + defaultVariants: {}, + variants: { + variant: { + default: '', + fixedImage: 'w-[10rem] sm:w-[10.666rem] md:w-[12rem]', + image: + 'min-w-[10rem] min-h-[15rem] sm:min-w-[10.666rem] sm:min-h-[16rem] md:min-w-[12rem] md:min-h-[18.666rem]', + }, + }, +}) +type CardBaseProps = ComponentProps<'div'> & { + to?: string + overlay?: React.ReactNode + children: React.ReactNode +} +export type CardProps = VariantProps & CardBaseProps + +// TODO: fix tab focus +export default function Card({ to, overlay, children, variant, className, ...rest }: CardProps) { + const card = ( + + {overlay} + + {children} + + ) + + if (to) { + return {card} + } + + return card +} + +interface CardBodyProps extends Omit { + children: React.ReactNode +} + +export function CardBody({ children, className, ...props }: CardBodyProps) { + return ( + + {children} + + ) +} + +interface CardFooterProps extends Omit { + children: React.ReactNode +} + +export function CardFooter({ children, ...props }: CardFooterProps) { + return ( + + {children} + + ) +} + +export function CardGrid({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ) +} + +// function CardCornerDecoration() { +// return ( +// +// {children} +// +// ); +// } diff --git a/common/interface/src/components/CommandPalette.tsx b/packages/interface/src/components/CommandPalette.tsx similarity index 56% rename from common/interface/src/components/CommandPalette.tsx rename to packages/interface/src/components/CommandPalette.tsx index 854b6e219..aebd77a20 100644 --- a/common/interface/src/components/CommandPalette.tsx +++ b/packages/interface/src/components/CommandPalette.tsx @@ -1,9 +1,3 @@ -import { MagnifyingGlass } from 'phosphor-react'; -import { useEffect, useRef, useState } from 'react'; -import toast from 'react-hot-toast'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useDebounce } from 'rooks'; - import { Badge, Box, @@ -23,95 +17,102 @@ import { useBoolean, useColorModeValue, VStack, -} from '@chakra-ui/react'; -import type { FileStatus } from '@stump/client'; +} from '@chakra-ui/react' +import type { FileStatus } from '@stump/types' +import { MagnifyingGlass } from 'phosphor-react' +import { useEffect, useRef, useState } from 'react' +import toast from 'react-hot-toast' +import { useHotkeys } from 'react-hotkeys-hook' +import { useDebounce } from 'rooks' -import FileStatusBadge from './FileStatusBadge'; +import FileStatusBadge from './FileStatusBadge' const fakeResults = [ { - id: '1622643048696-883eafe4d8dc', - title: 'The Witcher', description: 'A witcher is a monster hunter who has been trained from birth to hunt and kill monsters. The witchers are a solitary group, and they are feared by the monsters they hunt.', href: '/books/1', + id: '1622643048696-883eafe4d8dc', status: 'READY', tags: [ { id: 'fjkd', name: 'fantasy' }, { id: 'fjkdd', name: 'adventure' }, ], + title: 'The Witcher', }, { - id: '1600637453426-7c64826b19d9', - title: 'Lord of the Rings: The Fellowship of the Ring', description: 'Easily the best fantasy world ever written, The Lord of the Rings revolves around high adventure, undertaken by a group of companions on a perilous journey to save their world from the evil of Sauron. It is a story of friendship, courage, duty, loyalty, love, sacrifice, and the triumph of good over evil.', href: '/books/2', + id: '1600637453426-7c64826b19d9', status: 'READY', tags: [ { id: 'fjkd', name: 'fantasy' }, { id: 'fjkdd', name: 'adventure' }, { id: 'fjkdde', name: 'epic' }, ], + title: 'Lord of the Rings: The Fellowship of the Ring', }, { - id: '1597350289957-120f34437361', - title: 'The Hobbit', description: 'The Hobbit is a fantasy novel by English author J.R.R. Tolkien. It was published on 21 September 1937 to wide critical acclaim, being nominated for the Carnegie Medal and awarded a prize from the New York Herald Tribune for best juvenile fiction.', href: '/books/3', + id: '1597350289957-120f34437361', status: 'READY', tags: [ { id: 'fjkd', name: 'fantasy' }, { id: 'fjkdd', name: 'adventure' }, ], + title: 'The Hobbit', }, -]; +] export default function CommandPalette() { - const inputRef = useRef(null); + const inputRef = useRef(null) - const [open, { on, off }] = useBoolean(false); - const [results, setResults] = useState(); - const [loading, setLoading] = useState(false); + const [open, { on, off }] = useBoolean(false) + const [results, setResults] = useState() + const [loading, setLoading] = useState(false) useEffect(() => { if (open) { - toast.error("I don't support search yet, check back soon!"); + toast.error("I don't support search yet, check back soon!") } return () => { - setResults(undefined); - }; - }, [open]); + setResults(undefined) + } + }, [open]) - useHotkeys('ctrl+k, cmd+k', (e, _hotKeyEvent) => { - e.preventDefault(); + useHotkeys('ctrl+k, cmd+k', (e) => { + e.preventDefault() // TODO: only use cmd+k on mac, ctrl+k on windows, etc. - on(); - inputRef.current?.focus(); - }); + on() + inputRef.current?.focus() + }) async function handleSearch() { - setLoading(true); + setLoading(true) setTimeout(() => { - setLoading(false); - setResults(fakeResults); - }, 800); + setLoading(false) + setResults(fakeResults) + }, 800) } - const onInputStop = useDebounce(handleSearch, 500); + const onInputStop = useDebounce(handleSearch, 500) return ( - + - } /> + + + {loading && ( - } - /> + + + )} @@ -136,25 +136,33 @@ export default function CommandPalette() { - ); + ) } -const fakeBaseUrl = 'https://images.unsplash.com/photo-'; +const fakeBaseUrl = 'https://images.unsplash.com/photo-' function QueryResults({ results }: { results?: typeof fakeResults }) { - const [selected, setSelected] = useState(0); + const [selected, setSelected] = useState(0) - useEffect(() => { - if (selected !== 0) { - setSelected(0); - } + const bgColor = useColorModeValue('gray.300', 'gray.600') + const selectedItemDescriptionColor = useColorModeValue('gray.500', 'gray.450') - return () => { - setSelected(0); - }; - }, [results]); + useEffect( + () => { + if (selected !== 0) { + setSelected(0) + } + + return () => { + setSelected(0) + } + }, + + // eslint-disable-next-line react-hooks/exhaustive-deps + [results], + ) if (!results) { - return null; + return null } if (results.length === 0) { @@ -162,10 +170,10 @@ function QueryResults({ results }: { results?: typeof fakeResults }) { No results found - ); + ) } - const selectedItem = results[selected]; + const selectedItem = results[selected] return ( <> @@ -183,33 +191,35 @@ function QueryResults({ results }: { results?: typeof fakeResults }) { key={`${id}-${title}-listitem`} onClick={() => setSelected(i)} className="px-2 py-1 cursor-pointer rounded-md" - bg={selected === i ? useColorModeValue('gray.300', 'gray.600') : undefined} - _hover={{ bg: useColorModeValue('gray.300', 'gray.600') }} + bg={selected === i ? bgColor : undefined} + _hover={{ bg: bgColor }} > {title} ))} - - {selectedItem.title} - - - {selectedItem.description} - - - - - {selectedItem.tags.map(({ id, name }) => ( - - {name} - - ))} - - + {selectedItem && ( + + {selectedItem.title} + + + {selectedItem.description} + + + + + {selectedItem.tags.map(({ id, name }) => ( + + {name} + + ))} + + + )} - ); + ) } diff --git a/common/interface/src/components/DirectoryPickerModal.tsx b/packages/interface/src/components/DirectoryPickerModal.tsx similarity index 80% rename from common/interface/src/components/DirectoryPickerModal.tsx rename to packages/interface/src/components/DirectoryPickerModal.tsx index 8915168b5..6f4134628 100644 --- a/common/interface/src/components/DirectoryPickerModal.tsx +++ b/packages/interface/src/components/DirectoryPickerModal.tsx @@ -1,4 +1,3 @@ -import { useEffect, useMemo } from 'react'; import { Checkbox, Flex, @@ -13,53 +12,55 @@ import { Text, useBoolean, useDisclosure, -} from '@chakra-ui/react'; -import { ArrowLeft, Folder, FolderNotch } from 'phosphor-react'; -import toast from 'react-hot-toast'; -import { useDirectoryListing } from '@stump/client'; -import Button, { ModalCloseButton } from '../ui/Button'; -import Input from '../ui/Input'; -import ToolTip from '../ui/ToolTip'; +} from '@chakra-ui/react' +import { useDirectoryListing } from '@stump/client' +import { ArrowLeft, Folder, FolderNotch } from 'phosphor-react' +import { useEffect, useMemo } from 'react' +import toast from 'react-hot-toast' + +import Button, { ModalCloseButton } from '../ui/Button' +import Input from '../ui/Input' +import ToolTip from '../ui/ToolTip' interface Props { - startingPath?: string; - onUpdate(path: string | null): void; + startingPath?: string + onUpdate(path: string | null): void } export default function DirectoryPickerModal({ startingPath, onUpdate }: Props) { - const { isOpen, onOpen, onClose } = useDisclosure(); + const { isOpen, onOpen, onClose } = useDisclosure() - const [showHidden, { toggle }] = useBoolean(false); + const [showHidden, { toggle }] = useBoolean(false) // FIXME: This component needs to render a *virtual* list AND pass a page param as the user scrolls // down the list. I recently tested a directory with 1000+ files and it took a while to load. So, // I am paging the results to 100 per page. Might reduce to 50. const { errorMessage, path, parent, directories, onSelect, goBack } = useDirectoryListing({ - startingPath, enabled: isOpen, + startingPath, // TODO: page - }); + }) function handleUpdate() { if (!errorMessage) { - onUpdate(path); - onClose(); + onUpdate(path) + onClose() } } useEffect(() => { if (errorMessage) { - toast.error(errorMessage); + toast.error(errorMessage) } - }, [errorMessage]); + }, [errorMessage]) const directoryList = useMemo(() => { if (showHidden) { - return directories; + return directories } - return directories.filter((d) => !d.name.startsWith('.')); - }, [directories, showHidden]); + return directories.filter((d) => !d.name.startsWith('.')) + }, [directories, showHidden]) return ( <> @@ -89,6 +90,8 @@ export default function DirectoryPickerModal({ startingPath, onUpdate }: Props) { // if (newPath) { // onSelect(newPath); @@ -161,5 +164,5 @@ export default function DirectoryPickerModal({ startingPath, onUpdate }: Props) - ); + ) } diff --git a/common/interface/src/components/ErrorFallback.tsx b/packages/interface/src/components/ErrorFallback.tsx similarity index 85% rename from common/interface/src/components/ErrorFallback.tsx rename to packages/interface/src/components/ErrorFallback.tsx index 8301ed1cd..bd37e23f3 100644 --- a/common/interface/src/components/ErrorFallback.tsx +++ b/packages/interface/src/components/ErrorFallback.tsx @@ -1,16 +1,17 @@ -import { ButtonGroup, Code, Heading, HStack, Stack, Text } from '@chakra-ui/react'; -import { FallbackProps } from 'react-error-boundary'; -import toast from 'react-hot-toast'; -import Button from '../ui/Button'; -import { copyTextToClipboard } from '../utils/misc'; +import { ButtonGroup, Code, Heading, HStack, Stack, Text } from '@chakra-ui/react' +import { FallbackProps } from 'react-error-boundary' +import toast from 'react-hot-toast' + +import Button from '../ui/Button' +import { copyTextToClipboard } from '../utils/misc' // TODO: take in platform? export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { function copyErrorStack() { if (error.stack) { copyTextToClipboard(error.stack).then(() => { - toast.success('Copied error details to your clipboard'); - }); + toast.success('Copied error details to your clipboard') + }) } } @@ -77,5 +78,5 @@ export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { - ); + ) } diff --git a/common/interface/src/components/FileStatusBadge.tsx b/packages/interface/src/components/FileStatusBadge.tsx similarity index 62% rename from common/interface/src/components/FileStatusBadge.tsx rename to packages/interface/src/components/FileStatusBadge.tsx index bd007cf68..008028c63 100644 --- a/common/interface/src/components/FileStatusBadge.tsx +++ b/packages/interface/src/components/FileStatusBadge.tsx @@ -1,22 +1,21 @@ -import { Badge } from '@chakra-ui/react'; - -import type { FileStatus } from '@stump/client'; +import { Badge } from '@chakra-ui/react' +import type { FileStatus } from '@stump/types' export default function FileStatusBadge({ status }: { status: FileStatus }) { const color = (() => { if (status === 'READY') { - return 'green'; + return 'green' } else if (status === 'MISSING') { - return 'yellow'; + return 'yellow' } else if (status === 'ERROR') { - return 'red'; + return 'red' } - return 'gray'; - })(); + return 'gray' + })() return ( {status} - ); + ) } diff --git a/packages/interface/src/components/Lazy.tsx b/packages/interface/src/components/Lazy.tsx new file mode 100644 index 000000000..f8a471e6c --- /dev/null +++ b/packages/interface/src/components/Lazy.tsx @@ -0,0 +1,19 @@ +import nprogress from 'nprogress' +import { useEffect } from 'react' + +export default function Lazy() { + useEffect(() => { + let timeout: NodeJS.Timeout + // loader doesn't need to start immediately, if it only takes 100ms to load i'd rather + // not show it at all than a quick flash + // eslint-disable-next-line prefer-const + timeout = setTimeout(() => nprogress.start(), 100) + + return () => { + clearTimeout(timeout) + nprogress.done() + } + }) + + return null +} diff --git a/packages/interface/src/components/LazyImage.tsx b/packages/interface/src/components/LazyImage.tsx new file mode 100644 index 000000000..3b4f1aa66 --- /dev/null +++ b/packages/interface/src/components/LazyImage.tsx @@ -0,0 +1,22 @@ +import clsx from 'clsx' +import { useState } from 'react' + +type Props = React.ComponentProps<'img'> + +// TODO: improve image loading here, looks doggy doody right now +export default function LazyImage({ src, className, ...props }: Props) { + const [isLoaded, setIsLoaded] = useState(false) + + return ( + <> + + + setIsLoaded(true)} + /> + + ) +} diff --git a/common/interface/src/components/ListItem.tsx b/packages/interface/src/components/ListItem.tsx similarity index 81% rename from common/interface/src/components/ListItem.tsx rename to packages/interface/src/components/ListItem.tsx index 55f74547a..e6ecf64f0 100644 --- a/common/interface/src/components/ListItem.tsx +++ b/packages/interface/src/components/ListItem.tsx @@ -1,12 +1,12 @@ -import { Heading, HStack, Text, useColorModeValue } from '@chakra-ui/react'; -import { Link } from 'react-router-dom'; +import { Heading, HStack, Text, useColorModeValue } from '@chakra-ui/react' +import { Link } from 'react-router-dom' interface Props { - id: string; - title: string; - subtitle?: string | null; - href: string; - even?: boolean; + id: string + title: string + subtitle?: string | null + href: string + even?: boolean } // Used to render the items in the series list and media list @@ -34,7 +34,7 @@ export default function ListItem({ id, title, subtitle, href, even }: Props) { {title} @@ -49,5 +49,5 @@ export default function ListItem({ id, title, subtitle, href, even }: Props) { {subtitle} - ); + ) } diff --git a/common/interface/src/components/Notifications.tsx b/packages/interface/src/components/Notifications.tsx similarity index 75% rename from common/interface/src/components/Notifications.tsx rename to packages/interface/src/components/Notifications.tsx index 34a566e80..0654d56a8 100644 --- a/common/interface/src/components/Notifications.tsx +++ b/packages/interface/src/components/Notifications.tsx @@ -1,8 +1,8 @@ -import { useColorMode } from '@chakra-ui/react'; -import { Toaster, ToastBar } from 'react-hot-toast'; +import { useColorMode } from '@chakra-ui/react' +import { ToastBar, Toaster } from 'react-hot-toast' export default function Notifications() { - const { colorMode } = useColorMode(); + const { colorMode } = useColorMode() return ( @@ -24,5 +24,5 @@ export default function Notifications() { )} - ); + ) } diff --git a/common/interface/src/components/PagePopoverForm.tsx b/packages/interface/src/components/PagePopoverForm.tsx similarity index 71% rename from common/interface/src/components/PagePopoverForm.tsx rename to packages/interface/src/components/PagePopoverForm.tsx index a2f8a1cd1..85eb5905e 100644 --- a/common/interface/src/components/PagePopoverForm.tsx +++ b/packages/interface/src/components/PagePopoverForm.tsx @@ -11,20 +11,21 @@ import { PopoverTrigger, useColorModeValue, useDisclosure, -} from '@chakra-ui/react'; -import { zodResolver } from '@hookform/resolvers/zod'; -import React, { useMemo, useRef } from 'react'; -import { FieldValues, useForm } from 'react-hook-form'; -import { z } from 'zod'; -import Button from '../ui/Button'; -import Form, { FormControl } from '../ui/Form'; -import Input from '../ui/Input'; +} from '@chakra-ui/react' +import { zodResolver } from '@hookform/resolvers/zod' +import React, { useMemo, useRef } from 'react' +import { FieldValues, useForm } from 'react-hook-form' +import { z } from 'zod' + +import Button from '../ui/Button' +import Form, { FormControl } from '../ui/Form' +import Input from '../ui/Input' interface PagePopoverFormProps { - pos: number; - totalPages: number; - onPageChange: (page: number) => void; - trigger: React.ReactElement; + pos: number + totalPages: number + onPageChange: (page: number) => void + trigger: React.ReactElement } export default function PagePopoverForm({ @@ -33,40 +34,40 @@ export default function PagePopoverForm({ pos, trigger, }: PagePopoverFormProps) { - const inputRef = useRef(null); + const inputRef = useRef(null) - const { isOpen, onOpen, onClose } = useDisclosure(); + const { isOpen, onOpen, onClose } = useDisclosure() const schema = z.object({ goTo: z.string().refine( (val) => { - const num = parseInt(val, 10); + const num = parseInt(val, 10) - return num > 0 && num <= totalPages; + return num > 0 && num <= totalPages }, () => ({ message: `Please enter a number from 1 to ${totalPages}.`, }), ), - }); + }) const form = useForm({ resolver: zodResolver(schema), - }); + }) - const register = form.register('goTo'); + const register = form.register('goTo') const errors = useMemo(() => { - return form.formState.errors; - }, [form.formState.errors]); + return form.formState.errors + }, [form.formState.errors]) function handleSubmit(values: FieldValues) { if (values.goTo) { - onClose(); + onClose() setTimeout(() => { - form.reset(); - onPageChange(values.goTo); - }, 50); + form.reset() + onPageChange(values.goTo) + }, 50) } } @@ -97,8 +98,8 @@ export default function PagePopoverForm({ {...register} ref={(ref) => { if (ref) { - register.ref(ref); - inputRef.current = ref; + register.ref(ref) + inputRef.current = ref } }} /> @@ -123,5 +124,5 @@ export default function PagePopoverForm({ - ); + ) } diff --git a/common/interface/src/components/ServerStatusOverlay.tsx b/packages/interface/src/components/ServerStatusOverlay.tsx similarity index 65% rename from common/interface/src/components/ServerStatusOverlay.tsx rename to packages/interface/src/components/ServerStatusOverlay.tsx index 0af3b0605..be5cc1a18 100644 --- a/common/interface/src/components/ServerStatusOverlay.tsx +++ b/packages/interface/src/components/ServerStatusOverlay.tsx @@ -1,30 +1,30 @@ -import { Box, Heading, Text } from '@chakra-ui/react'; -import { useStumpStore } from '@stump/client'; -import { AnimatePresence, motion } from 'framer-motion'; -import { useEffect, useState } from 'react'; +import { Box, Heading, Text } from '@chakra-ui/react' +import { useStumpStore } from '@stump/client' +import { AnimatePresence, motion } from 'framer-motion' +import { useEffect, useState } from 'react' // FIXME: make this not as ugly lol export default function ServerStatusOverlay() { - const { connected } = useStumpStore(); - const [show, setShow] = useState(false); + const { connected } = useStumpStore(({ connected }) => ({ connected })) + const [show, setShow] = useState(false) useEffect(() => { - let timeout: NodeJS.Timeout; + let timer: NodeJS.Timer // after 4 seconds, if still !connected, show the overlay if (!connected) { - timeout = setTimeout(() => { + timer = setInterval(() => { if (!connected) { - setShow(true); + setShow(true) } - }, 4000); + }, 4000) } else if (connected) { - setShow(false); + setShow(false) } return () => { - clearTimeout(timeout); - }; - }, [connected]); + clearInterval(timer) + } + }, [connected]) return ( @@ -34,9 +34,9 @@ export default function ServerStatusOverlay() { bg={'white'} _dark={{ bg: 'gray.700' }} className="fixed right-[1rem] bottom-[1rem] rounded-md shadow p-2 flex flex-col justify-center items-center w-64" - initial={{ opacity: 0, y: 100, scale: 0.9 }} - animate={{ opacity: 1, y: 0, scale: 1 }} - exit={{ opacity: 0, y: 100, scale: 0.9 }} + initial={{ opacity: 0, scale: 0.9, y: 100 }} + animate={{ opacity: 1, scale: 1, y: 0 }} + exit={{ opacity: 0, scale: 0.9, y: 100 }} >
@@ -47,7 +47,7 @@ export default function ServerStatusOverlay() {
- Server isn't connected. + Server isn’t connected.
@@ -61,5 +61,5 @@ export default function ServerStatusOverlay() { )}
- ); + ) } diff --git a/common/interface/src/components/ServerUrlForm.tsx b/packages/interface/src/components/ServerUrlForm.tsx similarity index 59% rename from common/interface/src/components/ServerUrlForm.tsx rename to packages/interface/src/components/ServerUrlForm.tsx index c5341ef43..127b844ff 100644 --- a/common/interface/src/components/ServerUrlForm.tsx +++ b/packages/interface/src/components/ServerUrlForm.tsx @@ -1,8 +1,3 @@ -import { CloudCheck, CloudSlash } from 'phosphor-react'; -import { ChangeEvent, useMemo, useState } from 'react'; -import { FieldValues, useForm } from 'react-hook-form'; -import { z } from 'zod'; - import { FormErrorMessage, FormHelperText, @@ -11,20 +6,23 @@ import { InputRightElement, Spinner, useBoolean, -} from '@chakra-ui/react'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useStumpStore } from '@stump/client'; -import { checkUrl, isUrl } from '@stump/client/api'; - -import Form, { FormControl } from '../ui/Form'; -import { DebouncedInput } from '../ui/Input'; -import Button from '../ui/Button'; -import { useNavigate } from 'react-router'; +} from '@chakra-ui/react' +import { zodResolver } from '@hookform/resolvers/zod' +import { checkUrl, isUrl } from '@stump/api' +import { useStumpStore } from '@stump/client' +import { CloudCheck, CloudSlash } from 'phosphor-react' +import { ChangeEvent, useMemo, useState } from 'react' +import { FieldValues, useForm } from 'react-hook-form' +import { z } from 'zod' + +import Button from '../ui/Button' +import Form, { FormControl } from '../ui/Form' +import { DebouncedInput } from '../ui/Input' export default function ServerUrlForm() { - const { setBaseUrl } = useStumpStore(); - const [isCheckingUrl, { on, off }] = useBoolean(false); - const [sucessfulConnection, setSuccessfulConnection] = useState(false); + const { setBaseUrl } = useStumpStore(({ setBaseUrl }) => ({ setBaseUrl })) + const [isCheckingUrl, { on, off }] = useBoolean(false) + const [sucessfulConnection, setSuccessfulConnection] = useState(false) const schema = z.object({ baseUrl: z @@ -32,75 +30,75 @@ export default function ServerUrlForm() { .min(1, { message: 'URL is required' }) .refine(isUrl, { message: 'Invalid URL' }) .refine(checkUrl, (url) => ({ message: `Failed to connect to ${url}` })), - }); + }) const form = useForm({ - resolver: zodResolver(schema), mode: 'onSubmit', - }); + resolver: zodResolver(schema), + }) async function validateUrl() { - on(); - const url = form.getValues('baseUrl'); + on() + const url = form.getValues('baseUrl') if (!url) { - off(); - return; + off() + return } - let errorMessage: string; + let errorMessage: string // TODO: this function doesn't work lol if (!isUrl(url)) { - errorMessage = 'Invalid URL'; + errorMessage = 'Invalid URL' } else { - const isValid = await checkUrl(url); + const isValid = await checkUrl(url) if (!isValid) { - errorMessage = `Failed to connect to ${url}`; + errorMessage = `Failed to connect to ${url}` } else { - setSuccessfulConnection(true); + setSuccessfulConnection(true) } } setTimeout(() => { - off(); + off() if (errorMessage) { form.setError('baseUrl', { message: `Failed to connect to ${url}`, - }); + }) } - }, 300); + }, 300) } async function handleSubmit(values: FieldValues) { - const { baseUrl } = values; + const { baseUrl } = values - setBaseUrl(baseUrl); + setBaseUrl(baseUrl) // FIXME: super cringe, big no - window.location.href = '/'; + window.location.href = '/' } const InputDecoration = useMemo(() => { if (isCheckingUrl) { - return ; + return } else if (Object.keys(form.formState.errors).length > 0) { - return ; + return } else if (sucessfulConnection) { - return ; + return } - return null; - }, [isCheckingUrl, form.formState.errors, sucessfulConnection]); + return null + }, [isCheckingUrl, form.formState.errors, sucessfulConnection]) - const { onChange, ...register } = form.register('baseUrl'); + const { onChange, ...register } = form.register('baseUrl') function handleChange(e: ChangeEvent) { - setSuccessfulConnection(false); - form.clearErrors('baseUrl'); + setSuccessfulConnection(false) + form.clearErrors('baseUrl') - onChange(e); + onChange(e) } return ( @@ -120,10 +118,11 @@ export default function ServerUrlForm() { // TODO: remove ternary, yuck isCheckingUrl ? 'Testing connection...' - : !!InputDecoration + : InputDecoration ? 'Failed to connect!' : undefined } + // eslint-disable-next-line react/no-children-prop children={InputDecoration} /> @@ -132,7 +131,7 @@ export default function ServerUrlForm() { {sucessfulConnection && ( - Sucessfully connected to {form.getValues('baseUrl')}! + Successfully connected to {form.getValues('baseUrl')}! )} @@ -141,5 +140,5 @@ export default function ServerUrlForm() { Submit Form - ); + ) } diff --git a/common/interface/src/components/ShortcutToolTip.tsx b/packages/interface/src/components/ShortcutToolTip.tsx similarity index 71% rename from common/interface/src/components/ShortcutToolTip.tsx rename to packages/interface/src/components/ShortcutToolTip.tsx index 8515e72c2..c435c4920 100644 --- a/common/interface/src/components/ShortcutToolTip.tsx +++ b/packages/interface/src/components/ShortcutToolTip.tsx @@ -1,9 +1,10 @@ -import { HStack, Kbd } from '@chakra-ui/react'; -import ToolTip, { ToolTipProps } from '../ui/ToolTip'; +import { HStack, Kbd } from '@chakra-ui/react' + +import ToolTip, { ToolTipProps } from '../ui/ToolTip' interface ShortcutToolTipProps extends ToolTipProps { - shortcutAction?: string; - keybind: string[]; + shortcutAction?: string + keybind: string[] } export default function ShortcutToolTip({ @@ -19,15 +20,15 @@ export default function ShortcutToolTip({ ))} - ); + ) if (shortcutAction) { label = (
{shortcutAction} · {label}
- ); + ) } - return ; + return } diff --git a/packages/interface/src/components/SlidingCardList.tsx b/packages/interface/src/components/SlidingCardList.tsx new file mode 100644 index 000000000..e3f6b9c26 --- /dev/null +++ b/packages/interface/src/components/SlidingCardList.tsx @@ -0,0 +1,156 @@ +import { ButtonGroup, Heading, Text } from '@chakra-ui/react' +import { defaultRangeExtractor, Range, useVirtualizer } from '@tanstack/react-virtual' +import clsx from 'clsx' +import { CaretLeft, CaretRight } from 'phosphor-react' +import { useCallback, useEffect, useRef } from 'react' + +import { IconButton } from '../ui/Button' +import ToolTip from '../ui/ToolTip' + +interface Props { + title?: string + emptyMessage?: string + cards: JSX.Element[] + onScrollEnd?: () => void + isLoadingNext?: boolean + hasNext?: boolean + hideIfEmpty?: boolean +} + +export default function SlidingCardList({ + cards, + onScrollEnd, + isLoadingNext, + hasNext, + title, + emptyMessage, + hideIfEmpty, +}: Props) { + const parentRef = useRef(null) + const visibleRef = useRef([0, 0]) + const columnVirtualizer = useVirtualizer({ + count: cards.length, + enableSmoothScroll: true, + estimateSize: () => 350, + + getScrollElement: () => parentRef.current, + + horizontal: true, + // FIXME: this is an absurd overscan... needs to change, however I cannot get it to work with less + overscan: 75, + rangeExtractor: useCallback((range: Range) => { + visibleRef.current = [range.startIndex, range.endIndex] + return defaultRangeExtractor(range) + }, []), + }) + + useEffect( + () => { + const [lastItem] = [...columnVirtualizer.getVirtualItems()].reverse() + + if (!lastItem) { + return + } + + if (lastItem.index >= cards.length - 5 && hasNext && !isLoadingNext) { + onScrollEnd?.() + } + }, + + // eslint-disable-next-line react-hooks/exhaustive-deps + [hasNext, onScrollEnd, cards.length, isLoadingNext, columnVirtualizer.getVirtualItems().length], + ) + + const handleSkipAhead = (skipValue = 5) => { + let nextIndex = (visibleRef.current[1] ?? 5) + skipValue || 10 + + if (nextIndex > columnVirtualizer.getVirtualItems().length - 1) { + nextIndex = columnVirtualizer.getVirtualItems().length - 1 + } + + columnVirtualizer.scrollToIndex(nextIndex, { smoothScroll: true }) + } + + const handleSkipBackward = (skipValue = 5) => { + let nextIndex = (visibleRef?.current[0] ?? 0) - skipValue || 0 + + if (nextIndex < 0) { + nextIndex = 0 + } + + columnVirtualizer.scrollToIndex(nextIndex, { smoothScroll: true }) + } + + const [lowerBound, upperBound] = visibleRef.current + const canSkipBackward = (lowerBound ?? 0) > 0 + const canSkipForward = (upperBound ?? 0) > 0 && (upperBound ?? 0) * 2 < cards.length + + const virtualItems = columnVirtualizer.getVirtualItems() + const isEmpty = virtualItems.length === 0 + + // console.debug('SlidingCardList', title, { + // canSkipBackward, + // canSkipForward, + // cards, + // isEmpty, + // virtualItems, + // visibleBounds: visibleRef.current, + // }) + + const renderVirtualItems = () => { + if (isEmpty) { + return {emptyMessage || 'No items available'} + } else { + return columnVirtualizer.getVirtualItems().map((virtualItem) => { + return cards[virtualItem.index] + }) + } + } + + if (hideIfEmpty && isEmpty) { + return null + } + + return ( +
+
+ {title && {title}} +
+ + + handleSkipBackward()} + onDoubleClick={() => handleSkipBackward(20)} + > + + + + + handleSkipAhead()} + onDoubleClick={() => handleSkipAhead(20)} + > + + + + +
+
+
+
+ {renderVirtualItems()} +
+
+
+ ) +} diff --git a/common/interface/src/components/TagSelect.tsx b/packages/interface/src/components/TagSelect.tsx similarity index 71% rename from common/interface/src/components/TagSelect.tsx rename to packages/interface/src/components/TagSelect.tsx index d74fe51cf..012d0d52e 100644 --- a/common/interface/src/components/TagSelect.tsx +++ b/packages/interface/src/components/TagSelect.tsx @@ -1,15 +1,15 @@ -import { CreatableSelect } from 'chakra-react-select'; -import { FormControl, FormHelperText, FormLabel } from '@chakra-ui/react'; -import { Controller, useFormContext } from 'react-hook-form'; -import { TagOption } from '@stump/client'; +import { FormControl, FormHelperText, FormLabel, SystemStyleObject } from '@chakra-ui/react' +import { TagOption } from '@stump/client' +import { CreatableSelect } from 'chakra-react-select' +import { Controller, useFormContext } from 'react-hook-form' interface TagSelectProps { - name?: string; - label?: string; - options: TagOption[]; - defaultValue?: TagOption[]; - isLoading?: boolean; - hint?: string; + name?: string + label?: string + options: TagOption[] + defaultValue?: TagOption[] + isLoading?: boolean + hint?: string } export default function TagSelect({ @@ -20,7 +20,7 @@ export default function TagSelect({ isLoading, hint, }: TagSelectProps) { - const form = useFormContext(); + const form = useFormContext() return ( !options.length ? ( -

You haven't created any tags yet

+

You haven’t created any tags yet

) : (

Start typing to create a new tag

) @@ -64,19 +64,19 @@ export default function TagSelect({ focusBorderColor="0 0 0 2px rgba(196, 130, 89, 0.6);" // menuIsOpen chakraStyles={{ - control: (provided) => ({ + control: (provided: SystemStyleObject) => ({ ...provided, _focus: { boxShadow: '0 0 0 2px rgba(196, 130, 89, 0.6);', }, }), - dropdownIndicator: (provided) => ({ + dropdownIndicator: (provided: SystemStyleObject) => ({ ...provided, bg: 'transparent', - px: 2, cursor: 'inherit', + px: 2, }), - indicatorSeparator: (provided) => ({ + indicatorSeparator: (provided: SystemStyleObject) => ({ ...provided, display: 'none', }), @@ -87,5 +87,5 @@ export default function TagSelect({ )} /> - ); + ) } diff --git a/common/interface/src/components/UpdateAvailableOverlay.tsx b/packages/interface/src/components/UpdateAvailableOverlay.tsx similarity index 94% rename from common/interface/src/components/UpdateAvailableOverlay.tsx rename to packages/interface/src/components/UpdateAvailableOverlay.tsx index 097d5f3ef..05146d50a 100644 --- a/common/interface/src/components/UpdateAvailableOverlay.tsx +++ b/packages/interface/src/components/UpdateAvailableOverlay.tsx @@ -4,5 +4,5 @@ export default function UpdateAvailableOverlay() { // 2. server responds with latest versions of client and server, and if update is available // for either - return null; + return null } diff --git a/common/interface/src/components/files/FileExplorer.tsx b/packages/interface/src/components/files/FileExplorer.tsx similarity index 84% rename from common/interface/src/components/files/FileExplorer.tsx rename to packages/interface/src/components/files/FileExplorer.tsx index 201bc7a6e..134ecb39c 100644 --- a/common/interface/src/components/files/FileExplorer.tsx +++ b/packages/interface/src/components/files/FileExplorer.tsx @@ -1,8 +1,8 @@ -import { Text } from '@chakra-ui/react'; -import type { DirectoryListingFile } from '@stump/client'; +import { Text } from '@chakra-ui/react' +import type { DirectoryListingFile } from '@stump/types' interface FileExplorerProps { - files: DirectoryListingFile[]; + files: DirectoryListingFile[] } // TODO: this needs to be virtualized, as I am not paginating it like other lists/grids throughout Stump. @@ -15,24 +15,24 @@ export default function FileExplorer({ files }: FileExplorerProps) { ))}
- ); + ) } // Lol the name is just reversed... function ExplorerFile({ name, path, is_directory }: DirectoryListingFile) { function getIconSrc() { - const archivePattern = new RegExp(/^.*\.(cbz|zip|rar|cbr)$/gi); + const archivePattern = new RegExp(/^.*\.(cbz|zip|rar|cbr)$/gi) if (is_directory) { - return '/icons/folder.png'; + return '/assets/icons/folder.png' } else if (archivePattern.test(path)) { // TODO: no lol, I want to try and render a small preview still // will have to create a new endpoint to try and grab a thumbnail by path - return '/icons/archive.svg'; + return '/assets/icons/archive.svg' } else if (path.endsWith('.epub')) { - return '/icons/epub.svg'; + return '/assets/icons/epub.svg' } else { - return ''; + return '' } } @@ -44,5 +44,5 @@ function ExplorerFile({ name, path, is_directory }: DirectoryListingFile) { {name} - ); + ) } diff --git a/common/interface/src/components/jobs/JobOverlay.tsx b/packages/interface/src/components/jobs/JobOverlay.tsx similarity index 63% rename from common/interface/src/components/jobs/JobOverlay.tsx rename to packages/interface/src/components/jobs/JobOverlay.tsx index a184eb3e5..16320507c 100644 --- a/common/interface/src/components/jobs/JobOverlay.tsx +++ b/packages/interface/src/components/jobs/JobOverlay.tsx @@ -1,28 +1,33 @@ -import { useMemo } from 'react'; -import { AnimatePresence, motion } from 'framer-motion'; -import { Box, Progress, Text } from '@chakra-ui/react'; -import { useJobContext } from '@stump/client'; +import { Box, Progress, Text } from '@chakra-ui/react' +import { useJobContext } from '@stump/client' +import { AnimatePresence, motion } from 'framer-motion' export default function JobOverlay() { - const context = useJobContext(); + const context = useJobContext() if (!context) { - throw new Error('JobContextProvider not found'); + throw new Error('JobContextProvider not found') } - const { activeJobs } = context; + const { activeJobs } = context // get the first job that is running from the activeJobs object - const jobShown = Object.values(activeJobs).find((job) => job.status?.toLowerCase() === 'running'); + const jobShown = Object.values(activeJobs).find((job) => job.status?.toLowerCase() === 'running') function formatMessage(message?: string | null) { if (message?.startsWith('Analyzing')) { - let filePieces = message.replace(/"/g, '').split('Analyzing ').filter(Boolean)[0].split('/'); + const filePieces = message + .replace(/"/g, '') + .split('Analyzing ') + .filter(Boolean)[0] + ?.split('/') - return `Analyzing ${filePieces.slice(filePieces.length - 1).join('/')}`; + if (filePieces?.length) { + return `Analyzing ${filePieces.slice(filePieces.length - 1).join('/')}` + } } - return message; + return message } return ( @@ -33,9 +38,9 @@ export default function JobOverlay() { bg={'white'} _dark={{ bg: 'gray.700' }} className="fixed right-[1rem] bottom-[1rem] rounded-md shadow p-2 flex flex-col justify-center items-center w-52" - initial={{ opacity: 0, y: 100, scale: 0.9 }} - animate={{ opacity: 1, y: 0, scale: 1 }} - exit={{ opacity: 0, y: 100, scale: 0.9 }} + initial={{ opacity: 0, scale: 0.9, y: 100 }} + animate={{ opacity: 1, scale: 1, y: 0 }} + exit={{ opacity: 0, scale: 0.9, y: 100 }} >
{formatMessage(jobShown.message) ?? 'Job in Progress'} @@ -59,5 +64,5 @@ export default function JobOverlay() { )} - ); + ) } diff --git a/packages/interface/src/components/jobs/JobsTable.tsx b/packages/interface/src/components/jobs/JobsTable.tsx new file mode 100644 index 000000000..096af10d0 --- /dev/null +++ b/packages/interface/src/components/jobs/JobsTable.tsx @@ -0,0 +1,84 @@ +/* eslint-disable react/prop-types */ +import { useJobReport } from '@stump/client' +import type { JobReport, JobStatus } from '@stump/types' +import { ColumnDef, getCoreRowModel, getPaginationRowModel } from '@tanstack/react-table' +import dayjs from 'dayjs' +import { useMemo } from 'react' + +import Table from '../../ui/table/Table' +import { formatJobStatus, readableKind } from './utils' + +const IS_DEV = import.meta.env.DEV + +// FIXME: loading state +export default function JobsTable() { + const { jobReports } = useJobReport() + + // TODO: mobile columns less? or maybe scroll? idk what would be best UX + const columns = useMemo[]>( + () => [ + { + columns: [ + { + accessorKey: 'id', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + header: 'Job ID', + }, + { + accessorKey: 'kind', + cell: (info) => readableKind(info.getValue()), + footer: (props) => props.column.id, + header: 'Type', + }, + { + accessorKey: 'status', + // change value to all lowercase except for first letter + cell: (info) => formatJobStatus(info.getValue()), + + footer: (props) => props.column.id, + header: 'Status', + }, + // FIXME: I think sorting of this is backwards, because it is string sorting + // and this particular column needs to be sorted differently.... AGH + { + accessorKey: 'completed_at', + cell: (info) => { + const completed_at = info.getValue() + if (completed_at) { + return dayjs(completed_at).format('YYYY-MM-DD HH:mm:ss') + } + return undefined + }, + footer: (props) => props.column.id, + header: 'Time Completed', + }, + ], + id: 'jobHistory', + }, + ], + [], + ) + + return ( + + ) +} diff --git a/common/interface/src/components/jobs/RunningJobs.tsx b/packages/interface/src/components/jobs/RunningJobs.tsx similarity index 70% rename from common/interface/src/components/jobs/RunningJobs.tsx rename to packages/interface/src/components/jobs/RunningJobs.tsx index 33b5f0679..4073e76f7 100644 --- a/common/interface/src/components/jobs/RunningJobs.tsx +++ b/packages/interface/src/components/jobs/RunningJobs.tsx @@ -1,49 +1,51 @@ -import { useMemo } from 'react'; +import { Heading, Progress, Stack, Text, useColorModeValue, VStack } from '@chakra-ui/react' +import { cancelJob } from '@stump/api' +import { useJobContext } from '@stump/client' +import type { JobReport } from '@stump/types' +import { useMemo } from 'react' +import toast from 'react-hot-toast' -import { Heading, Progress, Stack, Text, useColorModeValue, VStack } from '@chakra-ui/react'; -import { useJobContext } from '@stump/client'; - -import type { JobReport } from '@stump/client'; -import { cancelJob } from '@stump/client/api'; -import Button from '../../ui/Button'; -import toast from 'react-hot-toast'; -import { readableKind } from './utils'; +import Button from '../../ui/Button' +import { readableKind } from './utils' function EmptyState({ message }: { message: string }) { return ( {message} - ); + ) } export function RunningJobs({ jobReports }: { jobReports: JobReport[] }) { - const context = useJobContext(); + const context = useJobContext() if (!context) { - throw new Error('JobContextProvider not found'); + throw new Error('JobContextProvider not found') } - const { activeJobs } = context; + const { activeJobs } = context const runningJobs = useMemo(() => { return jobReports .filter((job) => job.status === 'RUNNING' && job.id && activeJobs[job.id]) - .map((job) => ({ ...job, ...activeJobs[job.id!] })); - }, [activeJobs, jobReports]); + .map((job) => ({ ...job, ...activeJobs[job.id!] })) + }, [activeJobs, jobReports]) async function handleCancelJob(id: string | null) { if (id) { toast.promise(cancelJob(id), { + error: 'Failed to cancel job', loading: 'Cancelling job...', success: 'Job cancelled', - error: 'Failed to cancel job', - }); + }) } else { - console.debug('Tried to cancel job with no ID: ', runningJobs); + console.debug('Tried to cancel job with no ID: ', runningJobs) } } + const bgColor = useColorModeValue('whiteAlpha.600', 'blackAlpha.300') + const detailsColor = useColorModeValue('gray.600', 'gray.400') + return ( @@ -53,25 +55,15 @@ export function RunningJobs({ jobReports }: { jobReports: JobReport[] }) { {!runningJobs.length && } - {runningJobs.map((job) => { + {runningJobs.map((job, i) => { return ( - +
{readableKind(job.kind)} - + {job.details}
@@ -103,9 +95,9 @@ export function RunningJobs({ jobReports }: { jobReports: JobReport[] }) {
- ); + ) })}
- ); + ) } diff --git a/common/interface/src/components/jobs/utils.ts b/packages/interface/src/components/jobs/utils.ts similarity index 69% rename from common/interface/src/components/jobs/utils.ts rename to packages/interface/src/components/jobs/utils.ts index 304c7a197..9eeb646af 100644 --- a/common/interface/src/components/jobs/utils.ts +++ b/packages/interface/src/components/jobs/utils.ts @@ -1,16 +1,16 @@ -import { JobStatus } from '@stump/client'; +import { JobStatus } from '@stump/types' export function readableKind(kind: string | null) { if (!kind) { - return 'Unknown'; + return 'Unknown' } return kind .replace(/(Job|Jobs)$/, '') .replace(/([A-Z])/g, ' $1') - .replace(/^./, (str) => str.toUpperCase()); + .replace(/^./, (str) => str.toUpperCase()) } export function formatJobStatus(status: JobStatus) { - return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase(); + return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase() } diff --git a/common/interface/src/components/library/CreateLibraryModal.tsx b/packages/interface/src/components/library/CreateLibraryModal.tsx similarity index 74% rename from common/interface/src/components/library/CreateLibraryModal.tsx rename to packages/interface/src/components/library/CreateLibraryModal.tsx index 459b7511d..684ca0c18 100644 --- a/common/interface/src/components/library/CreateLibraryModal.tsx +++ b/packages/interface/src/components/library/CreateLibraryModal.tsx @@ -1,6 +1,3 @@ -import { FieldValues } from 'react-hook-form'; -import toast from 'react-hot-toast'; - import { Modal, ModalBody, @@ -9,85 +6,88 @@ import { ModalHeader, ModalOverlay, useDisclosure, -} from '@chakra-ui/react'; -import { useLibraryMutation, useTags } from '@stump/client'; - -import Button, { ModalCloseButton } from '../../ui/Button'; -import LibraryModalForm from './form/LibraryModalForm'; +} from '@chakra-ui/react' +import type { ApiResult } from '@stump/api' +import type { TagOption } from '@stump/client' +import { useLibraryMutation, useTags } from '@stump/client' +import type { LibraryOptions, Tag } from '@stump/types' +import { FieldValues } from 'react-hook-form' +import toast from 'react-hot-toast' -import type { ApiResult, LibraryOptions, Tag, TagOption } from '@stump/client'; +import Button, { ModalCloseButton } from '../../ui/Button' +import LibraryModalForm from './form/LibraryModalForm' interface Props { - trigger?: (props: any) => JSX.Element; - disabled?: boolean; + trigger?: (props: { onClick(): void }) => JSX.Element + disabled?: boolean } export default function CreateLibraryModal({ disabled, ...props }: Props) { - const { isOpen, onOpen, onClose } = useDisclosure(); + const { isOpen, onOpen, onClose } = useDisclosure() - const { tags, options, isLoading: fetchingTags, createTagsAsync: tryCreateTags } = useTags(); + const { tags, options, isLoading: fetchingTags, createTagsAsync: tryCreateTags } = useTags() const { createIsLoading, createLibraryAsync: createLibrary } = useLibraryMutation({ - onCreated: onClose, onCreateFailed(res) { - toast.error('Failed to create library.'); - console.error(res); + toast.error('Failed to create library.') + console.error(res) }, + onCreated: onClose, onError(err) { - toast.error('Failed to create library.'); - console.error(err); + toast.error('Failed to create library.') + console.error(err) }, - }); + }) // /Users/aaronleopold/Documents/Stump/Demo async function handleSubmit(values: FieldValues) { if (disabled) { // This is extra protection, should never happen. Making it an error so it is // easier to find on the chance it does. - throw new Error('You do not have permission to create libraries.'); + throw new Error('You do not have permission to create libraries.') } - const { name, path, description, tags: formTags, scan_mode, ...library_options } = values; + const { name, path, description, tags: formTags, scan_mode, ...library_options } = values // console.log({ name, path, description, tags: formTags, scan_mode, library_options }); - let existingTags = tags.filter((tag) => formTags?.some((t: TagOption) => t.value === tag.name)); + let existingTags = tags.filter((tag) => formTags?.some((t: TagOption) => t.value === tag.name)) - let tagsToCreate = formTags + const tagsToCreate = formTags ?.map((tag: TagOption) => tag.value) - .filter((tagName: string) => !existingTags.some((t) => t.name === tagName)); + .filter((tagName: string) => !existingTags.some((t) => t.name === tagName)) if (tagsToCreate && tagsToCreate.length > 0) { - const res: ApiResult = await tryCreateTags(tagsToCreate); + const res: ApiResult = await tryCreateTags(tagsToCreate) if (res.status > 201) { - toast.error('Something went wrong when creating the tags.'); - return; + toast.error('Something went wrong when creating the tags.') + return } - existingTags = existingTags.concat(res.data); + existingTags = existingTags.concat(res.data) } toast.promise( createLibrary({ + description, + library_options: library_options as LibraryOptions, name, path, - description, - tags: existingTags, scan_mode, - library_options: library_options as LibraryOptions, + tags: existingTags, }), { + error: 'Something went wrong.', loading: 'Creating library...', success: 'Library created!', - error: 'Something went wrong.', }, - ); + ) } function handleOpen() { if (!disabled) { - onOpen(); + onOpen() } } @@ -104,9 +104,9 @@ export default function CreateLibraryModal({ disabled, ...props }: Props) { size="sm" color={{ _dark: 'gray.200', _light: 'gray.600' }} _hover={{ - color: 'gray.900', - bg: 'gray.50', _dark: { bg: 'gray.700', color: 'gray.100' }, + bg: 'gray.50', + color: 'gray.900', }} fontSize="sm" fontWeight={'medium'} @@ -152,5 +152,5 @@ export default function CreateLibraryModal({ disabled, ...props }: Props) { - ); + ) } diff --git a/common/interface/src/components/library/DeleteLibraryModal.tsx b/packages/interface/src/components/library/DeleteLibraryModal.tsx similarity index 64% rename from common/interface/src/components/library/DeleteLibraryModal.tsx rename to packages/interface/src/components/library/DeleteLibraryModal.tsx index 809786f47..b68d8ff0b 100644 --- a/common/interface/src/components/library/DeleteLibraryModal.tsx +++ b/packages/interface/src/components/library/DeleteLibraryModal.tsx @@ -1,7 +1,3 @@ -import { Trash } from 'phosphor-react'; -import toast from 'react-hot-toast'; -import { useNavigate } from 'react-router-dom'; - import { MenuItem, Modal, @@ -11,46 +7,55 @@ import { ModalHeader, ModalOverlay, useDisclosure, -} from '@chakra-ui/react'; -import { useLibraryMutation } from '@stump/client'; - -import Button, { ModalCloseButton } from '../../ui/Button'; +} from '@chakra-ui/react' +import { queryClient, useLibraryMutation } from '@stump/client' +import type { Library } from '@stump/types' +import { Trash } from 'phosphor-react' +import toast from 'react-hot-toast' +import { useNavigate } from 'react-router-dom' -import type { Library } from '@stump/client'; +import Button, { ModalCloseButton } from '../../ui/Button' interface Props { - disabled?: boolean; - library: Library; + disabled?: boolean + library: Library } // TODO: custom tabs, active state is atrocious export default function DeleteLibraryModal({ disabled, library }: Props) { - const navigate = useNavigate(); + const navigate = useNavigate() - const { isOpen, onOpen, onClose } = useDisclosure(); + const { isOpen, onOpen, onClose } = useDisclosure() const { deleteLibraryAsync } = useLibraryMutation({ onDeleted() { - onClose(); - navigate('/'); + // TODO: these just needs to be wrapped around some utility at this point with + // how often I'm doing it + queryClient.invalidateQueries(['getLibrariesStats']) + queryClient.invalidateQueries(['getSeries']) + queryClient.invalidateQueries(['getRecentlyAddedSeries']) + queryClient.invalidateQueries(['getRecentlyAddedMedia']) + + onClose() + navigate('/') }, - }); + }) function handleDelete() { if (disabled) { // This should never happen, but here just in case - throw new Error('You do not have permission to delete libraries.'); + throw new Error('You do not have permission to delete libraries.') } else { toast.promise(deleteLibraryAsync(library.id), { + error: 'Error Deleting Library', loading: 'Deleting Library...', success: 'Library Deleted!', - error: 'Error Deleting Library', - }); + }) } } function handleOpen() { if (!disabled) { - onOpen(); + onOpen() } } @@ -87,5 +92,5 @@ export default function DeleteLibraryModal({ disabled, library }: Props) { - ); + ) } diff --git a/common/interface/src/components/library/EditLibraryModal.tsx b/packages/interface/src/components/library/EditLibraryModal.tsx similarity index 71% rename from common/interface/src/components/library/EditLibraryModal.tsx rename to packages/interface/src/components/library/EditLibraryModal.tsx index 6501210b7..ec1ffc56e 100644 --- a/common/interface/src/components/library/EditLibraryModal.tsx +++ b/packages/interface/src/components/library/EditLibraryModal.tsx @@ -1,7 +1,3 @@ -import { NotePencil } from 'phosphor-react'; -import { FieldValues } from 'react-hook-form'; -import toast from 'react-hot-toast'; - import { MenuItem, Modal, @@ -11,105 +7,108 @@ import { ModalHeader, ModalOverlay, useDisclosure, -} from '@chakra-ui/react'; -import { useLibraryMutation, useTags } from '@stump/client'; - -import Button, { ModalCloseButton } from '../../ui/Button'; -import LibraryModalForm from './form/LibraryModalForm'; - -import type { Library, LibraryOptions, Tag, TagOption } from '@stump/client'; +} from '@chakra-ui/react' +import type { TagOption } from '@stump/client' +import { useLibraryMutation, useTags } from '@stump/client' +import type { Library, LibraryOptions, Tag } from '@stump/types' +import { NotePencil } from 'phosphor-react' +import { FieldValues } from 'react-hook-form' +import toast from 'react-hot-toast' + +import Button, { ModalCloseButton } from '../../ui/Button' +import LibraryModalForm from './form/LibraryModalForm' interface Props { - library: Library; - disabled?: boolean; + library: Library + disabled?: boolean } // FIXME: tab navigation not working export default function EditLibraryModal({ disabled, library }: Props) { - const { isOpen, onOpen, onClose } = useDisclosure(); + const { isOpen, onOpen, onClose } = useDisclosure() - const { tags, options, isLoading: fetchingTags, createTagsAsync: tryCreateTags } = useTags(); + const { tags, options, isLoading: fetchingTags, createTagsAsync: tryCreateTags } = useTags() const { editIsLoading, editLibraryAsync: editLibrary } = useLibraryMutation({ - onUpdated: onClose, onError(err) { - console.error(err); - toast.error('Failed to edit library.'); + console.error(err) + toast.error('Failed to edit library.') }, - }); + onUpdated: onClose, + }) function getRemovedTags(tags: TagOption[]): Tag[] | null { // All tags were removed, or no tags were there to begin with if (tags.length === 0) { - return library.tags || null; + return library.tags || null } if (!library.tags || library.tags.length === 0) { - return null; + return null } // Some tags were removed, but not all - return library.tags.filter((tag) => !tags.some((tagOption) => tagOption.value === tag.name)); + return library.tags.filter((tag) => !tags.some((tagOption) => tagOption.value === tag.name)) } async function handleSubmit(values: FieldValues) { if (disabled) { // This is extra protection, should never happen. Making it an error so it is // easier to find on the chance it does. - throw new Error('You do not have permission to update libraries.'); + throw new Error('You do not have permission to update libraries.') } - const { name, path, description, tags: formTags, scan_mode, ...rest } = values; + const { name, path, description, tags: formTags, scan_mode, ...rest } = values const library_options = { ...rest, id: library.library_options.id, - } as LibraryOptions; + } as LibraryOptions - let existingTags = tags.filter((tag) => formTags.some((t: TagOption) => t.value === tag.name)); + let existingTags = tags.filter((tag) => formTags.some((t: TagOption) => t.value === tag.name)) - let tagsToCreate = formTags + const tagsToCreate = formTags .map((tag: TagOption) => tag.value) - .filter((tagName: string) => !existingTags.some((t) => t.name === tagName)); + .filter((tagName: string) => !existingTags.some((t) => t.name === tagName)) - let removedTags = getRemovedTags(formTags); + let removedTags = getRemovedTags(formTags) if (!removedTags?.length) { - removedTags = null; + removedTags = null } if (tagsToCreate.length) { - const res = await tryCreateTags(tagsToCreate); + const res = await tryCreateTags(tagsToCreate) if (res.status > 201) { - toast.error('Something went wrong when creating the tags.'); - return; + toast.error('Something went wrong when creating the tags.') + return } - existingTags = existingTags.concat(res.data); + existingTags = existingTags.concat(res.data) } toast.promise( editLibrary({ ...library, + description, + library_options, name, path, - description, - tags: existingTags, removed_tags: removedTags, scan_mode, - library_options, + tags: existingTags, }), { + error: 'Something went wrong.', loading: 'Updating library...', success: 'Updates saved!', - error: 'Something went wrong.', }, - ); + ) } function handleOpen() { if (!disabled) { - onOpen(); + onOpen() } } @@ -156,5 +155,5 @@ export default function EditLibraryModal({ disabled, library }: Props) { - ); + ) } diff --git a/common/interface/src/components/library/LibrariesStats.tsx b/packages/interface/src/components/library/LibrariesStats.tsx similarity index 54% rename from common/interface/src/components/library/LibrariesStats.tsx rename to packages/interface/src/components/library/LibrariesStats.tsx index 06771f02e..f03a9566a 100644 --- a/common/interface/src/components/library/LibrariesStats.tsx +++ b/packages/interface/src/components/library/LibrariesStats.tsx @@ -1,19 +1,20 @@ -import { useMemo } from 'react'; -import { HStack } from '@chakra-ui/react'; -import { useLibraryStats, useQueryParamStore } from '@stump/client'; -import { formatBytesSeparate } from '../../utils/format'; -import AnimatedStat from '../../ui/AnimatedStat'; +import { HStack } from '@chakra-ui/react' +import { useLibraryStats } from '@stump/client' +import { useMemo } from 'react' + +import AnimatedStat from '../../ui/AnimatedStat' +import { formatBytesSeparate } from '../../utils/format' // Note: I don't ~love~ the plural here, but I want to make sure it is understood it // encompasses *all* libraries, not just one. export default function LibrariesStats() { - const { libraryStats } = useLibraryStats(); + const { libraryStats } = useLibraryStats() const libraryUsage = useMemo(() => { - return formatBytesSeparate(libraryStats?.total_bytes); - }, [libraryStats?.total_bytes]); + return formatBytesSeparate(libraryStats?.total_bytes) + }, [libraryStats?.total_bytes]) - if (!libraryStats || !libraryUsage) return null; + if (!libraryStats || !libraryUsage) return null return ( @@ -26,5 +27,5 @@ export default function LibrariesStats() { decimal={true} /> - ); + ) } diff --git a/packages/interface/src/components/library/LibraryOptionsMenu.tsx b/packages/interface/src/components/library/LibraryOptionsMenu.tsx new file mode 100644 index 000000000..71875546e --- /dev/null +++ b/packages/interface/src/components/library/LibraryOptionsMenu.tsx @@ -0,0 +1,78 @@ +import { Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@chakra-ui/react' +import { queryClient, useScanLibrary, useUserStore } from '@stump/client' +import type { Library } from '@stump/types' +import { ArrowsClockwise, Binoculars, DotsThreeVertical } from 'phosphor-react' +import { useNavigate } from 'react-router-dom' + +import DeleteLibraryModal from './DeleteLibraryModal' +import EditLibraryModal from './EditLibraryModal' + +interface Props { + library: Library +} + +export default function LibraryOptionsMenu({ library }: Props) { + const navigate = useNavigate() + + const user = useUserStore((store) => store.user) + + const { scanAsync } = useScanLibrary() + + function handleScan() { + // extra protection, should not be possible to reach this. + if (user?.role !== 'SERVER_OWNER') { + throw new Error('You do not have permission to scan libraries.') + } + + // The UI will receive updates from SSE in fractions of ms lol and it can get bogged down. + // So, add a slight delay so the close animation of the menu can finish cleanly. + setTimeout(async () => { + await scanAsync(library.id) + await queryClient.invalidateQueries(['getJobReports']) + }, 50) + } + + // FIXME: so, disabled on the MenuItem doesn't seem to actually work... how cute. + return ( + // TODO: https://chakra-ui.com/docs/theming/customize-theme#customizing-component-styles +
+ + + + + {/* TODO: scanMode */} + } + onClick={handleScan} + > + Scan + + + } + onClick={() => navigate(`libraries/${library.id}/explorer`)} + > + File Explorer + + + + + +
+ ) +} diff --git a/common/interface/src/components/library/NoLibraries.tsx b/packages/interface/src/components/library/NoLibraries.tsx similarity index 58% rename from common/interface/src/components/library/NoLibraries.tsx rename to packages/interface/src/components/library/NoLibraries.tsx index a827a0844..54ac748cc 100644 --- a/common/interface/src/components/library/NoLibraries.tsx +++ b/packages/interface/src/components/library/NoLibraries.tsx @@ -1,13 +1,13 @@ -import React from 'react'; -import { Heading, Stack, Text } from '@chakra-ui/react'; -import CreateLibraryModal from './CreateLibraryModal'; +import { Heading, Stack, Text } from '@chakra-ui/react' + +import CreateLibraryModal from './CreateLibraryModal' export default function NoLibraries() { return ( - You don't have any libraries configured + You don’t have any libraries configured - That's okay! To get started, click{' '} + That’s okay! To get started, click{' '} ( @@ -18,5 +18,5 @@ export default function NoLibraries() { to add your first one. - ); + ) } diff --git a/common/interface/src/components/library/form/LibraryModalForm.tsx b/packages/interface/src/components/library/form/LibraryModalForm.tsx similarity index 81% rename from common/interface/src/components/library/form/LibraryModalForm.tsx rename to packages/interface/src/components/library/form/LibraryModalForm.tsx index 4d3ea8d98..a10e9e6bf 100644 --- a/common/interface/src/components/library/form/LibraryModalForm.tsx +++ b/packages/interface/src/components/library/form/LibraryModalForm.tsx @@ -1,7 +1,3 @@ -import { useEffect, useMemo } from 'react'; -import { FieldValues, useForm, useFormContext } from 'react-hook-form'; -import { z } from 'zod'; - import { FormErrorMessage, FormLabel, @@ -12,26 +8,29 @@ import { TabPanel, TabPanels, Tabs, -} from '@chakra-ui/react'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useLibraries } from '@stump/client'; - -import Checkbox from '../../../ui/Checkbox'; -import Form, { FormControl } from '../../../ui/Form'; -import Input from '../../../ui/Input'; -import { Tab } from '../../../ui/Tabs'; -import TextArea from '../../../ui/TextArea'; -import DirectoryPickerModal from '../../DirectoryPickerModal'; -import TagSelect from '../../TagSelect'; -import { LibraryPatternRadio } from './LibraryPatternRadio'; - -import type { TagOption, Library, LibraryPattern, LibraryScanMode } from '@stump/client'; +} from '@chakra-ui/react' +import { zodResolver } from '@hookform/resolvers/zod' +import type { TagOption } from '@stump/client' +import { useLibraries } from '@stump/client' +import type { Library, LibraryPattern, LibraryScanMode } from '@stump/types' +import { useEffect, useMemo, useRef } from 'react' +import { FieldValues, useForm } from 'react-hook-form' +import { z } from 'zod' + +import Checkbox from '../../../ui/Checkbox' +import Form, { FormControl } from '../../../ui/Form' +import Input from '../../../ui/Input' +import { Tab } from '../../../ui/Tabs' +import TextArea from '../../../ui/TextArea' +import DirectoryPickerModal from '../../DirectoryPickerModal' +import TagSelect from '../../TagSelect' +import { LibraryPatternRadio } from './LibraryPatternRadio' interface Props { - tags: TagOption[]; - onSubmit(values: FieldValues): void; - fetchingTags?: boolean; - reset?: boolean; - library?: Library; + tags: TagOption[] + onSubmit(values: FieldValues): void + fetchingTags?: boolean + reset?: boolean + library?: Library } /** @@ -41,25 +40,30 @@ interface Props { // FIXME: tab focus is not working, e.g. when you press tab, it should go to the next form element // TODO: I think this should be broken up... it's getting a bit big export default function LibraryModalForm({ tags, onSubmit, fetchingTags, reset, library }: Props) { - const { libraries } = useLibraries(); + const { libraries } = useLibraries() function isLibraryScanMode(input: string): input is LibraryScanMode { - return input === 'SYNC' || input === 'BATCHED' || input === 'NONE' || !input; + return input === 'SYNC' || input === 'BATCHED' || input === 'NONE' || !input } function isLibraryPattern(input: string): input is LibraryPattern { - return input === 'SERIES_BASED' || input === 'COLLECTION_BASED' || !input; + return input === 'SERIES_BASED' || input === 'COLLECTION_BASED' || !input } function getNewScanMode(value: string) { if (value === scanMode) { - return 'NONE'; + return 'NONE' } - return value; + return value } const schema = z.object({ + convert_rar_to_zip: z.boolean().default(false), + create_webp_thumbnails: z.boolean().default(false), + description: z.string().nullable(), + hard_delete_conversions: z.boolean().default(false), + library_pattern: z.string().refine(isLibraryPattern).default('SERIES_BASED'), name: z .string() .min(1, { message: 'Library name is required' }) @@ -82,7 +86,7 @@ export default function LibraryModalForm({ tags, onSubmit, fetchingTags, reset, message: 'Invalid library, parent directory already exists as library.', }), ), - description: z.string().nullable(), + scan_mode: z.string().refine(isLibraryScanMode).default('BATCHED'), tags: z .array( z.object({ @@ -92,44 +96,40 @@ export default function LibraryModalForm({ tags, onSubmit, fetchingTags, reset, // z.any(), ) .optional(), - scan_mode: z.string().refine(isLibraryScanMode).default('BATCHED'), - library_pattern: z.string().refine(isLibraryPattern).default('SERIES_BASED'), - convert_rar_to_zip: z.boolean().default(false), - hard_delete_conversions: z.boolean().default(false), - create_webp_thumbnails: z.boolean().default(false), - }); + }) const form = useForm({ - resolver: zodResolver(schema), defaultValues: library ? { - name: library.name, - path: library.path, - description: library.description, - tags: library.tags?.map((t) => ({ label: t.name, value: t.name })), convert_rar_to_zip: library.library_options.convert_rar_to_zip, - hard_delete_conversions: library.library_options.hard_delete_conversions, create_webp_thumbnails: library.library_options.create_webp_thumbnails, + description: library.description, + hard_delete_conversions: library.library_options.hard_delete_conversions, library_pattern: library.library_options.library_pattern, + name: library.name, + path: library.path, scan_mode: 'BATCHED', + tags: library.tags?.map((t) => ({ label: t.name, value: t.name })), } : {}, - }); + resolver: zodResolver(schema), + }) // TODO: maybe check if each error has a message? then if not, log it for // debugging purposes. const errors = useMemo(() => { - return form.formState.errors; - }, [form.formState.errors]); + return form.formState.errors + }, [form.formState.errors]) // const convertRarToZip = form.watch('convertRarToZip'); - const [scanMode, convertRarToZip] = form.watch(['scan_mode', 'convert_rar_to_zip']); + const [scanMode, convertRarToZip] = form.watch(['scan_mode', 'convert_rar_to_zip']) + const resetRef = useRef(form.reset) useEffect(() => { if (reset) { - form.reset(); + resetRef.current() } - }, [reset]); + }, [reset]) return (
{/* FIXME: on small breakpoints, paths are visible behind element */} - form.setValue('path', newPath ?? undefined)} - /> - } - /> + + form.setValue('path', newPath ?? undefined)} + /> + {!!errors.path && {errors.path?.message}} @@ -177,7 +171,7 @@ export default function LibraryModalForm({ tags, onSubmit, fetchingTags, reset, ({ value: t.name, label: t.name }))} + defaultValue={library?.tags?.map((t) => ({ label: t.name, value: t.name }))} /> @@ -255,5 +249,5 @@ export default function LibraryModalForm({ tags, onSubmit, fetchingTags, reset, - ); + ) } diff --git a/common/interface/src/components/library/form/LibraryPatternRadio.tsx b/packages/interface/src/components/library/form/LibraryPatternRadio.tsx similarity index 68% rename from common/interface/src/components/library/form/LibraryPatternRadio.tsx rename to packages/interface/src/components/library/form/LibraryPatternRadio.tsx index 4f17e71a0..d7dc6640b 100644 --- a/common/interface/src/components/library/form/LibraryPatternRadio.tsx +++ b/packages/interface/src/components/library/form/LibraryPatternRadio.tsx @@ -6,51 +6,57 @@ import { HStack, Text, useColorModeValue, -} from '@chakra-ui/react'; -import type { Library, LibraryPattern } from '@stump/client'; -import { useEffect } from 'react'; -import { useFormContext } from 'react-hook-form'; -import Link from '../../../ui/Link'; +} from '@chakra-ui/react' +import type { Library, LibraryPattern } from '@stump/types' +import { useEffect } from 'react' +import { useFormContext } from 'react-hook-form' interface Option { - label: string; - value: LibraryPattern; - description: string; + label: string + value: LibraryPattern + description: string } const options: Option[] = [ { + description: "Creates series from the bottom-most level of the library's directory.", label: 'Series Based', value: 'SERIES_BASED', - description: "Creates series from the bottom-most level of the library's directory.", }, { + description: "Creates series from the top-most level of the library's directory", label: 'Collection Based', value: 'COLLECTION_BASED', - description: "Creates series from the top-most level of the library's directory", }, -]; +] interface LibraryPatternRadioProps { - library?: Library; + library?: Library } // Wow, radio groups with chakra is absolutely terrible. Just another // reason to switch to tailwind full time. // TODO: After 0.1.0 is released, I'm going to start working on a new UI. export function LibraryPatternRadio({ library }: LibraryPatternRadioProps) { - const form = useFormContext(); + const form = useFormContext() + const boxColor = useColorModeValue('whiteAlpha.100', 'blackAlpha.100') + const labelColor = useColorModeValue('gray.500', 'gray.300') + const descriptionColor = useColorModeValue('gray.600', 'gray.400') - const libraryPattern: LibraryPattern = form.watch('library_pattern'); + const libraryPattern: LibraryPattern = form.watch('library_pattern') - const disabled = !!library; + const disabled = !!library // TODO: fix zod form, I should not have to do this. - useEffect(() => { - if (!libraryPattern) { - form.setValue('library_pattern', library?.library_options.library_pattern || 'SERIES_BASED'); - } - }, []); + useEffect( + () => { + if (!libraryPattern) { + form.setValue('library_pattern', library?.library_options.library_pattern || 'SERIES_BASED') + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ) return ( Click here {' '} @@ -81,11 +88,11 @@ export function LibraryPatternRadio({ library }: LibraryPatternRadioProps) { {options.map(({ label, value, description }) => { - const isChecked = libraryPattern === value; + const isChecked = libraryPattern === value return ( form.setValue('library_pattern', value)} > - + {label} - + {description} - ); + ) })} - ); + ) } diff --git a/packages/interface/src/components/media/BooksAfter.tsx b/packages/interface/src/components/media/BooksAfter.tsx new file mode 100644 index 000000000..abf001ba2 --- /dev/null +++ b/packages/interface/src/components/media/BooksAfter.tsx @@ -0,0 +1,27 @@ +import { useMediaCursor } from '@stump/client' +import { Media } from '@stump/types' + +import SlidingCardList from '../SlidingCardList' +import MediaCard from './MediaCard' + +type Props = { + media: Media +} +export default function BooksAfter({ media }: Props) { + const { media: data, isLoading, hasMore, fetchMore } = useMediaCursor(media.id, media.series_id) + if (isLoading || !data) { + return null + } + + return ( + ( + + ))} + isLoadingNext={isLoading} + hasNext={hasMore} + onScrollEnd={fetchMore} + /> + ) +} diff --git a/packages/interface/src/components/media/ContinueReading.tsx b/packages/interface/src/components/media/ContinueReading.tsx new file mode 100644 index 000000000..73cd5a990 --- /dev/null +++ b/packages/interface/src/components/media/ContinueReading.tsx @@ -0,0 +1,25 @@ +import { useContinueReading } from '@stump/client' + +import SlidingCardList from '../SlidingCardList' +import MediaCard from './MediaCard' + +// TODO: better empty state +export default function ContinueReadingMedia() { + const { data, isLoading, hasMore, fetchMore } = useContinueReading() + + if (isLoading || !data) { + return null + } + + return ( + ( + + ))} + isLoadingNext={isLoading} + hasNext={hasMore} + onScrollEnd={fetchMore} + /> + ) +} diff --git a/packages/interface/src/components/media/MediaCard.tsx b/packages/interface/src/components/media/MediaCard.tsx new file mode 100644 index 000000000..cf582cfa2 --- /dev/null +++ b/packages/interface/src/components/media/MediaCard.tsx @@ -0,0 +1,72 @@ +import { Progress, Text, useColorModeValue } from '@chakra-ui/react' +import { getMediaThumbnail } from '@stump/api' +import { prefetchMedia } from '@stump/client' +import type { Media } from '@stump/types' + +import pluralizeStat from '../../utils/pluralize' +import { prefetchMediaPage } from '../../utils/prefetch' +import Card, { CardBody, CardFooter } from '../Card' + +export type MediaCardProps = { + media: Media + // changes the card link to go directly to a reader, rather than overview page + readingLink?: boolean + // Used on the home page to set the height/width of the card for the sliding flex layout + fixed?: boolean +} + +export default function MediaCard({ media, readingLink, fixed }: MediaCardProps) { + const pagesLeft = media.current_page ? media.pages - media.current_page : undefined + const link = readingLink + ? `/books/${media.id}/pages/${media.current_page ?? 1}` + : `/books/${media.id}` + + function handleMouseOver() { + prefetchMedia(media.id) + + if (media.current_page) { + prefetchMediaPage(media.id, media.current_page) + } + } + + return ( + + + {!!pagesLeft && pagesLeft !== media.pages && ( +
+ +
+ )} +
+ + {/* TODO: figure out how to make this not look like shit with 2 lines */} + + {media.name} + + + + {pluralizeStat('pages', media.pages)} + + +
+ ) +} diff --git a/packages/interface/src/components/media/MediaGrid.tsx b/packages/interface/src/components/media/MediaGrid.tsx new file mode 100644 index 000000000..03461f7f1 --- /dev/null +++ b/packages/interface/src/components/media/MediaGrid.tsx @@ -0,0 +1,32 @@ +import { Heading } from '@chakra-ui/react' +import type { Media } from '@stump/types' + +import { CardGrid } from '../Card' +import MediaCard from './MediaCard' + +interface Props { + isLoading: boolean + media?: Media[] +} + +export default function MediaGrid({ media, isLoading }: Props) { + if (isLoading) { + return
Loading...
+ } else if (!media || !media.length) { + return ( +
+ {/* TODO: If I take in pageData, I can determine if it is an out of bounds issue or if the series truly has + no media. */} + It doesn’t look like there is any media here. +
+ ) + } + + return ( + + {media.map((m) => ( + + ))} + + ) +} diff --git a/common/interface/src/components/media/MediaList.tsx b/packages/interface/src/components/media/MediaList.tsx similarity index 68% rename from common/interface/src/components/media/MediaList.tsx rename to packages/interface/src/components/media/MediaList.tsx index c4a14954a..6e1ef6873 100644 --- a/common/interface/src/components/media/MediaList.tsx +++ b/packages/interface/src/components/media/MediaList.tsx @@ -1,16 +1,17 @@ -import type { Media } from '@stump/client'; -import ListItem from '../ListItem'; +import type { Media } from '@stump/types' + +import ListItem from '../ListItem' interface Props { - isLoading: boolean; - media?: Media[]; + isLoading: boolean + media?: Media[] } export default function MediaList({ media, isLoading }: Props) { if (isLoading) { - return
Loading...
; + return
Loading...
} else if (!media) { - return
whoop
; + return
whoop
} return ( @@ -26,5 +27,5 @@ export default function MediaList({ media, isLoading }: Props) { /> ))} - ); + ) } diff --git a/packages/interface/src/components/media/RecentlyAddedMedia.tsx b/packages/interface/src/components/media/RecentlyAddedMedia.tsx new file mode 100644 index 000000000..ab1c90c20 --- /dev/null +++ b/packages/interface/src/components/media/RecentlyAddedMedia.tsx @@ -0,0 +1,25 @@ +import { useRecentlyAddedMedia } from '@stump/client' + +import SlidingCardList from '../SlidingCardList' +import MediaCard from './MediaCard' + +// TODO: better empty state +export default function RecentlyAddedMedia() { + const { data, isLoading, hasMore, fetchMore } = useRecentlyAddedMedia() + + if (isLoading || !data) { + return null + } + + return ( + ( + + ))} + isLoadingNext={isLoading} + hasNext={hasMore} + onScrollEnd={fetchMore} + /> + ) +} diff --git a/common/interface/src/components/media/Toolbar.tsx b/packages/interface/src/components/media/Toolbar.tsx similarity index 77% rename from common/interface/src/components/media/Toolbar.tsx rename to packages/interface/src/components/media/Toolbar.tsx index fbd8df42a..5b5499d5d 100644 --- a/common/interface/src/components/media/Toolbar.tsx +++ b/packages/interface/src/components/media/Toolbar.tsx @@ -1,29 +1,23 @@ -import { AnimatePresence, motion } from 'framer-motion'; -import { ArrowLeft } from 'phosphor-react'; -import { Link, useParams } from 'react-router-dom'; -import { Heading } from '@chakra-ui/react'; -import { getMediaPage } from '@stump/client/api'; +import { Heading } from '@chakra-ui/react' +import { getMediaPage } from '@stump/api' +import { AnimatePresence, motion } from 'framer-motion' +import { ArrowLeft } from 'phosphor-react' +import { Link, useParams } from 'react-router-dom' interface ToolbarProps { - title: string; - currentPage: number; - pages: number; - visible: boolean; - onPageChange(page: number): void; + title: string + currentPage: number + pages: number + visible: boolean + onPageChange(page: number): void } -export default function Toolbar({ - title, - currentPage, - pages, - visible, - onPageChange, -}: ToolbarProps) { - const { id } = useParams(); +export default function Toolbar({ title, pages, visible, onPageChange }: ToolbarProps) { + const { id } = useParams() if (!id) { // should never happen - throw new Error('woah boy how strange 0.o'); + throw new Error('woah boy how strange 0.o') } return ( @@ -77,5 +71,5 @@ export default function Toolbar({ )} - ); + ) } diff --git a/common/interface/src/components/onboarding/ServerURLInput.tsx b/packages/interface/src/components/onboarding/ServerURLInput.tsx similarity index 55% rename from common/interface/src/components/onboarding/ServerURLInput.tsx rename to packages/interface/src/components/onboarding/ServerURLInput.tsx index bee86cdf4..bbf337906 100644 --- a/common/interface/src/components/onboarding/ServerURLInput.tsx +++ b/packages/interface/src/components/onboarding/ServerURLInput.tsx @@ -1,14 +1,17 @@ -import { InputGroup, InputRightElement } from '@chakra-ui/react'; -import { FormControl } from '../../ui/Form'; -import Input from '../../ui/Input'; +import { InputGroup, InputRightElement } from '@chakra-ui/react' +import { FormControl } from '../../ui/Form' +import Input from '../../ui/Input' + +// TODO: type this +// eslint-disable-next-line @typescript-eslint/no-explicit-any export default function ServerURLInput(props: any) { return ( - } /> + - ); + ) } diff --git a/common/interface/src/components/readers/EpubReader.tsx b/packages/interface/src/components/readers/EpubReader.tsx similarity index 67% rename from common/interface/src/components/readers/EpubReader.tsx rename to packages/interface/src/components/readers/EpubReader.tsx index 05a929b83..c79fd6278 100644 --- a/common/interface/src/components/readers/EpubReader.tsx +++ b/packages/interface/src/components/readers/EpubReader.tsx @@ -1,7 +1,9 @@ -import { UseEpubReturn } from '@stump/client'; -import { getEpubResource } from '@stump/client/api'; -import React, { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ +// FIXME: this file is a mess +import { getEpubResource } from '@stump/api' +import { UseEpubReturn } from '@stump/client' +import React, { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' /* NOTE: I have decided to move this streamable epub reading to a future feature. @@ -20,7 +22,7 @@ import { useNavigate } from 'react-router-dom'; Some of this has been started, but not finished. */ export default function EpubReader({ epub, actions, ...rest }: UseEpubReturn) { - const navigate = useNavigate(); + const navigate = useNavigate() // const { isLoading: isFetchingResource, data: content } = useQuery( // ['getEbubResource', actions.currentResource()], @@ -29,27 +31,33 @@ export default function EpubReader({ epub, actions, ...rest }: UseEpubReturn) { // }, // ); - const [content, setContent] = useState(); + const [content, setContent] = useState() - useEffect(() => { - getEpubResource({ - id: epub.media_entity.id, - root: epub.root_base, - resourceId: actions.currentResource()?.content!, - }).then((res) => { - console.log(res); + useEffect( + () => { + getEpubResource({ + id: epub.media_entity.id, + resourceId: actions.currentResource()?.content!, + root: epub.root_base, + }).then((res) => { + console.debug(res) - setContent(rest.correctHtmlUrls(res.data)); - }); - }, []); + // FIXME: don't cast + setContent(rest.correctHtmlUrls(res.data as string)) + }) + }, + + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ) function handleClickEvent(e: React.MouseEvent) { if (e.target instanceof HTMLAnchorElement && e.target.href) { - e.preventDefault(); + e.preventDefault() // Note: I am assuming at this point I have a valid href. // i.e. the epub link has been canonicalized and points to a valid // epubcfi. - navigate(e.target.href); + navigate(e.target.href) } } @@ -57,5 +65,5 @@ export default function EpubReader({ epub, actions, ...rest }: UseEpubReturn) {
{content &&
}
- ); + ) } diff --git a/common/interface/src/components/readers/ImageBasedReader.tsx b/packages/interface/src/components/readers/ImageBasedReader.tsx similarity index 53% rename from common/interface/src/components/readers/ImageBasedReader.tsx rename to packages/interface/src/components/readers/ImageBasedReader.tsx index bc8fc1740..1e2f2ba6d 100644 --- a/common/interface/src/components/readers/ImageBasedReader.tsx +++ b/packages/interface/src/components/readers/ImageBasedReader.tsx @@ -1,19 +1,19 @@ -import { motion, useAnimation, useMotionValue, useTransform } from 'framer-motion'; -import React, { useEffect, useMemo, useRef } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useWindowSize } from 'rooks'; - -import { useBoolean } from '@chakra-ui/react'; - -import Toolbar from './utils/Toolbar'; - -import type { Media } from '@stump/client'; +import { useBoolean } from '@chakra-ui/react' +import { queryClient } from '@stump/client' +import type { Media } from '@stump/types' +import clsx from 'clsx' +import { motion, useAnimation, useMotionValue, useTransform } from 'framer-motion' +import React, { useEffect, useMemo, useRef } from 'react' +import { useHotkeys } from 'react-hotkeys-hook' +import { useWindowSize } from 'rooks' + +import Toolbar from './utils/Toolbar' export interface ImageBasedReaderProps { - currentPage: number; - media: Media; + currentPage: number + media: Media - onPageChange: (page: number) => void; - getPageUrl(page: number): string; + onPageChange: (page: number) => void + getPageUrl(page: number): string } // TODO: merge with AnimatedImageBasedReader once animates aren't ass @@ -24,48 +24,59 @@ export default function ImageBasedReader({ onPageChange, getPageUrl, }: ImageBasedReaderProps) { - const currPageRef = React.useRef(currentPage); + const currPageRef = React.useRef(currentPage) - const [toolbarVisible, { toggle: toggleToolbar, off: hideToolbar }] = useBoolean(false); + const [toolbarVisible, { toggle: toggleToolbar, off: hideToolbar }] = useBoolean(false) // TODO: is this enough? - useEffect(() => { - const pageArray = Array.from({ length: media.pages }); + useEffect( + () => { + const pageArray = Array.from({ length: media.pages }) + + const start = currentPage >= 1 ? currentPage - 1 : 0 - let start = currentPage >= 1 ? currentPage - 1 : 0; + pageArray.slice(start, 3).forEach((_, i) => { + const preloadedImg = new Image() + preloadedImg.src = getPageUrl(currentPage + (i + 1)) + }) + }, - pageArray.slice(start, 3).forEach((_, i) => { - const preloadedImg = new Image(); - preloadedImg.src = getPageUrl(currentPage + (i + 1)); - }); - }, [currentPage, media.pages]); + // eslint-disable-next-line react-hooks/exhaustive-deps + [currentPage, media.pages], + ) useEffect(() => { - currPageRef.current = currentPage; - }, [currentPage]); + currPageRef.current = currentPage + }, [currentPage]) + + useEffect(() => { + return () => { + queryClient.invalidateQueries(['getInProgressMedia']) + } + }, []) function handlePageChange(newPage: number) { if (newPage < media.pages && newPage > 0) { - onPageChange(newPage); + onPageChange(newPage) } } useHotkeys('right, left, space, esc', (_, handler) => { switch (handler.key) { case 'right': - handlePageChange(currPageRef.current + 1); - break; + handlePageChange(currPageRef.current + 1) + break case 'left': - handlePageChange(currPageRef.current - 1); - break; + handlePageChange(currPageRef.current - 1) + break case 'space': - toggleToolbar(); - break; + toggleToolbar() + break case 'esc': - hideToolbar(); - break; + hideToolbar() + break } - }); + }) return (
@@ -76,54 +87,64 @@ export default function ImageBasedReader({ visible={toolbarVisible} onPageChange={handlePageChange} /> - -
onPageChange(currentPage - 1)} - /> - + onPageChange(currentPage - 1)} /> { - // @ts-ignore - err.target.src = '/favicon.png'; + // @ts-expect-error: is oke + err.target.src = '/favicon.png' }} onClick={toggleToolbar} /> - -
onPageChange(currentPage + 1)} - /> + onPageChange(currentPage + 1)} />
- ); + ) +} + +type SideBarControlProps = { + onClick: () => void + position: 'left' | 'right' +} +function SideBarControl({ onClick, position }: SideBarControlProps) { + return ( +
+ ) } const RESET_CONTROLS = { x: '0%', -}; +} const FORWARD_START_ANIMATION = { - x: '-200%', transition: { duration: 0.35, }, -}; + x: '-200%', +} const TO_CENTER_ANIMATION = { - x: 0, transition: { duration: 0.35, }, -}; + x: 0, +} const BACKWARD_START_ANIMATION = { - x: '200%', transition: { duration: 0.35, }, -}; + x: '200%', +} // FIXME: its much better overall, Works very well on portrait images, still a little stuttering // when moving between images of different aspect ratios. (e.g. portrait vs landscape). A little @@ -137,105 +158,109 @@ export function AnimatedImageBasedReader({ onPageChange, getPageUrl, }: ImageBasedReaderProps) { - const { innerWidth } = useWindowSize(); + const { innerWidth } = useWindowSize() - const prevRef = useRef(null); - const nextRef = useRef(null); + const prevRef = useRef(null) + const nextRef = useRef(null) - const x = useMotionValue(0); + const x = useMotionValue(0) const nextX = useTransform(x, (latest) => { if (nextRef.current) { // Only time this will happen is when no motion is happening, or a swipe right // to go to previous page is happening. if (latest >= 0) { - return latest; + return latest } - const center = (innerWidth ?? 0) / 2; - const imageWidth = nextRef.current.width; + const center = (innerWidth ?? 0) / 2 + const imageWidth = nextRef.current.width - const imageCenter = imageWidth / 2; + const imageCenter = imageWidth / 2 - const centerPosition = center + imageCenter; + const centerPosition = center + imageCenter // latest will be 0 at the start, and go negative as we swipe // left. if (Math.abs(latest) >= centerPosition) { - return -centerPosition; + return -centerPosition } } - return latest; - }); + return latest + }) const prevX = useTransform(x, (latest) => { if (prevRef.current) { // Only time this will happen is when no motion is happening, or a swipe left // to go to next page is happening. if (latest <= 0) { - return latest; + return latest } - const center = (innerWidth ?? 0) / 2; - const imageWidth = prevRef.current.width; + const center = (innerWidth ?? 0) / 2 + const imageWidth = prevRef.current.width - const imageCenter = imageWidth / 2; + const imageCenter = imageWidth / 2 - const centerPosition = center + imageCenter; + const centerPosition = center + imageCenter // latest will be 0 at the start, and go positive as we swipe // left. if (latest >= centerPosition) { - return centerPosition; + return centerPosition } } - return latest; - }); + return latest + }) - const controls = useAnimation(); - const nextControls = useAnimation(); - const prevControls = useAnimation(); + const controls = useAnimation() + const nextControls = useAnimation() + const prevControls = useAnimation() - const [toolbarVisible, { toggle: toggleToolbar, off: hideToolbar }] = useBoolean(false); + const [toolbarVisible, { toggle: toggleToolbar, off: hideToolbar }] = useBoolean(false) // This is for the hotkeys - const currPageRef = React.useRef(currentPage); + const currPageRef = React.useRef(currentPage) useEffect(() => { - currPageRef.current = currentPage; - }, [currentPage]); - - const imageUrls = useMemo(() => { - let urls = []; - - // if has previous - if (currentPage > 1) { - urls.push(getPageUrl(currentPage - 1)); - } else { - urls.push(undefined); - } + currPageRef.current = currentPage + }, [currentPage]) + + const imageUrls = useMemo( + () => { + const urls = [] + + // if has previous + if (currentPage > 1) { + urls.push(getPageUrl(currentPage - 1)) + } else { + urls.push(undefined) + } - urls.push(getPageUrl(currentPage)); + urls.push(getPageUrl(currentPage)) - // if has next - if (currentPage < media.pages) { - urls.push(getPageUrl(currentPage + 1)); - } else { - urls.push(undefined); - } + // if has next + if (currentPage < media.pages) { + urls.push(getPageUrl(currentPage + 1)) + } else { + urls.push(undefined) + } - return urls; - }, [currentPage]); + return urls + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [currentPage], + ) function startNextPageAnimation() { Promise.all([ controls.start(FORWARD_START_ANIMATION), nextControls.start(TO_CENTER_ANIMATION), ]).then(() => { - onPageChange(currPageRef.current + 1); - }); + onPageChange(currPageRef.current + 1) + }) } function startPrevPageAnimation() { @@ -243,40 +268,44 @@ export function AnimatedImageBasedReader({ controls.start(BACKWARD_START_ANIMATION), prevControls.start(TO_CENTER_ANIMATION), ]).then(() => { - onPageChange(currPageRef.current - 1); - }); + onPageChange(currPageRef.current - 1) + }) } - useEffect(() => { - controls.set(RESET_CONTROLS); - nextControls.set({ left: '100%' }); - prevControls.set({ right: '100%' }); - }, [currentPage]); + useEffect( + () => { + controls.set(RESET_CONTROLS) + nextControls.set({ left: '100%' }) + prevControls.set({ right: '100%' }) + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [currentPage], + ) function handleHotKeyPagination(direction: 'next' | 'prev') { if (direction === 'next' && currPageRef.current < media.pages) { - startNextPageAnimation(); + startNextPageAnimation() } else if (direction === 'prev' && currPageRef.current > 1) { - startPrevPageAnimation(); + startPrevPageAnimation() } } useHotkeys('right, left, space, esc', (_, handler) => { switch (handler.key) { case 'right': - handleHotKeyPagination('next'); - break; + handleHotKeyPagination('next') + break case 'left': - handleHotKeyPagination('prev'); - break; + handleHotKeyPagination('prev') + break case 'space': - toggleToolbar(); - break; + toggleToolbar() + break case 'esc': - hideToolbar(); - break; + hideToolbar() + break } - }); + }) return (
@@ -285,7 +314,7 @@ export function AnimatedImageBasedReader({ currentPage={currentPage} pages={media.pages} visible={toolbarVisible} - onPageChange={(_) => alert('TODO;')} + onPageChange={() => alert('TODO;')} /> {imageUrls[0] && ( @@ -297,8 +326,8 @@ export function AnimatedImageBasedReader({ className="absolute w-full max-h-full md:w-auto" src={imageUrls[0]} onError={(err) => { - // @ts-ignore - err.target.src = '/src/favicon.png'; + // @ts-expect-error: is oke + err.target.src = '/src/favicon.png' }} /> )} @@ -309,12 +338,12 @@ export function AnimatedImageBasedReader({ dragElastic={1} dragConstraints={{ left: 0, right: 0 }} onDragEnd={(_e, info) => { - const { velocity, offset } = info; + const { velocity, offset } = info if ((velocity.x <= -200 || offset.x <= -300) && currPageRef.current < media.pages) { - startNextPageAnimation(); + startNextPageAnimation() } else if ((velocity.x >= 200 || offset.x >= 300) && currPageRef.current > 1) { - startPrevPageAnimation(); + startPrevPageAnimation() } }} transition={{ ease: 'easeOut' }} @@ -322,8 +351,8 @@ export function AnimatedImageBasedReader({ className="absolute w-full max-h-full md:w-auto z-30" src={imageUrls[1]} onError={(err) => { - // @ts-ignore - err.target.src = '/favicon.png'; + // @ts-expect-error: is oke + err.target.src = '/favicon.png' }} // TODO: figure this out, I can't do this anymore with the drag... // onClick={toggleToolbar} @@ -338,11 +367,11 @@ export function AnimatedImageBasedReader({ className="absolute w-full max-h-full md:w-auto" src={imageUrls[2]} onError={(err) => { - // @ts-ignore - err.target.src = '/favicon.png'; + // @ts-expect-error: is oke + err.target.src = '/favicon.png' }} /> )}
- ); + ) } diff --git a/packages/interface/src/components/readers/LazyEpubReader.tsx b/packages/interface/src/components/readers/LazyEpubReader.tsx new file mode 100644 index 000000000..772f541d7 --- /dev/null +++ b/packages/interface/src/components/readers/LazyEpubReader.tsx @@ -0,0 +1,288 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// FIXME: remove the two above once epub is more completed +import { useColorMode } from '@chakra-ui/react' +import { API } from '@stump/api' +import { queryClient, useEpubLazy } from '@stump/client' +import { Book, Rendition } from 'epubjs' +import { useEffect, useMemo, useRef, useState } from 'react' +import toast from 'react-hot-toast' +import { useSwipeable } from 'react-swipeable' + +import { epubDarkTheme } from '../../utils/epubTheme' +import EpubControls from './utils/EpubControls' + +// Color manipulation reference: https://github.com/futurepress/epub.js/issues/1019 + +/** + +looks like epubcfi generates the first two elements of the cfi like /6/{(index+1) * 2} (indexing non-zero based): + - index 1 /6/2, index=2 /6/4, index=3 /6/8 etc. + +can't figure out rest yet -> https://www.heliconbooks.com/?id=blog&postid=EPUB3Links +*/ + +interface LazyEpubReaderProps { + id: string + loc: string | null +} + +// TODO: https://github.com/FormidableLabs/react-swipeable#how-to-share-ref-from-useswipeable + +export default function LazyEpubReader({ id, loc }: LazyEpubReaderProps) { + const { colorMode } = useColorMode() + + const ref = useRef(null) + + const [book, setBook] = useState(null) + const [rendition, setRendition] = useState(null) + + const [location, setLocation] = useState({ epubcfi: loc }) + const [chapter, setChapter] = useState('') + const [fontSize, setFontSize] = useState(13) + + const { epub, isLoading } = useEpubLazy(id) + + // TODO: type me + function handleLocationChange(changeState: any) { + const start = changeState?.start + + if (!start) { + return + } + + const newChapter = controls.getChapter(start.href) + + if (newChapter) { + setChapter(newChapter) + } + + setLocation({ + // @ts-ignore: types are wrong >:( + epubcfi: start.cfi ?? null, + href: start.href, + index: start.index, + // @ts-ignore: types are wrong >:( + page: start.displayed?.page, + // @ts-ignore: types are wrong >:( + total: start.displayed?.total, + }) + } + + useEffect( + () => { + if (!ref.current) return + + if (!book) { + setBook( + new Book(`${API.getUri()}/media/${id}/file`, { + openAs: 'epub', + // @ts-ignore: more incorrect types >:( I really truly cannot stress enough how much I want to just + // rip out my eyes working with epubjs... + requestCredentials: true, + }), + ) + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ref], + ) + + // Note: not sure this is possible anymore? epub.js isn't maintained it seems, + // and I haven't figured this out yet. + // function pageAnimation(_iframeView: any, _rendition: Rendition) { + // // console.log('pageAnimation', { iframeView, _rendition }); + // // window.setTimeout(() => { + // // console.log('in pageAnimation timeout'); + // // }, 100); + // } + + useEffect( + () => { + if (!book) return + if (!ref.current) return + + book.ready.then(() => { + if (book.spine) { + const defaultLoc = book.rendition?.location?.start?.cfi + + const rendition_ = book.renderTo(ref.current!, { + height: '100%', + width: '100%', + }) + + // TODO more styles, probably separate this out + rendition_.themes.register('dark', epubDarkTheme) + + // book.spine.hooks.serialize // Section is being converted to text + // book.spine.hooks.content // Section has been loaded and parsed + // rendition.hooks.render // Section is rendered to the screen + // rendition.hooks.content // Section contents have been loaded + // rendition.hooks.unloaded // Section contents are being unloaded + // rendition_.hooks.render.register(pageAnimation) + + rendition_.on('relocated', handleLocationChange) + + if (colorMode === 'dark') { + rendition_.themes.select('dark') + } + + rendition_.themes.fontSize('13px') + + setRendition(rendition_) + + // Note: this *does* work, returns epubcfi. I might consider this... + // console.log(book.spine.get('chapter001.xhtml')); + + if (location?.epubcfi) { + rendition_.display(location.epubcfi) + } else if (defaultLoc) { + rendition_.display(defaultLoc) + } else { + rendition_.display() + } + } + }) + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [book], + ) + + useEffect(() => { + if (!rendition) { + return + } + + if (colorMode === 'dark') { + rendition.themes.select('dark') + } else { + rendition.themes.select('default') + } + }, [rendition, colorMode]) + + useEffect(() => { + return () => { + queryClient.invalidateQueries(['getInProgressMedia']) + } + }, []) + + // epubcfi(/6/10!/4/2/2[Chapter1]/48/1:0) + + // I hate this... + const controls = useMemo( + () => ({ + changeFontSize(size: number) { + if (rendition) { + setFontSize(size) + rendition.themes.fontSize(`${size}px`) + } + }, + + // Note: some books have entries in the spine for each href, some don't. This means for some + // books the chapter will be null after the first page of that chapter. This function is + // used to get the current chapter, which will only work, in some cases, on the first page + // of the chapter. The chapter state will only get updated when this function returns a non-null + // value. + getChapter(href: string): string | null { + if (book) { + const filteredToc = book.navigation.toc.filter((toc) => toc.href === href) + + return filteredToc[0]?.label.trim() ?? null + } + + return null + }, + + // FIXME: make async? I just need to programmatically detect failures so + // I don't close the TOC drawer. + goTo(href: string) { + if (!book || !rendition || !ref.current) { + return + } + + const adjusted = href.split('#')[0] + + let match = book.spine.get(adjusted) + + if (!match) { + // @ts-ignore: types are wrong >:( + // Note: epubjs it literally terrible and this should be classified as torture dealing + // with this terrible library. The fact that I have to do this really blows my mind. + const matches = book.spine.items + .filter((item: any) => { + const withPrefix = `/${adjusted}` + return ( + item.url === adjusted || + item.canonical == adjusted || + item.url === withPrefix || + item.canonical === withPrefix + ) + }) + .map((item: any) => book.spine.get(item.index)) + .filter(Boolean) + + if (matches.length > 0) { + match = matches[0] + } else { + console.error(`Could not find ${href}`) + return + } + } + + const epubcfi = match.cfiFromElement(ref.current) + + if (epubcfi) { + rendition.display(epubcfi) + } else { + toast.error('Could not generate a valid epubcfi.') + } + }, + + async next() { + if (rendition) { + await rendition + .next() + .then(() => { + // rendition.hooks.render.trigger(pageAnimation); + }) + .catch((err) => { + console.error(err) + toast.error('Something went wrong!') + }) + } + }, + + async prev() { + if (rendition) { + await rendition.prev().catch((err) => { + console.error(err) + toast.error('Something went wrong!') + }) + } + }, + }), + [rendition, book, ref], + ) + + const swipeHandlers = useSwipeable({ + onSwipedLeft: controls.next, + onSwipedRight: controls.prev, + preventScrollOnSwipe: true, + }) + + if (isLoading) { + return
Loading TODO.....
+ } + + return ( + +
+ + ) +} diff --git a/common/interface/src/components/readers/utils/EpubControls.tsx b/packages/interface/src/components/readers/utils/EpubControls.tsx similarity index 75% rename from common/interface/src/components/readers/utils/EpubControls.tsx rename to packages/interface/src/components/readers/utils/EpubControls.tsx index 5db50f528..2927fe718 100644 --- a/common/interface/src/components/readers/utils/EpubControls.tsx +++ b/packages/interface/src/components/readers/utils/EpubControls.tsx @@ -1,8 +1,3 @@ -import React from 'react'; -import { ArrowLeft, CaretLeft, CaretRight, MagnifyingGlass } from 'phosphor-react'; -import { useNavigate } from 'react-router-dom'; -import { SwipeableHandlers } from 'react-swipeable'; - import { Box, ButtonGroup, @@ -14,27 +9,33 @@ import { useColorModeValue, useDisclosure, VStack, -} from '@chakra-ui/react'; -import type { Epub } from '@stump/client'; +} from '@chakra-ui/react' +import type { Epub } from '@stump/types' +import { ArrowLeft, CaretLeft, CaretRight, MagnifyingGlass } from 'phosphor-react' +import React from 'react' +import { useNavigate } from 'react-router-dom' +import { SwipeableHandlers } from 'react-swipeable' -import Button, { IconButton } from '../../../ui/Button'; -import EpubTocDrawer from './EpubTocDrawer'; -import FontSelection from './FontSelection'; +import Button, { IconButton } from '../../../ui/Button' +import EpubTocDrawer from './EpubTocDrawer' +import FontSelection from './FontSelection' interface IEpubControls { - next(): Promise; - prev(): Promise; - goTo(href: string): void; - changeFontSize(size: number): void; + next(): Promise + prev(): Promise + goTo(href: string): void + changeFontSize(size: number): void } interface EpubControlsProps { - controls: IEpubControls; - fontSize: number; - swipeHandlers: SwipeableHandlers; - location: any; - children: React.ReactNode; - epub: Epub; + controls: IEpubControls + fontSize: number + swipeHandlers: SwipeableHandlers + // FIXME: type this + /* eslint-disable @typescript-eslint/no-explicit-any */ + location: any + children: React.ReactNode + epub: Epub } interface HeaderControlsProps @@ -48,15 +49,17 @@ function EpubHeaderControls({ epub, goTo, }: HeaderControlsProps) { - const navigate = useNavigate(); + const navigate = useNavigate() - const [visible, { on, off }] = useBoolean(false); + const [visible, { on, off }] = useBoolean(false) - const { isOpen, onOpen, onClose } = useDisclosure(); + const chapterColor = useColorModeValue('gray.700', 'gray.400') + const entityColor = useColorModeValue('gray.700', 'gray.200') + const { isOpen, onOpen, onClose } = useDisclosure() function handleMouseEnter() { if (!visible) { - on(); + on() } } @@ -64,8 +67,8 @@ function EpubHeaderControls({ if (visible && !isOpen) { setTimeout(() => { // TODO: need to check if still in div before shutting off - off(); - }, 500); + off() + }, 500) } } @@ -104,19 +107,11 @@ function EpubHeaderControls({ - + {epub.media_entity.name} {location.chapter && ( - + {location.chapter} )} @@ -132,7 +127,7 @@ function EpubHeaderControls({ - ); + ) } export default function EpubControls({ @@ -143,29 +138,29 @@ export default function EpubControls({ location, epub, }: EpubControlsProps) { - const [visibleNav, { on: showNav, off: hideNav }] = useBoolean(true); + const [visibleNav, { on: showNav, off: hideNav }] = useBoolean(true) - const { prev, next, changeFontSize } = controls; + const { prev, next, changeFontSize } = controls function handleMouseEnterNav() { if (!visibleNav) { - showNav(); + showNav() } } function handleMouseLeaveNav() { if (visibleNav) { - hideNav(); + hideNav() } } function handleTapEvent(e: React.MouseEvent) { // if tap is really close to right edge of screen, next page if (e.clientX > window.innerWidth - 75) { - next(); + next() } else if (e.clientX < 75) { // if tap is really close to left edge of screen, previous page - prev(); + prev() } } @@ -220,5 +215,5 @@ export default function EpubControls({
- ); + ) } diff --git a/common/interface/src/components/readers/utils/EpubTocDrawer.tsx b/packages/interface/src/components/readers/utils/EpubTocDrawer.tsx similarity index 61% rename from common/interface/src/components/readers/utils/EpubTocDrawer.tsx rename to packages/interface/src/components/readers/utils/EpubTocDrawer.tsx index e938ae962..c7c1003c8 100644 --- a/common/interface/src/components/readers/utils/EpubTocDrawer.tsx +++ b/packages/interface/src/components/readers/utils/EpubTocDrawer.tsx @@ -1,7 +1,3 @@ -import { ListBullets } from 'phosphor-react'; -import { useEffect, useRef } from 'react'; -import { useLocation } from 'react-router-dom'; - import { Box, Drawer, @@ -12,19 +8,22 @@ import { Stack, Text, useColorModeValue, -} from '@chakra-ui/react'; - -import { IconButton } from '../../../ui/Button'; + usePrevious, +} from '@chakra-ui/react' +import type { EpubContent } from '@stump/types' +import { ListBullets } from 'phosphor-react' +import { useEffect, useRef } from 'react' +import { useLocation } from 'react-router-dom' -import type { EpubContent } from '@stump/client'; +import { IconButton } from '../../../ui/Button' interface EpubTocDrawerProps { - isOpen: boolean; - onClose(): void; - onOpen(): void; + isOpen: boolean + onClose(): void + onOpen(): void // TODO: TYPE THESE, has to work both with epubjs and streaming epub engine (not built yet) - toc: EpubContent[]; - onSelect(tocItem: string): void; + toc: EpubContent[] + onSelect(tocItem: string): void } export default function EpubTocDrawer({ @@ -34,19 +33,20 @@ export default function EpubTocDrawer({ toc, onSelect, }: EpubTocDrawerProps) { - const location = useLocation(); + const location = useLocation() + const previousLocation = usePrevious(location) - const btnRef = useRef(null); + const btnRef = useRef(null) useEffect(() => { - if (isOpen) { - onClose(); + if (previousLocation?.pathname !== location.pathname && isOpen) { + onClose() } - }, [location]); + }, [location, previousLocation, isOpen, onClose]) function handleSelect(href: string) { - onSelect(href); - onClose(); + onSelect(href) + onClose() } return ( @@ -76,7 +76,11 @@ export default function EpubTocDrawer({ className="scrollbar-hide" > {toc?.map((item) => ( - handleSelect(item.content)}> + handleSelect(item.content)} + > {item.label} ))} @@ -84,5 +88,5 @@ export default function EpubTocDrawer({ - ); + ) } diff --git a/common/interface/src/components/readers/utils/FontSelection.tsx b/packages/interface/src/components/readers/utils/FontSelection.tsx similarity index 85% rename from common/interface/src/components/readers/utils/FontSelection.tsx rename to packages/interface/src/components/readers/utils/FontSelection.tsx index 8be34f1e8..1f7fd1af9 100644 --- a/common/interface/src/components/readers/utils/FontSelection.tsx +++ b/packages/interface/src/components/readers/utils/FontSelection.tsx @@ -8,13 +8,14 @@ import { Portal, Stack, Text, -} from '@chakra-ui/react'; -import { TextAa } from 'phosphor-react'; -import { IconButton } from '../../../ui/Button'; +} from '@chakra-ui/react' +import { TextAa } from 'phosphor-react' + +import { IconButton } from '../../../ui/Button' interface Props { - changeFontSize(size: number): void; - fontSize: number; + changeFontSize(size: number): void + fontSize: number } export default function FontSelection({ changeFontSize, fontSize }: Props) { @@ -56,5 +57,5 @@ export default function FontSelection({ changeFontSize, fontSize }: Props) { - ); + ) } diff --git a/packages/interface/src/components/readers/utils/Toolbar.tsx b/packages/interface/src/components/readers/utils/Toolbar.tsx new file mode 100644 index 000000000..b175ab702 --- /dev/null +++ b/packages/interface/src/components/readers/utils/Toolbar.tsx @@ -0,0 +1,147 @@ +import { Heading, useColorMode } from '@chakra-ui/react' +import { getMediaPage } from '@stump/api' +import { defaultRangeExtractor, Range, useVirtualizer } from '@tanstack/react-virtual' +import clsx from 'clsx' +import { motion } from 'framer-motion' +import { ArrowLeft } from 'phosphor-react' +import { useCallback, useEffect, useRef } from 'react' +import { Link, useParams } from 'react-router-dom' + +import ThemeToggle from '../../sidebar/ThemeToggle' + +interface ToolbarProps { + title: string + currentPage: number + pages: number + visible: boolean + onPageChange(page: number): void +} + +export default function Toolbar({ + title, + currentPage, + pages, + visible, + onPageChange, +}: ToolbarProps) { + const { id } = useParams() + + if (!id) { + // should never happen + throw new Error('No ID provided') + } + + const parentRef = useRef(null) + const rangeRef = useRef([0, 0]) + const columnVirtualizer = useVirtualizer({ + count: pages, + enableSmoothScroll: true, + estimateSize: () => 80, + getScrollElement: () => parentRef.current, + horizontal: true, + overscan: 5, + rangeExtractor: useCallback((range: Range) => { + rangeRef.current = [range.startIndex, range.endIndex] + return defaultRangeExtractor(range) + }, []), + }) + + // FIXME: this is super scufffed, something is throwing off the scrollToIndex and the + // workaround is atrocious... + useEffect( + () => { + if (visible) { + // FIXME: why no work + // columnVirtualizer.scrollToIndex(currentPage, { smoothScroll: true }); + setTimeout(() => { + const totalSize = columnVirtualizer.getTotalSize() + const offset = (totalSize / pages) * currentPage + + const targetID = `${id}-page-${currentPage}` + const target = document.getElementById(targetID) + + if (target) { + target.scrollIntoView({ behavior: 'smooth', inline: 'center' }) + } else { + // FIXME: this actually doesn't work lol + parentRef.current?.scrollTo({ behavior: 'smooth', left: offset }) + } + }, 50) + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [visible, currentPage], + ) + + const variants = (position: 'top' | 'bottom') => ({ + hidden: { + opacity: 0, + transition: { + duration: 0.2, + ease: 'easeInOut', + }, + y: position === 'top' ? '-100%' : '100%', + }, + visible: { + opacity: 1, + transition: { + duration: 0.2, + ease: 'easeInOut', + }, + y: 0, + }, + }) + + return ( +
+ +
+
+ + + + + {title} +
+
+ +
+
+
+ +
+ {/* TODO: tool tips? */} + {/* FIXME: styling isn't quite right, should have space on either side... */} + {columnVirtualizer.getVirtualItems().map((virtualItem, idx) => ( + onPageChange(idx + 1)} + /> + ))} +
+
+
+ ) +} diff --git a/common/interface/src/components/series/DownloadSeriesButton.tsx b/packages/interface/src/components/series/DownloadSeriesButton.tsx similarity index 59% rename from common/interface/src/components/series/DownloadSeriesButton.tsx rename to packages/interface/src/components/series/DownloadSeriesButton.tsx index 1dc29fd5d..93df6e557 100644 --- a/common/interface/src/components/series/DownloadSeriesButton.tsx +++ b/packages/interface/src/components/series/DownloadSeriesButton.tsx @@ -1,15 +1,17 @@ -import { CloudArrowDown } from 'phosphor-react'; -import { IconButton } from '../../ui/Button'; +import { CloudArrowDown } from 'phosphor-react' + +import { IconButton } from '../../ui/Button' interface Props { - seriesId: string; + seriesId: string } +// eslint-disable-next-line @typescript-eslint/no-unused-vars export default function DownloadSeriesButton({ seriesId }: Props) { return ( // title="Download series as ZIP archive" - ); + ) } diff --git a/packages/interface/src/components/series/RecentlyAddedSeries.tsx b/packages/interface/src/components/series/RecentlyAddedSeries.tsx new file mode 100644 index 000000000..f8b43d15f --- /dev/null +++ b/packages/interface/src/components/series/RecentlyAddedSeries.tsx @@ -0,0 +1,25 @@ +import { useRecentlyAddedSeries } from '@stump/client' + +import SlidingCardList from '../SlidingCardList' +import SeriesCard from './SeriesCard' + +// TODO: better empty state +export default function RecentlyAddedSeries() { + const { data, isLoading, hasMore, fetchMore } = useRecentlyAddedSeries() + + if (isLoading || !data) { + return null + } + + return ( + ( + + ))} + isLoadingNext={isLoading} + hasNext={hasMore} + onScrollEnd={fetchMore} + /> + ) +} diff --git a/packages/interface/src/components/series/SeriesCard.tsx b/packages/interface/src/components/series/SeriesCard.tsx new file mode 100644 index 000000000..664155d08 --- /dev/null +++ b/packages/interface/src/components/series/SeriesCard.tsx @@ -0,0 +1,56 @@ +import { Progress, Text, useColorModeValue } from '@chakra-ui/react' +import { getSeriesThumbnail } from '@stump/api' +import { prefetchSeries } from '@stump/client' +import { Series } from '@stump/types' + +import pluralizeStat from '../../utils/pluralize' +import Card, { CardBody, CardFooter } from '../Card' + +export type SeriesCardProps = { + series: Series + fixed?: boolean +} + +export default function SeriesCard({ series, fixed }: SeriesCardProps) { + const bookCount = Number(series.media ? series.media.length : series.media_count ?? 0) + const unreadCount = series.unread_media_count + + return ( + prefetchSeries(series.id)} + title={series.name} + > + + {!!unreadCount && Number(unreadCount) !== bookCount && ( +
+ +
+ )} +
+ + {/* TODO: figure out how to make this not look like shit with 2 lines */} + + {series.name} + + + + {pluralizeStat('book', Number(bookCount))} + + +
+ ) +} diff --git a/packages/interface/src/components/series/SeriesGrid.tsx b/packages/interface/src/components/series/SeriesGrid.tsx new file mode 100644 index 000000000..60bcf8771 --- /dev/null +++ b/packages/interface/src/components/series/SeriesGrid.tsx @@ -0,0 +1,33 @@ +import { Heading } from '@chakra-ui/react' +import type { Series } from '@stump/types' + +import { CardGrid } from '../Card' +import SeriesCard from './SeriesCard' + +interface Props { + isLoading: boolean + series?: Series[] +} + +// TODO: I think this *might* need a redesign... Not sure, gotta do some UX research about this +export default function SeriesGrid({ series, isLoading }: Props) { + if (isLoading) { + return
Loading...
+ } else if (!series || !series.length) { + return ( +
+ {/* TODO: If I take in pageData, I can determine if it is an out of bounds issue or if the series truly has + no media. */} + It doesn’t look like there are any series here. +
+ ) + } + + return ( + + {series.map((s) => ( + + ))} + + ) +} diff --git a/common/interface/src/components/series/SeriesList.tsx b/packages/interface/src/components/series/SeriesList.tsx similarity index 68% rename from common/interface/src/components/series/SeriesList.tsx rename to packages/interface/src/components/series/SeriesList.tsx index ad42f96e5..8b8b0e8d6 100644 --- a/common/interface/src/components/series/SeriesList.tsx +++ b/packages/interface/src/components/series/SeriesList.tsx @@ -1,25 +1,24 @@ -import { Heading } from '@chakra-ui/react'; +import { Heading } from '@chakra-ui/react' +import type { Series } from '@stump/types' -import ListItem from '../ListItem'; - -import type { Series } from '@stump/client'; +import ListItem from '../ListItem' interface Props { - isLoading: boolean; - series?: Series[]; + isLoading: boolean + series?: Series[] } export default function SeriesList({ series, isLoading }: Props) { if (isLoading) { - return
Loading...
; + return
Loading...
} else if (!series || !series.length) { return (
{/* TODO: If I take in pageData, I can determine if it is an out of bounds issue or if the series truly has no media. */} - It doesn't look like there are any series here. + It doesn’t look like there are any series here.
- ); + ) } return ( @@ -35,5 +34,5 @@ export default function SeriesList({ series, isLoading }: Props) { /> ))}
- ); + ) } diff --git a/packages/interface/src/components/series/UpNextInSeriesButton.tsx b/packages/interface/src/components/series/UpNextInSeriesButton.tsx new file mode 100644 index 000000000..c317dfcb7 --- /dev/null +++ b/packages/interface/src/components/series/UpNextInSeriesButton.tsx @@ -0,0 +1,32 @@ +import { useUpNextInSeries } from '@stump/client' +import { Link } from 'react-router-dom' + +import Button, { ButtonProps } from '../../ui/Button' + +type Props = { + seriesId: string + title?: string +} & ButtonProps + +export default function UpNextInSeriesButton({ seriesId, title, ...props }: Props) { + const { media, isLoading } = useUpNextInSeries(seriesId) + + // TODO: Change this once Stump supports epub progress tracking. + if (media?.extension === 'epub') { + return null + } + + return ( + + ) +} diff --git a/packages/interface/src/components/settings/LocaleSelector.tsx b/packages/interface/src/components/settings/LocaleSelector.tsx new file mode 100644 index 000000000..dba9e2abd --- /dev/null +++ b/packages/interface/src/components/settings/LocaleSelector.tsx @@ -0,0 +1,50 @@ +import { FormControl, FormLabel } from '@chakra-ui/react' +import { Select, SingleValue } from 'chakra-react-select' + +import { Locale, LocaleSelectOption, useLocale } from '../../hooks/useLocale' + +// FIXME: style is not aligned with theme, but I am lazy right now so future aaron problem +// TODO: use locale for labels +export default function LocaleSelector() { + const { locale, setLocale, locales } = useLocale() + + function handleLocaleChange(newLocale?: Locale) { + if (newLocale) { + setLocale(newLocale) + } + } + + return ( + + + Language / Locale + + + + + + + {t('settingsPage.general.profileForm.labels.password')} + + + + + + + + + ) +} diff --git a/packages/interface/src/components/settings/forms/index.ts b/packages/interface/src/components/settings/forms/index.ts new file mode 100644 index 000000000..9cd7eeb79 --- /dev/null +++ b/packages/interface/src/components/settings/forms/index.ts @@ -0,0 +1,2 @@ +export { default as UserPreferencesForm } from './PreferencesForm' +export { default as UserProfileForm } from './ProfileForm' diff --git a/common/interface/src/components/sidebar/Logout.tsx b/packages/interface/src/components/sidebar/Logout.tsx similarity index 70% rename from common/interface/src/components/sidebar/Logout.tsx rename to packages/interface/src/components/sidebar/Logout.tsx index dc09299f8..4fb95bd5a 100644 --- a/common/interface/src/components/sidebar/Logout.tsx +++ b/packages/interface/src/components/sidebar/Logout.tsx @@ -1,7 +1,3 @@ -import { SignOut } from 'phosphor-react'; -import toast from 'react-hot-toast'; -import { useNavigate } from 'react-router-dom'; - import { Modal, ModalBody, @@ -10,31 +6,34 @@ import { ModalHeader, ModalOverlay, useDisclosure, -} from '@chakra-ui/react'; -import { useUserStore } from '@stump/client'; -import { logout } from '@stump/client/api'; +} from '@chakra-ui/react' +import { logout } from '@stump/api' +import { useUserStore } from '@stump/client' +import { SignOut } from 'phosphor-react' +import toast from 'react-hot-toast' +import { useNavigate } from 'react-router-dom' -import Button, { ModalCloseButton } from '../../ui/Button'; -import ToolTip from '../../ui/ToolTip'; +import Button, { ModalCloseButton } from '../../ui/Button' +import ToolTip from '../../ui/ToolTip' export default function Logout() { - const { isOpen, onOpen, onClose } = useDisclosure(); + const { isOpen, onOpen, onClose } = useDisclosure() - const { setUser } = useUserStore(); + const setUser = useUserStore((store) => store.setUser) - const navigate = useNavigate(); + const navigate = useNavigate() async function handleLogout() { toast .promise(logout(), { + error: 'There was an error logging you out. Please try again.', loading: null, success: 'You have been logged out. Redirecting...', - error: 'There was an error logging you out. Please try again.', }) .then(() => { - setUser(null); - navigate('/auth'); - }); + setUser(null) + navigate('/auth') + }) } return ( @@ -74,5 +73,5 @@ export default function Logout() { - ); + ) } diff --git a/common/interface/src/components/sidebar/MobileDrawer.tsx b/packages/interface/src/components/sidebar/MobileDrawer.tsx similarity index 52% rename from common/interface/src/components/sidebar/MobileDrawer.tsx rename to packages/interface/src/components/sidebar/MobileDrawer.tsx index 026992f21..de74e998b 100644 --- a/common/interface/src/components/sidebar/MobileDrawer.tsx +++ b/packages/interface/src/components/sidebar/MobileDrawer.tsx @@ -1,34 +1,38 @@ -import { useEffect, useRef } from 'react'; import { Drawer, DrawerBody, + DrawerCloseButton, DrawerContent, DrawerOverlay, Stack, useColorModeValue, useDisclosure, -} from '@chakra-ui/react'; -import { List } from 'phosphor-react'; -import { useLocation } from 'react-router-dom'; -import { SidebarContent } from './Sidebar'; -import Button from '../../ui/Button'; + usePrevious, +} from '@chakra-ui/react' +import { List } from 'phosphor-react' +import { useEffect, useRef } from 'react' +import { useLocation } from 'react-router-dom' + +import Button from '../../ui/Button' +import { SidebarContent } from './Sidebar' export default function MobileDrawer() { - const location = useLocation(); + const { isOpen, onOpen, onClose } = useDisclosure() - const btnRef = useRef(null); - const { isOpen, onOpen, onClose } = useDisclosure(); + const location = useLocation() + const previousLocation = usePrevious(location) + const btnRef = useRef(null) useEffect(() => { - if (isOpen) { - onClose(); + if (previousLocation?.pathname !== location.pathname && isOpen) { + onClose() } - }, [location]); + }, [location, previousLocation, isOpen, onClose]) + // NOTE: this div supresses a popper.js warning about not being able to calculate margins return ( - <> +
+ ) } diff --git a/common/interface/src/components/sidebar/Sidebar.tsx b/packages/interface/src/components/sidebar/Sidebar.tsx similarity index 50% rename from common/interface/src/components/sidebar/Sidebar.tsx rename to packages/interface/src/components/sidebar/Sidebar.tsx index d1a6a8741..1340f3f48 100644 --- a/common/interface/src/components/sidebar/Sidebar.tsx +++ b/packages/interface/src/components/sidebar/Sidebar.tsx @@ -1,9 +1,5 @@ -import clsx from 'clsx'; -import { AnimatePresence } from 'framer-motion'; -import { Books, CaretRight, Gear, House } from 'phosphor-react'; -import { useMemo } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; - +/* eslint-disable @typescript-eslint/no-explicit-any */ +// TODO: remove this when I have time, fix the icon types import { Box, Button, @@ -13,36 +9,45 @@ import { useColorModeValue, useDisclosure, VStack, -} from '@chakra-ui/react'; -import { useLibraries } from '@stump/client'; - -import { useLocale } from '../../hooks/useLocale'; -import ApplicationVersion from '../ApplicationVersion'; -import CreateLibraryModal from '../library/CreateLibraryModal'; -import LibraryOptionsMenu from '../library/LibraryOptionsMenu'; -import NavigationButtons from '../topbar/NavigationButtons'; -import Logout from './Logout'; -import ThemeToggle from './ThemeToggle'; +} from '@chakra-ui/react' +import { refreshUseLibrary, useLibraries } from '@stump/client' +import type { Library } from '@stump/types' +import clsx from 'clsx' +import { AnimatePresence } from 'framer-motion' +import { Books, CaretRight, Gear, House } from 'phosphor-react' +import { useMemo } from 'react' +import { Link, useLocation, useNavigate } from 'react-router-dom' -import type { Library } from '@stump/client'; +import { useLocale } from '../../hooks/useLocale' +import ApplicationVersion from '../ApplicationVersion' +import CreateLibraryModal from '../library/CreateLibraryModal' +import LibraryOptionsMenu from '../library/LibraryOptionsMenu' +import NavigationButtons from '../topbar/NavigationButtons' +import Logout from './Logout' +import ThemeToggle from './ThemeToggle' interface NavMenuItemProps extends Library { - href: string; + active: boolean + href: string + onHover?: () => void } interface NavItemProps { - name: string; - icon: React.ReactNode; - onClick?: (href: string) => void; - href?: string; - items?: NavMenuItemProps[]; + name: string + icon: React.ReactNode + onClick?: (href: string) => void + href?: string + items?: NavMenuItemProps[] + active?: boolean } -function NavMenuItem({ name, items, onClick, ...rest }: NavItemProps) { - const { isOpen, onToggle } = useDisclosure(); +function NavMenuItem({ name, items, ...rest }: NavItemProps) { + const { isOpen, onToggle } = useDisclosure() + + const activeBgColor = useColorModeValue('gray.50', 'gray.750') return ( - + <>
- {/* @ts-ignore */} + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore: TODO: fixme */} {name}
@@ -69,13 +75,13 @@ function NavMenuItem({ name, items, onClick, ...rest }: NavItemProps) { {isOpen && ( - <> + - - {items!.map((item) => ( + + {items!.map(({ onHover, active, ...item }) => ( - + {item.name} @@ -97,14 +108,16 @@ function NavMenuItem({ name, items, onClick, ...rest }: NavItemProps) { ))} - + )} -
- ); + + ) } -function NavItem({ name, href, ...rest }: NavItemProps) { +function NavItem({ name, href, active, ...rest }: NavItemProps) { + const activeBgColor = useColorModeValue('gray.50', 'gray.750') + return ( - ); + ) } export function SidebarContent() { - const navigate = useNavigate(); + const location = useLocation() + const navigate = useNavigate() + + const { locale, t } = useLocale() + const { libraries } = useLibraries() - const { locale, t } = useLocale(); + // TODO: I'd like to also highlight the library when viewing an item from it. + // e.g. a book from the library, or a book from a series in the library, etc + const libraryIsActive = (id: string) => location.pathname.startsWith(`/libraries/${id}`) + const linkIsActive = (href?: string) => { + if (!href) { + return false + } else if (href === '/') { + return location.pathname === '/' + } - const { libraries } = useLibraries(); + return location.pathname.startsWith(href) + } const links: Array = useMemo( () => [ - { name: t('sidebar.buttons.home'), icon: House as any, href: '/' }, + { href: '/', icon: House as any, name: t('sidebar.buttons.home') }, { - name: t('sidebar.buttons.libraries'), icon: Books as any, items: libraries?.map((library) => ({ ...library, + active: libraryIsActive(library.id), href: `/libraries/${library.id}`, + onHover: () => refreshUseLibrary(library.id), })), + name: t('sidebar.buttons.libraries'), + }, + { + href: '/settings', + icon: Gear as any, + name: t('sidebar.buttons.settings'), + // onHover: () => queryClient.prefetchQuery([]) }, - { name: t('sidebar.buttons.settings'), icon: Gear as any, href: '/settings' }, ], - [libraries, locale], - ); + + // eslint-disable-next-line react-hooks/exhaustive-deps + [libraries, locale, location.pathname], + ) return ( <> @@ -171,13 +208,12 @@ export function SidebarContent() { - {/* TODO: this needs to scroll on 'overflow' */} - + {links.map((link) => link.items ? ( navigate(href)} /> ) : ( - + ), )} @@ -191,7 +227,7 @@ export function SidebarContent() { - ); + ) } export default function Sidebar() { @@ -209,14 +245,15 @@ export default function Sidebar() { bg={useColorModeValue('white', 'gray.800')} borderRight="1px" borderRightColor={useColorModeValue('gray.200', 'gray.700')} - w={52} + w={56} h="full" px={2} zIndex={10} spacing={4} + className="relative" > - ); + ) } diff --git a/common/interface/src/components/sidebar/ThemeToggle.tsx b/packages/interface/src/components/sidebar/ThemeToggle.tsx similarity index 56% rename from common/interface/src/components/sidebar/ThemeToggle.tsx rename to packages/interface/src/components/sidebar/ThemeToggle.tsx index 7711a58c8..61265ddbd 100644 --- a/common/interface/src/components/sidebar/ThemeToggle.tsx +++ b/packages/interface/src/components/sidebar/ThemeToggle.tsx @@ -1,14 +1,15 @@ -import { Button, useColorMode } from '@chakra-ui/react'; -import { AnimatePresence, motion } from 'framer-motion'; -import { Moon, Sun } from 'phosphor-react'; -import ToolTip from '../../ui/ToolTip'; +import { Button, useColorMode } from '@chakra-ui/react' +import { AnimatePresence, motion } from 'framer-motion' +import { Moon, Sun } from 'phosphor-react' + +import ToolTip from '../../ui/ToolTip' export default function ThemeToggle() { - const { colorMode, toggleColorMode } = useColorMode(); + const { colorMode, toggleColorMode } = useColorMode() return (
- +
- ); + ) } diff --git a/packages/interface/src/components/tags/Tag.tsx b/packages/interface/src/components/tags/Tag.tsx new file mode 100644 index 000000000..a370d8eca --- /dev/null +++ b/packages/interface/src/components/tags/Tag.tsx @@ -0,0 +1,14 @@ +import { Badge } from '@chakra-ui/react' +import { Tag } from '@stump/types' + +interface Props { + tag: Tag +} + +export default function TagComponent({ tag }: Props) { + return ( + + {tag.name} + + ) +} diff --git a/packages/interface/src/components/tags/TagList.tsx b/packages/interface/src/components/tags/TagList.tsx new file mode 100644 index 000000000..a7b8e991b --- /dev/null +++ b/packages/interface/src/components/tags/TagList.tsx @@ -0,0 +1,40 @@ +import { Tag } from '@stump/types' + +import TagComponent from './Tag' + +export const DEBUG_TAGS: Tag[] = [ + { + id: '1', + name: 'Action', + }, + { + id: '2', + name: 'Adventure', + }, + { + id: '3', + name: 'Comedy', + }, + { + id: '4', + name: 'Drama', + }, +] + +interface Props { + tags: Tag[] | null +} + +export default function TagList({ tags }: Props) { + if (!tags && !import.meta.env.DEV) { + return null + } + + return ( +
+ {(tags ?? DEBUG_TAGS).map((tag) => ( + + ))} +
+ ) +} diff --git a/common/interface/src/components/topbar/LayoutModeButtons.tsx b/packages/interface/src/components/topbar/LayoutModeButtons.tsx similarity index 70% rename from common/interface/src/components/topbar/LayoutModeButtons.tsx rename to packages/interface/src/components/topbar/LayoutModeButtons.tsx index f857d3a8f..a72217d56 100644 --- a/common/interface/src/components/topbar/LayoutModeButtons.tsx +++ b/packages/interface/src/components/topbar/LayoutModeButtons.tsx @@ -1,24 +1,22 @@ -import { Rows, SquaresFour } from 'phosphor-react'; -import toast from 'react-hot-toast'; +import { ButtonGroup, useColorModeValue } from '@chakra-ui/react' +import { LayoutEntity, useLayoutMode } from '@stump/client' +import type { LayoutMode } from '@stump/types' +import { Rows, SquaresFour } from 'phosphor-react' +import toast from 'react-hot-toast' -import { ButtonGroup, useColorModeValue } from '@chakra-ui/react'; -import { LayoutEntity, useLayoutMode } from '@stump/client'; - -import { IconButton } from '../../ui/Button'; - -import type { LayoutMode } from '@stump/client'; +import { IconButton } from '../../ui/Button' export default function LayoutModeButtons({ entity }: { entity: LayoutEntity }) { - const { layoutMode, updateLayoutMode } = useLayoutMode(entity); + const { layoutMode, updateLayoutMode } = useLayoutMode(entity) async function handleChange(mode: LayoutMode) { updateLayoutMode(mode, (err) => { - console.error(err); - toast.error('Failed to update layout mode'); - }); + console.error(err) + toast.error('Failed to update layout mode') + }) } - const viewAsGrid = layoutMode === 'GRID'; + const viewAsGrid = layoutMode === 'GRID' return ( @@ -48,5 +46,5 @@ export default function LayoutModeButtons({ entity }: { entity: LayoutEntity }) - ); + ) } diff --git a/common/interface/src/components/topbar/NavigationButtons.tsx b/packages/interface/src/components/topbar/NavigationButtons.tsx similarity index 68% rename from common/interface/src/components/topbar/NavigationButtons.tsx rename to packages/interface/src/components/topbar/NavigationButtons.tsx index 566135269..4fb91e6e0 100644 --- a/common/interface/src/components/topbar/NavigationButtons.tsx +++ b/packages/interface/src/components/topbar/NavigationButtons.tsx @@ -1,46 +1,48 @@ -import { CaretLeft, CaretRight } from 'phosphor-react'; -import { useCallback } from 'react'; -import { useNavigate, To } from 'react-router-dom'; +import { HStack, useColorModeValue } from '@chakra-ui/react' +import { useTopBarStore } from '@stump/client' +import { CaretLeft, CaretRight } from 'phosphor-react' +import { useCallback } from 'react' +import { useHotkeys } from 'react-hotkeys-hook' +import { To, useNavigate } from 'react-router-dom' -import { HStack, useColorModeValue } from '@chakra-ui/react'; -import { useTopBarStore } from '@stump/client'; - -import Button from '../../ui/Button'; -import { useHotkeys } from 'react-hotkeys-hook'; +import Button from '../../ui/Button' export default function NavigationButtons() { - const navigate = useNavigate(); + const navigate = useNavigate() // FIXME: still not a perfect solution, but it works for now. // https://github.com/remix-run/react-router/discussions/8782 - const { backwardsUrl, forwardsUrl } = useTopBarStore(); + const { backwardsUrl, forwardsUrl } = useTopBarStore(({ backwardsUrl, forwardsUrl }) => ({ + backwardsUrl, + forwardsUrl, + })) const navigateForward = useCallback(() => { if (forwardsUrl != undefined) { - navigate(forwardsUrl as To); + navigate(forwardsUrl as To) } else if (forwardsUrl !== 0) { - navigate(1); + navigate(1) } - }, [forwardsUrl]); + }, [navigate, forwardsUrl]) const navigateBackward = useCallback(() => { if (backwardsUrl) { - navigate(backwardsUrl as To); + navigate(backwardsUrl as To) } else if (backwardsUrl !== 0) { - navigate(-1); + navigate(-1) } - }, [backwardsUrl]); + }, [navigate, backwardsUrl]) // FIXME: this is pretty buggy, but it works for now. // TODO: platform specific keybinds useHotkeys('ctrl+], cmd+],ctrl+[, cmd+[', (e) => { - e.preventDefault(); + e.preventDefault() if (e.key === ']') { - navigateForward(); + navigateForward() } else if (e.key === '[') { - navigateBackward(); + navigateBackward() } - }); + }) return ( - ); + ) } diff --git a/common/interface/src/components/topbar/Search.tsx b/packages/interface/src/components/topbar/Search.tsx similarity index 51% rename from common/interface/src/components/topbar/Search.tsx rename to packages/interface/src/components/topbar/Search.tsx index 668f925d7..11bda490e 100644 --- a/common/interface/src/components/topbar/Search.tsx +++ b/packages/interface/src/components/topbar/Search.tsx @@ -1,48 +1,43 @@ -import { - InputGroup, - InputRightElement, - Kbd, - useBoolean, - useColorModeValue, -} from '@chakra-ui/react'; -import { MagnifyingGlass } from 'phosphor-react'; -import React, { FormEvent, useMemo } from 'react'; -import toast from 'react-hot-toast'; -import { useHotkeys } from 'react-hotkeys-hook'; -import Input from '../../ui/Input'; +import { InputGroup, InputRightElement, Kbd, useBoolean, useColorModeValue } from '@chakra-ui/react' +import { MagnifyingGlass } from 'phosphor-react' +import React, { FormEvent, useMemo } from 'react' +import toast from 'react-hot-toast' +import { useHotkeys } from 'react-hotkeys-hook' + +import Input from '../../ui/Input' function Shortcut({ visible }: { visible?: boolean }) { // FIXME: don't use deprecated - const key = window.navigator.platform.match(/^Mac/) ? '⌘k' : 'ctrl+k'; + const key = window.navigator.platform.match(/^Mac/) ? '⌘k' : 'ctrl+k' return ( - ); + ) } // TODO: I think I am going to remove this component from the TopBar. It interferes with the // UI of the navigation and the title. I think a potential solution would be to have the search // be solely rendered via keybind, and have it be in a portal. export default function Search() { - const inputRef = React.useRef(null); - const [expanded, { on, off }] = useBoolean(false); + const inputRef = React.useRef(null) + const [expanded, { on, off }] = useBoolean(false) - useHotkeys('ctrl+k, cmd+k', () => inputRef.current?.focus()); + useHotkeys('ctrl+k, cmd+k', () => inputRef.current?.focus()) const width = useMemo(() => { if (expanded) { - return { base: 44, md: 72 }; + return { base: 44, md: 72 } } - return { base: 28, md: 52 }; - }, [expanded]); + return { base: 28, md: 52 } + }, [expanded]) function handleSubmit(e: FormEvent) { - e.preventDefault(); + e.preventDefault() - toast.error("I don't support search yet, check back soon!"); + toast.error("I don't support search yet, check back soon!") } return ( @@ -58,16 +53,17 @@ export default function Search() { transition="all 0.2s" onKeyDown={(e) => { if (e.key === 'Escape') { - inputRef.current?.blur(); + inputRef.current?.blur() } }} /> - } - /> - } /> + + + + + + - ); + ) } diff --git a/common/interface/src/components/topbar/TopBar.tsx b/packages/interface/src/components/topbar/TopBar.tsx similarity index 65% rename from common/interface/src/components/topbar/TopBar.tsx rename to packages/interface/src/components/topbar/TopBar.tsx index e2ebffb58..a2d2c860b 100644 --- a/common/interface/src/components/topbar/TopBar.tsx +++ b/packages/interface/src/components/topbar/TopBar.tsx @@ -1,36 +1,35 @@ -import { useMemo } from 'react'; -import { useLocation } from 'react-router'; +import { Box, Heading, HStack, useColorModeValue } from '@chakra-ui/react' +import { LayoutEntity, useTopBarStore } from '@stump/client' +import { useMemo } from 'react' +import { useLocation } from 'react-router' -import { Box, Heading, HStack, useColorModeValue } from '@chakra-ui/react'; -import { LayoutEntity, useTopBarStore } from '@stump/client'; - -import MobileDrawer from '../sidebar/MobileDrawer'; -import LayoutModeButtons from './LayoutModeButtons'; -import QueryConfig from './query-options/QueryConfig'; +import MobileDrawer from '../sidebar/MobileDrawer' +import LayoutModeButtons from './LayoutModeButtons' +import QueryConfig from './query-options/QueryConfig' // FIXME: this is not good AT ALL for mobile. It *looks* fine, but the navigation is gone, the // sort/view mode buttons are gone, the sort config is gone,and the search bar is meh. I need to // plan out the layout for mobile. export default function TopBar() { - const { title } = useTopBarStore(); - const location = useLocation(); + const title = useTopBarStore(({ title }) => title) + const location = useLocation() const { entity, showQueryParamOptions } = useMemo(() => { - let match = - location.pathname.match(/\/libraries\/.+$/) || location.pathname.match(/\/series\/.+$/); + const match = + location.pathname.match(/\/libraries\/.+$/) || location.pathname.match(/\/series\/.+$/) - let _entity: LayoutEntity = 'SERIES'; + let _entity: LayoutEntity = 'SERIES' // TODO: what if not either of these? if (location.pathname.startsWith('/libraries')) { - _entity = 'LIBRARY'; + _entity = 'LIBRARY' } return { entity: _entity, showQueryParamOptions: !!match, - }; - }, [location]); + } + }, [location]) return ( - - {/* @ts-ignore: this seems to work, idky it has type error */} + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore: TODO: this seems to work, idky it has type error */} {title} @@ -60,5 +59,5 @@ export default function TopBar() { )} - ); + ) } diff --git a/common/interface/src/components/topbar/query-options/QueryConfig.tsx b/packages/interface/src/components/topbar/query-options/QueryConfig.tsx similarity index 85% rename from common/interface/src/components/topbar/query-options/QueryConfig.tsx rename to packages/interface/src/components/topbar/query-options/QueryConfig.tsx index 562cc86c2..25fa390ef 100644 --- a/common/interface/src/components/topbar/query-options/QueryConfig.tsx +++ b/packages/interface/src/components/topbar/query-options/QueryConfig.tsx @@ -1,7 +1,3 @@ -import { CaretDown, Sliders } from 'phosphor-react'; -import React, { useMemo } from 'react'; -import { useLocation } from 'react-router'; - import { Heading, HStack, @@ -16,25 +12,32 @@ import { Switch, Text, useColorModeValue, -} from '@chakra-ui/react'; -import { mediaOrderByOptions, seriesOrderByOptions, useQueryParamStore } from '@stump/client'; - -import Button from '../../../ui/Button'; +} from '@chakra-ui/react' +import { useQueryParamStore } from '@stump/client' +import { Direction, mediaOrderByOptions, seriesOrderByOptions } from '@stump/types' +import { CaretDown, Sliders } from 'phosphor-react' +import React, { useMemo } from 'react' +import { useLocation } from 'react-router' -import type { Direction } from '@stump/client'; +import Button from '../../../ui/Button' function QueryConfigSection({ children }: { children: React.ReactNode }) { return ( {children} - ); + ) } export default function QueryConfig() { - const { order_by, setOrderBy, direction, setDirection } = useQueryParamStore(); + const { order_by, setOrderBy, direction, setDirection } = useQueryParamStore((state) => ({ + direction: state.direction, + order_by: state.order_by, + setDirection: state.setDirection, + setOrderBy: state.setOrderBy, + })) - const location = useLocation(); + const location = useLocation() function intoOptions(keys: string[]) { return keys.map((key) => ({ @@ -43,7 +46,7 @@ export default function QueryConfig() { .replace(/_/g, ' ') .replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1)), value: key, - })); + })) } // FIXME: This will BREAK when switching between entities. For example, lets say @@ -54,14 +57,16 @@ export default function QueryConfig() { const orderByOptions = useMemo(() => { // TODO: remove this from component, to reduce computation... also it is just ugly. if (location.pathname.startsWith('/libraries')) { - return intoOptions(Object.keys(seriesOrderByOptions)); + return intoOptions(Object.keys(seriesOrderByOptions)) } else if (location.pathname.startsWith('/series')) { - return intoOptions(Object.keys(mediaOrderByOptions)); + return intoOptions(Object.keys(mediaOrderByOptions)) } - }, [location.pathname]); - const activeRadioFontColor = useColorModeValue('gray.800', 'gray.200'); - const inActiveRadioFontColor = useColorModeValue('gray.600', 'gray.400'); + return null + }, [location.pathname]) + + const activeRadioFontColor = useColorModeValue('gray.800', 'gray.200') + const inActiveRadioFontColor = useColorModeValue('gray.600', 'gray.400') return ( @@ -159,5 +164,5 @@ export default function QueryConfig() { )} - ); + ) } diff --git a/packages/interface/src/context.ts b/packages/interface/src/context.ts new file mode 100644 index 000000000..da7f56f99 --- /dev/null +++ b/packages/interface/src/context.ts @@ -0,0 +1,10 @@ +import { User } from '@stump/types' +import { createContext, useContext } from 'react' + +type AppContext = { + user: User + isServerOwner: boolean +} + +export const AppContext = createContext({} as AppContext) +export const useAppContext = () => useContext(AppContext) diff --git a/packages/interface/src/hooks/useGetPage.ts b/packages/interface/src/hooks/useGetPage.ts new file mode 100644 index 000000000..aa23d6cbc --- /dev/null +++ b/packages/interface/src/hooks/useGetPage.ts @@ -0,0 +1,23 @@ +import { useMemo } from 'react' +import { useSearchParams } from 'react-router-dom' + +export function useGetPage() { + const [search, setSearchParams] = useSearchParams() + + const page = useMemo(() => { + const searchPage = search.get('page') + + if (searchPage) { + return parseInt(searchPage, 10) + } + + return 1 + }, [search]) + + function setPage(page: number) { + search.set('page', page.toString()) + setSearchParams(search) + } + + return { page, setPage } +} diff --git a/packages/interface/src/hooks/useIsInView.ts b/packages/interface/src/hooks/useIsInView.ts new file mode 100644 index 000000000..0a3e872e9 --- /dev/null +++ b/packages/interface/src/hooks/useIsInView.ts @@ -0,0 +1,27 @@ +import { useEffect, useRef, useState } from 'react' + +export default function useIsInView( + rootMargin = '0px', +): [React.MutableRefObject, boolean] { + const ref = useRef() + + const [isIntersecting, setIntersecting] = useState(false) + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => setIntersecting(entry?.isIntersecting || false), + { + rootMargin, + }, + ) + + if (ref.current) { + observer.observe(ref.current) + } + return () => { + observer.disconnect() + } + }, [rootMargin]) + + return [ref, isIntersecting] +} diff --git a/packages/interface/src/hooks/useLocale.ts b/packages/interface/src/hooks/useLocale.ts new file mode 100644 index 000000000..3943b2455 --- /dev/null +++ b/packages/interface/src/hooks/useLocale.ts @@ -0,0 +1,46 @@ +import '../i18n/config' + +import { useUserStore } from '@stump/client' +import { useTranslation } from 'react-i18next' + +export enum Locale { + English = 'en', + French = 'fr', +} + +export type LocaleSelectOption = { + label: string + value: Locale +} + +export function useLocale() { + // TODO: update DB on changes + const { userPreferences, setUserPreferences } = useUserStore((store) => ({ + setUserPreferences: store.setUserPreferences, + userPreferences: store.userPreferences, + })) + + function setLocaleFromStr(localeStr: string) { + const locale = localeStr as Locale + + if (userPreferences && locale) { + setUserPreferences({ ...userPreferences, locale }) + } + } + + function setLocale(locale: Locale) { + if (userPreferences && locale) { + setUserPreferences({ ...userPreferences, locale }) + } + } + + const locale: string = userPreferences?.locale || 'en' + + const { t } = useTranslation(locale) + + const locales: LocaleSelectOption[] = Object.keys(Locale) + .map((key) => ({ label: key, value: Locale[key as keyof typeof Locale] })) + .filter((option) => typeof option.value === 'string') + + return { locale, locales, setLocale, setLocaleFromStr, t } +} diff --git a/common/interface/src/hooks/usePagination.ts b/packages/interface/src/hooks/usePagination.ts similarity index 71% rename from common/interface/src/hooks/usePagination.ts rename to packages/interface/src/hooks/usePagination.ts index 16043a8df..e100ad628 100644 --- a/common/interface/src/hooks/usePagination.ts +++ b/packages/interface/src/hooks/usePagination.ts @@ -1,9 +1,9 @@ -import { useMemo } from 'react'; +import { useMemo } from 'react' interface UsePaginationOptions { - totalPages: number; - currentPage: number; - numbersToShow?: number; + totalPages: number + currentPage: number + numbersToShow?: number } export function usePagination({ @@ -12,27 +12,27 @@ export function usePagination({ numbersToShow = 7, }: UsePaginationOptions) { function getRange(start: number, end: number) { - let length = end - start + 1; - return Array.from({ length }, (_, i) => i + start); + const length = end - start + 1 + return Array.from({ length }, (_, i) => i + start) } const pageRange = useMemo(() => { const start = Math.max( 1, Math.min(currentPage - Math.floor((numbersToShow - 3) / 2), totalPages - numbersToShow + 2), - ); + ) const end = Math.min( totalPages, Math.max(currentPage + Math.floor((numbersToShow - 2) / 2), numbersToShow - 1), - ); + ) return [ ...(start > 2 ? [1, '...'] : start > 1 ? [1] : []), ...getRange(start, end), ...(end < totalPages - 1 ? ['...', totalPages] : end < totalPages ? [totalPages] : []), - ]; - }, [numbersToShow, totalPages, currentPage]); + ] + }, [numbersToShow, totalPages, currentPage]) - return { pageRange }; + return { pageRange } } diff --git a/common/interface/src/i18n/config.ts b/packages/interface/src/i18n/config.ts similarity index 53% rename from common/interface/src/i18n/config.ts rename to packages/interface/src/i18n/config.ts index 3258f6538..b206a0e49 100644 --- a/common/interface/src/i18n/config.ts +++ b/packages/interface/src/i18n/config.ts @@ -1,7 +1,11 @@ -import i18n from 'i18next'; -import en from './locales/en.json'; -import fr from './locales/fr.json'; -import { initReactI18next } from 'react-i18next'; +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' + +// @ts-ignore: ugh, stinky tsconfig i'll deal with this later +import en from './locales/en.json' +// @ts-ignore: ugh, stinky tsconfig i'll deal with this later +import fr from './locales/fr.json' // NOTE: the fr locale is *NOT* complete or acurrate. Used for testing... // TODO: once the english locale is completed/mostly completed, the structure @@ -16,29 +20,31 @@ export const resources = { fr: { fr, }, -} as const; +} as const function parseMissingKeyHandler(missingKey: string) { - const translation = (missingKey ?? '') - .split('.') - // @ts-ignore - .reduce((previous, current) => previous[current], resources.en.en); + try { + const translation = (missingKey ?? '') + .split('.') + // @ts-expect-error: is fine + .reduce((previous, current) => previous[current], resources.en.en) - // console.log({ translation }); + if (typeof translation === 'string') { + return translation + } - if (typeof translation === 'string') { - return translation; + return missingKey + } catch (error) { + return missingKey } - - return missingKey; } i18n.use(initReactI18next).init({ + fallbackLng: 'en', // lng: 'en', interpolation: { escapeValue: false, // not needed for react as it escapes by default }, - fallbackLng: 'en', parseMissingKeyHandler, resources, -}); +}) diff --git a/packages/interface/src/i18n/locales/en.json b/packages/interface/src/i18n/locales/en.json new file mode 100644 index 000000000..395cc39a6 --- /dev/null +++ b/packages/interface/src/i18n/locales/en.json @@ -0,0 +1,77 @@ +{ + "loginPage": { + "claimText": "This Stump server is not yet claimed, you can use the form below to create a user account and claim it. Enter your preferred username and password.", + "form": { + "validation": { + "missingUsername": "Username is required", + "missingPassword": "Password is required" + }, + "labels": { + "username": "Username", + "password": "Password" + }, + "buttons": { + "createAccount": "Create Account", + "login": "Login" + } + }, + "toasts": { + "loggingIn": "Logging in...", + "loggedIn": "Welcome back! Redirecting...", + "loggedInFirstTime": "Welcome! Redirecting...", + "registering": "Registering...", + "registered": "Registered!", + "loginFailed": "Login failed. Please try again.", + "registrationFailed": "Registration failed. Please try again." + } + }, + "signOutModal": { + "title": "Sign out", + "message": "Are you sure you want to sign out?", + "buttons": { + "cancel": "Cancel", + "signOut": "Sign out" + } + }, + "settingsPage": { + "navigation": { + "generalSettings": "General Settings", + "serverSettings": "Server Settings", + "jobHistory": "Job History" + }, + "general": { + "profileForm": { + "validation": {}, + "labels": { + "username": "Username", + "password": "Password" + }, + "buttons": {} + } + } + }, + "sidebar": { + "buttons": { + "home": "Home", + "libraries": "Libraries", + "settings": "Settings" + } + }, + "libraryModalForm": { + "createLibraryHeading": "Create a new library", + "labels": { + "libraryName": "Library Name", + "libraryPath": "Library Path", + "libraryDescription": "Library Description", + "libraryTags": "Library Tags" + }, + "buttons": { + "cancel": "Cancel", + "editLibrary": "Save Changes", + "createLibrary": "Create Library" + } + }, + "search": { + "placeholder": "Search" + } +} diff --git a/packages/interface/src/i18n/locales/fr.json b/packages/interface/src/i18n/locales/fr.json new file mode 100644 index 000000000..29904b3b1 --- /dev/null +++ b/packages/interface/src/i18n/locales/fr.json @@ -0,0 +1,28 @@ +{ + "loginPage": { + "claimText": "Ce serveur Stump n'est pas encore réclamé, vous pouvez utiliser le formulaire ci-dessous pour créer un compte utilisateur et le réclamer. Entrez votre nom d'utilisateur et votre mot de passe préférés.", + "form": { + "validation": { + "missingUsername": "Nom d'utilisateur est nécessaire", + "missingPassword": "Mot de passe est nécessaire" + }, + "labels": { + "username": "Nom d'utilisateur", + "password": "Mot de passe" + }, + "buttons": { + "createAccount": "Créer un Compte", + "login": "Connexion" + } + }, + "toasts": { + "loggingIn": "Se connecter...", + "loggedIn": "Content de te revoir! Redirection...", + "loggedInFirstTime": "Accueillir! Redirection...", + "registering": "La création du compte...", + "registered": "Compte créé!", + "loginFailed": "Échec de la connexion. Veuillez réessayer.", + "registrationFailed": "La création du compte a échoué. Veuillez réessayer." + } + } +} diff --git a/packages/interface/src/index.ts b/packages/interface/src/index.ts new file mode 100644 index 000000000..cea43905b --- /dev/null +++ b/packages/interface/src/index.ts @@ -0,0 +1,6 @@ +import StumpInterface from './App' + +export const DEBUG_ENV = import.meta.env.DEV +export const API_VERSION = import.meta.env.API_VERSION ?? 'v1' + +export default StumpInterface diff --git a/packages/interface/src/pages/FourOhFour.tsx b/packages/interface/src/pages/FourOhFour.tsx new file mode 100644 index 000000000..55377fba4 --- /dev/null +++ b/packages/interface/src/pages/FourOhFour.tsx @@ -0,0 +1,5 @@ +// TODO: design page, don't be lazy and throw an error lmao +export default function FourOhFour() { + throw new Error("404, what you're looking for doesn't exist!") + return
404
+} diff --git a/packages/interface/src/pages/Home.tsx b/packages/interface/src/pages/Home.tsx new file mode 100644 index 000000000..60f817cb9 --- /dev/null +++ b/packages/interface/src/pages/Home.tsx @@ -0,0 +1,46 @@ +import { Stack } from '@chakra-ui/react' +import { useLibraries } from '@stump/client' +import { Helmet } from 'react-helmet' + +import LibrariesStats from '../components/library/LibrariesStats' +import NoLibraries from '../components/library/NoLibraries' +import ContinueReadingMedia from '../components/media/ContinueReading' +import RecentlyAddedMedia from '../components/media/RecentlyAddedMedia' +import RecentlyAddedSeries from '../components/series/RecentlyAddedSeries' + +// TODO: account for new accounts, i.e. no media at all +export default function Home() { + const { libraries, isLoading } = useLibraries() + + const helmet = ( + + {/* Doing this so Helmet splits the title into an array, I'm not just insane lol */} + Stump | {'Home'} + + ) + + if (isLoading) { + return <> + } + + if (!libraries?.length) { + return ( + <> + {helmet} + + + ) + } + + return ( + <> + {helmet} + + + + + + + + ) +} diff --git a/common/interface/src/pages/LoginOrClaim.tsx b/packages/interface/src/pages/LoginOrClaim.tsx similarity index 73% rename from common/interface/src/pages/LoginOrClaim.tsx rename to packages/interface/src/pages/LoginOrClaim.tsx index 402daaf47..6bc87c650 100644 --- a/common/interface/src/pages/LoginOrClaim.tsx +++ b/packages/interface/src/pages/LoginOrClaim.tsx @@ -1,7 +1,3 @@ -import { FieldValues, useForm } from 'react-hook-form'; -import { Navigate } from 'react-router-dom'; -import { z } from 'zod'; - import { Alert, AlertIcon, @@ -12,61 +8,70 @@ import { HStack, Stack, Text, -} from '@chakra-ui/react'; -import { zodResolver } from '@hookform/resolvers/zod'; -import toast from 'react-hot-toast'; -import { queryClient, useLoginOrRegister, useUserStore } from '@stump/client'; -import Form from '../ui/Form'; -import Input from '../ui/Input'; -import { useLocale } from '../hooks/useLocale'; +} from '@chakra-ui/react' +import { zodResolver } from '@hookform/resolvers/zod' +import { queryClient, useLoginOrRegister, useUserStore } from '@stump/client' +import { FieldValues, useForm } from 'react-hook-form' +import toast from 'react-hot-toast' +import { Navigate, useSearchParams } from 'react-router-dom' +import { z } from 'zod' + +import { useLocale } from '../hooks/useLocale' +import Form from '../ui/Form' +import Input from '../ui/Input' export default function LoginOrClaim() { - const { user, setUser } = useUserStore(); + const [params] = useSearchParams() + + const { user, setUser } = useUserStore((store) => ({ + setUser: store.setUser, + user: store.user, + })) - const { t } = useLocale(); + const { t } = useLocale() const { isClaimed, isCheckingClaimed, loginUser, registerUser, isLoggingIn, isRegistering } = useLoginOrRegister({ onSuccess: setUser, - }); + }) const schema = z.object({ - username: z.string().min(1, { message: t('loginPage.form.validation.missingUsername') }), password: z.string().min(1, { message: t('loginPage.form.validation.missingPassword') }), - }); + username: z.string().min(1, { message: t('loginPage.form.validation.missingUsername') }), + }) const form = useForm({ resolver: zodResolver(schema), - }); + }) async function handleSubmit(values: FieldValues) { - const { username, password } = values; + const { username, password } = values const doLogin = async (firstTime = false) => - toast.promise(loginUser({ username, password }), { + toast.promise(loginUser({ password, username }), { + error: t('loginPage.toasts.loginFailed'), loading: t('loginPage.toasts.loggingIn'), success: firstTime ? t('loginPage.toasts.loggedInFirstTime') : t('loginPage.toasts.loggedIn'), - error: t('loginPage.toasts.loginFailed'), - }); + }) if (isClaimed) { - await doLogin(); + await doLogin() } else { toast - .promise(registerUser({ username, password }), { + .promise(registerUser({ password, username }), { + error: t('loginPage.toasts.registrationFailed'), loading: t('loginPage.toasts.registering'), success: t('loginPage.toasts.registered'), - error: t('loginPage.toasts.registrationFailed'), }) - .then(() => doLogin(true)); + .then(() => doLogin(true)) } } if (user) { - queryClient.invalidateQueries(['getLibraries']); - return ; + queryClient.invalidateQueries(['getLibraries']) + return } else if (isCheckingClaimed) { - return null; + return null } return ( @@ -114,5 +119,5 @@ export default function LoginOrClaim() { - ); + ) } diff --git a/common/interface/src/pages/OnBoarding.tsx b/packages/interface/src/pages/OnBoarding.tsx similarity index 91% rename from common/interface/src/pages/OnBoarding.tsx rename to packages/interface/src/pages/OnBoarding.tsx index 943535432..d4ae956ac 100644 --- a/common/interface/src/pages/OnBoarding.tsx +++ b/packages/interface/src/pages/OnBoarding.tsx @@ -1,6 +1,6 @@ -import { Alert, AlertIcon, Container, HStack, Stack, Text } from '@chakra-ui/react'; +import { Alert, AlertIcon, Container, HStack, Stack, Text } from '@chakra-ui/react' -import ServerUrlForm from '../components/ServerUrlForm'; +import ServerUrlForm from '../components/ServerUrlForm' // Used primarily for setting the correct base url for the api when the app is // NOT running in a browser. I.e. when the app is running in Tauri. @@ -27,5 +27,5 @@ export default function OnBoarding() { - ); + ) } diff --git a/packages/interface/src/pages/SeriesOverview.tsx b/packages/interface/src/pages/SeriesOverview.tsx new file mode 100644 index 000000000..abeee822e --- /dev/null +++ b/packages/interface/src/pages/SeriesOverview.tsx @@ -0,0 +1,138 @@ +import { Box, ButtonGroup, Heading, Spacer } from '@chakra-ui/react' +import { getSeriesThumbnail } from '@stump/api' +import { useLayoutMode, useSeries, useSeriesMedia, useTopBarStore } from '@stump/client' +import { invalidateQueries } from '@stump/client/invalidate' +import { QUERY_KEYS } from '@stump/client/query_keys' +import type { Series } from '@stump/types' +import { useEffect } from 'react' +import { Helmet } from 'react-helmet' +import { useParams } from 'react-router-dom' + +import MediaGrid from '../components/media/MediaGrid' +import MediaList from '../components/media/MediaList' +import DownloadSeriesButton from '../components/series/DownloadSeriesButton' +import UpNextInSeriesButton from '../components/series/UpNextInSeriesButton' +import TagList from '../components/tags/TagList' +import { useGetPage } from '../hooks/useGetPage' +import useIsInView from '../hooks/useIsInView' +import Pagination from '../ui/Pagination' +import ReadMore from '../ui/ReadMore' + +interface OverviewTitleSectionProps { + isVisible: boolean + series: Series +} + +function OverviewTitleSection({ isVisible, series }: OverviewTitleSectionProps) { + if (!isVisible) { + return null + } + + return ( +
+
+ + + + + +
+
+ + {series.name} + + + + + + + + + {/* TODO: I want this at the bottom of the container here, but layout needs to be tweaked and I am tired. */} + +
+
+ ) +} + +export default function SeriesOverview() { + const [containerRef, isInView] = useIsInView() + + const { id } = useParams() + const { page } = useGetPage() + + if (!id) { + throw new Error('Series id is required') + } + + const setBackwardsUrl = useTopBarStore((state) => state.setBackwardsUrl) + + const { layoutMode } = useLayoutMode('SERIES') + const { series, isLoading: isLoadingSeries } = useSeries(id) + const { isLoading: isLoadingMedia, media, pageData } = useSeriesMedia(id, page) + + useEffect(() => { + if (!isInView) { + containerRef.current?.scrollIntoView({ + block: 'nearest', + inline: 'start', + }) + } + }, [isInView, containerRef, pageData?.current_page]) + + useEffect(() => { + if (series) { + setBackwardsUrl(`/libraries/${series.library_id}`) + } + + return () => { + setBackwardsUrl() + // FIXME: why do I need to do this??? What I found was happening was that + // the next series returned from `useSeries` would be ~correct~ BUT it would + // be wrapped in an axios response, i.e. `{ data: { ... } }`. This doesn't make a + // lick of sense to me yet... + invalidateQueries({ keys: [QUERY_KEYS.series.get_by_id] }) + } + }, [series, setBackwardsUrl]) + + // FIXME: ugly + if (isLoadingSeries) { + return
Loading...
+ } else if (!series) { + throw new Error('Series not found') + } + + const { current_page, total_pages } = pageData || {} + const hasStuff = current_page !== undefined && total_pages !== undefined + + return ( +
+ + Stump | {series.name || ''} + + + + + {/* @ts-expect-error: wrong ref but is okay */} +
+
+ {hasStuff ? : null} + {layoutMode === 'GRID' ? ( + + ) : ( + + )} + + {/* FIXME: spacing when empty */} + + + {hasStuff ? ( + + ) : null} +
+
+ ) +} diff --git a/common/interface/src/pages/ServerConnectionError.tsx b/packages/interface/src/pages/ServerConnectionError.tsx similarity index 51% rename from common/interface/src/pages/ServerConnectionError.tsx rename to packages/interface/src/pages/ServerConnectionError.tsx index 0f0d205ef..9633b3ce1 100644 --- a/common/interface/src/pages/ServerConnectionError.tsx +++ b/packages/interface/src/pages/ServerConnectionError.tsx @@ -1,17 +1,17 @@ -import { Alert, AlertIcon, Container, Stack } from '@chakra-ui/react'; -import ServerUrlForm from '../components/ServerUrlForm'; +import { Alert, AlertIcon, Container, Stack } from '@chakra-ui/react' + +import ServerUrlForm from '../components/ServerUrlForm' export default function ServerConnectionError() { return ( - {/* {t('loginPage.claimText')} */} - Darn! We couldn't connect to your configured Stump server. Please check your connection and - try again. If your server URL has changed, use the form below to update it. + Darn! We couldn’t connect to your configured Stump server. Please check your + connection and try again. If your server URL has changed, use the form below to update it. - ); + ) } diff --git a/packages/interface/src/pages/book/BookOverview.tsx b/packages/interface/src/pages/book/BookOverview.tsx new file mode 100644 index 000000000..c8c5417dd --- /dev/null +++ b/packages/interface/src/pages/book/BookOverview.tsx @@ -0,0 +1,73 @@ +import { Box, Heading, Text, useColorModeValue } from '@chakra-ui/react' +import { useMediaById, useTopBarStore } from '@stump/client' +import { Suspense, useEffect } from 'react' +import { Helmet } from 'react-helmet' +import { useParams } from 'react-router-dom' + +import Lazy from '../../components/Lazy' +import BooksAfter from '../../components/media/BooksAfter' +import MediaCard from '../../components/media/MediaCard' +import TagList from '../../components/tags/TagList' +import Link from '../../ui/Link' +import ReadMore from '../../ui/ReadMore' +import { formatBytes } from '../../utils/format' + +// TODO: looks shit on mobile +export default function BookOverview() { + const { id } = useParams() + + if (!id) { + throw new Error('Book id is required for this route.') + } + + const { media, isLoading } = useMediaById(id) + + const setBackwardsUrl = useTopBarStore((state) => state.setBackwardsUrl) + + useEffect(() => { + if (media?.series_id) { + setBackwardsUrl(`/series/${media.series_id}`) + } + + return () => { + setBackwardsUrl() + } + }, [media, setBackwardsUrl]) + + const textColor = useColorModeValue('gray.700', 'gray.400') + + if (!isLoading && !media) { + throw new Error('Media not found') + } + + return ( + }> + + Stump | {media?.name ?? ''} + +
+
+ +
+ + {media?.series && {media?.series.name}} + + {/* TODO: I want this at the bottom of the container here, but layout needs to be tweaked and I am tired. */} + +
+
+ +
+ File Information + + Size: {formatBytes(media?.size)} + Kind: {media?.extension.toUpperCase()} + + Checksum: {media?.checksum} + Path: {media?.path} +
+ {media && } +
+
+ ) +} diff --git a/common/interface/src/pages/book/ReadBook.tsx b/packages/interface/src/pages/book/ReadBook.tsx similarity index 55% rename from common/interface/src/pages/book/ReadBook.tsx rename to packages/interface/src/pages/book/ReadBook.tsx index 60907e285..eab1be0aa 100644 --- a/common/interface/src/pages/book/ReadBook.tsx +++ b/packages/interface/src/pages/book/ReadBook.tsx @@ -1,57 +1,58 @@ -import { useMedia, useMediaMutation } from '@stump/client'; -import { getMediaPage } from '@stump/client/api'; -import { Navigate, useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { getMediaPage } from '@stump/api' +import { useMediaById, useMediaMutation } from '@stump/client' +import { Navigate, useNavigate, useParams, useSearchParams } from 'react-router-dom' + import ImageBasedReader, { AnimatedImageBasedReader, -} from '../../components/readers/ImageBasedReader'; -import { ARCHIVE_EXTENSION, EBOOK_EXTENSION } from '../../utils/patterns'; +} from '../../components/readers/ImageBasedReader' +import { ARCHIVE_EXTENSION, EBOOK_EXTENSION } from '../../utils/patterns' export default function ReadBook() { - const navigate = useNavigate(); + const navigate = useNavigate() - const { id, page } = useParams(); + const { id, page } = useParams() - const [search] = useSearchParams(); + const [search] = useSearchParams() if (!id) { - throw new Error('Media id is required'); + throw new Error('Media id is required') } - const { isLoading: fetchingBook, media } = useMedia(id); + const { isLoading: fetchingBook, media } = useMediaById(id) const { updateReadProgress } = useMediaMutation(id, { onError(err) { - console.error(err); + console.error(err) }, - }); + }) function handleChangePage(newPage: number) { - updateReadProgress(newPage); - navigate(`/books/${id}/pages/${newPage}`); + updateReadProgress(newPage) + navigate(`/books/${id}/pages/${newPage}`) } if (fetchingBook) { - return
Loading...
; + return
Loading...
} if (!media) { - throw new Error('Media not found'); + throw new Error('Media not found') } if (media.extension.match(EBOOK_EXTENSION)) { - return ; + return } else if (!page || parseInt(page, 10) <= 0) { - return ; + return } else if (parseInt(page, 10) > media.pages) { - return ; + return } if (media.extension.match(ARCHIVE_EXTENSION)) { - const animated = !!search.get('animated'); + const animated = !!search.get('animated') // TODO: this will be merged under ImageBasedReader once animations get stable. animation will become a prop // eventually. This is just a debug tool for me right now, and will not remain as separate components in the future. - const Component = animated ? AnimatedImageBasedReader : ImageBasedReader; + const Component = animated ? AnimatedImageBasedReader : ImageBasedReader return ( getMediaPage(id, pageNumber)} onPageChange={handleChangePage} /> - ); + ) } - return
Not a supported book or i just can't do that yet! :)
; + return
Not a supported book or i just can’t do that yet! :)
} diff --git a/packages/interface/src/pages/book/ReadEpub.tsx b/packages/interface/src/pages/book/ReadEpub.tsx new file mode 100644 index 000000000..ea5830213 --- /dev/null +++ b/packages/interface/src/pages/book/ReadEpub.tsx @@ -0,0 +1,41 @@ +import { useEpub } from '@stump/client' +import { Navigate, useParams, useSearchParams } from 'react-router-dom' + +import EpubReader from '../../components/readers/EpubReader' +import LazyEpubReader from '../../components/readers/LazyEpubReader' + +export default function ReadEpub() { + const [search] = useSearchParams() + const loc = search.get('loc') + const lazyReader = search.get('stream') && search.get('stream') !== 'true' + + const { id } = useParams() + if (!id) { + throw new Error('Media id is required') + } + + const { isFetchingBook, epub, ...rest } = useEpub(id, { loc }, !lazyReader) + + if (lazyReader) { + // TODO: remove the loc from search.. + return + } + + if (isFetchingBook) { + return
Loading...
+ } + + if (!epub) { + throw new Error('Epub not found') + } + + if (!epub.media_entity.extension.match(/epub/)) { + return + } + + // else if (!loc) { + // return ; + // } + + return +} diff --git a/packages/interface/src/pages/library/CreateLibrary.tsx b/packages/interface/src/pages/library/CreateLibrary.tsx new file mode 100644 index 000000000..1c421f938 --- /dev/null +++ b/packages/interface/src/pages/library/CreateLibrary.tsx @@ -0,0 +1,23 @@ +import { Helmet } from 'react-helmet' +import { useNavigate } from 'react-router' + +import { useAppContext } from '../../context' + +export default function CreateLibrary() { + const navigate = useNavigate() + const { isServerOwner } = useAppContext() + + if (!isServerOwner) { + navigate('..') + return null + } + + return ( + <> + + Create Library + + TODO: Create Library + + ) +} diff --git a/common/interface/src/pages/library/LibraryFileExplorer.tsx b/packages/interface/src/pages/library/LibraryFileExplorer.tsx similarity index 66% rename from common/interface/src/pages/library/LibraryFileExplorer.tsx rename to packages/interface/src/pages/library/LibraryFileExplorer.tsx index 130aa865a..63a8c1e6c 100644 --- a/common/interface/src/pages/library/LibraryFileExplorer.tsx +++ b/packages/interface/src/pages/library/LibraryFileExplorer.tsx @@ -1,50 +1,49 @@ -import { useEffect, useRef } from 'react'; -import { Helmet } from 'react-helmet'; -import toast from 'react-hot-toast'; -import { useParams } from 'react-router-dom'; +import { useDirectoryListing, useLibrary } from '@stump/client' +import { useEffect, useRef } from 'react' +import { Helmet } from 'react-helmet' +import toast from 'react-hot-toast' +import { useParams } from 'react-router-dom' -import { useDirectoryListing, useLibrary } from '@stump/client'; - -import FileExplorer from '../../components/files/FileExplorer'; +import FileExplorer from '../../components/files/FileExplorer' // TODO: this is just a concept right now, its pretty ugly and I won't spend much more time on it // until more of stump is compelted. That being said, if someone wants to run with this go for it! // most of what would be needed on the backend is in place. export default function LibraryFileExplorer() { - const mounted = useRef(false); + const mounted = useRef(false) - const { id } = useParams(); + const { id } = useParams() if (!id) { - throw new Error('Library id is required'); + throw new Error('Library id is required') } useEffect(() => { if (!mounted.current) { - toast('This feature is planned. No functionality is implemented yet.'); + toast('This feature is planned. No functionality is implemented yet.') - mounted.current = true; + mounted.current = true } - }, []); + }, []) const { library, isLoading } = useLibrary(id, { onError(err) { - console.error(err); + console.error(err) }, - }); + }) // TODO: make different hook for explorer, will need a separate endpoint to // compare paths with DB media entries to pair them up (used for navigation and file icons) const { entries } = useDirectoryListing({ enabled: !!library?.path, startingPath: library?.path, - }); + }) // TODO: loading state if (isLoading) { - return null; + return null } else if (!library) { - throw new Error('Library not found'); + throw new Error('Library not found') } return ( @@ -57,5 +56,5 @@ export default function LibraryFileExplorer() {
- ); + ) } diff --git a/packages/interface/src/pages/library/LibraryOverview.tsx b/packages/interface/src/pages/library/LibraryOverview.tsx new file mode 100644 index 000000000..a0c9af517 --- /dev/null +++ b/packages/interface/src/pages/library/LibraryOverview.tsx @@ -0,0 +1,78 @@ +import { Spacer } from '@chakra-ui/react' +import { useLayoutMode, useLibrary, useLibrarySeries } from '@stump/client' +import { useEffect } from 'react' +import { Helmet } from 'react-helmet' +import { useParams } from 'react-router-dom' + +import SeriesGrid from '../../components/series/SeriesGrid' +import SeriesList from '../../components/series/SeriesList' +import { useGetPage } from '../../hooks/useGetPage' +import useIsInView from '../../hooks/useIsInView' +import Pagination from '../../ui/Pagination' + +export default function LibraryOverview() { + const [containerRef, isInView] = useIsInView() + + const { id } = useParams() + const { page } = useGetPage() + + if (!id) { + throw new Error('Library id is required') + } + + function handleError(err: unknown) { + console.error(err) + } + + const { layoutMode } = useLayoutMode('LIBRARY') + const { isLoading, library } = useLibrary(id, { onError: handleError }) + + const { isLoading: isLoadingSeries, series, pageData } = useLibrarySeries(id, page) + + useEffect( + () => { + if (!isInView) { + containerRef.current?.scrollIntoView() + } + }, + + // eslint-disable-next-line react-hooks/exhaustive-deps + [pageData?.current_page], + ) + + if (isLoading) { + return null + } else if (!library) { + throw new Error('Library not found') + } + + const { current_page, total_pages } = pageData || {} + const hasStuff = total_pages !== undefined && current_page !== undefined + + return ( + <> + + Stump | {library.name} + + + {/* @ts-expect-error: wrong ref, still okay */} +
+ +
+ {hasStuff ? : null} + + {layoutMode === 'GRID' ? ( + + ) : ( + + )} + + + + {hasStuff ? ( + + ) : null} +
+ + ) +} diff --git a/packages/interface/src/pages/settings/GeneralSettings.tsx b/packages/interface/src/pages/settings/GeneralSettings.tsx new file mode 100644 index 000000000..a79c135b9 --- /dev/null +++ b/packages/interface/src/pages/settings/GeneralSettings.tsx @@ -0,0 +1,17 @@ +import { Helmet } from 'react-helmet' + +import { UserPreferencesForm, UserProfileForm } from '../../components/settings/forms' + +export default function GeneralSettings() { + return ( + <> + + {/* Doing this so Helmet splits the title into an array, I'm not just insane lol */} + Stump | {'General Settings'} + + + + + + ) +} diff --git a/common/interface/src/pages/settings/JobSettings.tsx b/packages/interface/src/pages/settings/JobSettings.tsx similarity index 61% rename from common/interface/src/pages/settings/JobSettings.tsx rename to packages/interface/src/pages/settings/JobSettings.tsx index 88e842b7e..9dd1c60d9 100644 --- a/common/interface/src/pages/settings/JobSettings.tsx +++ b/packages/interface/src/pages/settings/JobSettings.tsx @@ -1,17 +1,18 @@ -import { Stack } from '@chakra-ui/react'; -import { useJobReport } from '@stump/client'; -import { Helmet } from 'react-helmet'; -import { RunningJobs } from '../../components/jobs/RunningJobs'; -import JobsTable from '../../components/jobs/JobsTable'; +import { Stack } from '@chakra-ui/react' +import { useJobReport } from '@stump/client' +import { Helmet } from 'react-helmet' + +import JobsTable from '../../components/jobs/JobsTable' +import { RunningJobs } from '../../components/jobs/RunningJobs' // TODO: fix error/loading state lol export default function JobSettings() { - const { isLoading, jobReports } = useJobReport(); + const { isLoading, jobReports } = useJobReport() if (!jobReports && isLoading) { - throw new Error('TODO'); + throw new Error('TODO') } else if (isLoading) { - return

Loading...

; + return

Loading...

} return ( @@ -28,5 +29,5 @@ export default function JobSettings() { - ); + ) } diff --git a/common/interface/src/pages/settings/ServerSettings.tsx b/packages/interface/src/pages/settings/ServerSettings.tsx similarity index 52% rename from common/interface/src/pages/settings/ServerSettings.tsx rename to packages/interface/src/pages/settings/ServerSettings.tsx index 10406e6fc..f97323eda 100644 --- a/common/interface/src/pages/settings/ServerSettings.tsx +++ b/packages/interface/src/pages/settings/ServerSettings.tsx @@ -1,6 +1,7 @@ -import { Helmet } from 'react-helmet'; -import LibrariesStats from '../../components/library/LibrariesStats'; -import ServerStats from '../../components/settings/ServerStats'; +import { Helmet } from 'react-helmet' + +// import LibrariesStats from '../../components/library/LibrariesStats'; +import ServerInformation from '../../components/settings/ServerInformation' export default function ServerSettings() { return ( @@ -10,11 +11,10 @@ export default function ServerSettings() { Stump | {'Server Settings'}
- - +
-
I am not implemented yet
+ {/*
TODO: add more
*/} - ); + ) } diff --git a/packages/interface/src/pages/settings/UserSettings.tsx b/packages/interface/src/pages/settings/UserSettings.tsx new file mode 100644 index 000000000..23ed75c1c --- /dev/null +++ b/packages/interface/src/pages/settings/UserSettings.tsx @@ -0,0 +1,13 @@ +import { Helmet } from 'react-helmet' + +export default function UserSettings() { + return ( + <> + + {/* Doing this so Helmet splits the title into an array, I'm not just insane lol */} + Stump | {'User Settings'} + +
I am not implemented yet
+ + ) +} diff --git a/common/interface/src/styles/index.css b/packages/interface/src/styles/index.css similarity index 100% rename from common/interface/src/styles/index.css rename to packages/interface/src/styles/index.css diff --git a/common/interface/src/ui/AnimatedStat.tsx b/packages/interface/src/ui/AnimatedStat.tsx similarity index 67% rename from common/interface/src/ui/AnimatedStat.tsx rename to packages/interface/src/ui/AnimatedStat.tsx index 1572f6e77..6301fc4fb 100644 --- a/common/interface/src/ui/AnimatedStat.tsx +++ b/packages/interface/src/ui/AnimatedStat.tsx @@ -1,21 +1,14 @@ -import { - Box, - Stat, - StatHelpText, - StatLabel, - StatNumber, - useColorModeValue, -} from '@chakra-ui/react'; -import { useCountUp } from 'use-count-up'; +import { Box, Stat, StatHelpText, StatLabel, StatNumber, useColorModeValue } from '@chakra-ui/react' +import { useCountUp } from 'use-count-up' export interface AnimatedStatProps { - value: number | bigint; - label: string; - helpText?: string; - unit?: string; - duration?: number; - decimal?: boolean; - enabled?: boolean; + value: number | bigint + label: string + helpText?: string + unit?: string + duration?: number + decimal?: boolean + enabled?: boolean } // TODO: prop for animateOnce or something... @@ -29,19 +22,20 @@ export default function AnimatedStat({ enabled = true, }: AnimatedStatProps) { const { value: currentValue } = useCountUp({ - isCounting: enabled, + duration, + // FIXME: not safe!? end: Number(value), - duration, formatter: (value) => { if (decimal) { // TODO: do locale conversion too? - return value.toFixed(2); + return value.toFixed(2) } - return Math.round(value).toLocaleString(); + return Math.round(value).toLocaleString() }, - }); + isCounting: enabled, + }) return ( @@ -54,5 +48,5 @@ export default function AnimatedStat({ {helpText && {helpText}} - ); + ) } diff --git a/common/interface/src/ui/Button.tsx b/packages/interface/src/ui/Button.tsx similarity index 74% rename from common/interface/src/ui/Button.tsx rename to packages/interface/src/ui/Button.tsx index eca958a3c..1dd9518a9 100644 --- a/common/interface/src/ui/Button.tsx +++ b/packages/interface/src/ui/Button.tsx @@ -1,21 +1,22 @@ import { - ButtonProps as ChakraButtonProps, Button as ChakraButton, - ModalCloseButton as ChakraModalCloseButton, + ButtonProps as ChakraButtonProps, forwardRef, -} from '@chakra-ui/react'; -import ShortcutToolTip from '../components/ShortcutToolTip'; -import ToolTip from './ToolTip'; + ModalCloseButton as ChakraModalCloseButton, +} from '@chakra-ui/react' + +import ShortcutToolTip from '../components/ShortcutToolTip' +import ToolTip from './ToolTip' export interface ButtonProps extends ChakraButtonProps { // TODO: change shortcutAction to toolTipLabel, more generic. // when shortcutKeybind is present, we know it is a shortcutAction. - shortcutAction?: string; - shortcutKeybind?: string[]; + shortcutAction?: string + shortcutKeybind?: string[] } export default forwardRef((props, ref) => { - const { shortcutKeybind, shortcutAction, ...rest } = props; + const { shortcutKeybind, shortcutAction, ...rest } = props const button = ( ((props, ref) => { boxShadow: '0 0 0 2px rgba(196, 130, 89, 0.6);', }} /> - ); + ) if (shortcutKeybind) { return ( {button} - ); + ) } else if (shortcutAction) { - return {button}; + return {button} } - return button; -}); + return button +}) export const IconButton = forwardRef((props, ref) => { - const { shortcutKeybind, shortcutAction, ...rest } = props; + const { shortcutKeybind, shortcutAction, ...rest } = props const button = ( ((props, ref) => { boxShadow: '0 0 0 2px rgba(196, 130, 89, 0.6);', }} /> - ); + ) if (shortcutKeybind) { return ( {button} - ); + ) } else if (shortcutAction) { - return {button}; + return {button} } - return button; -}); + return button +}) export const ModalCloseButton = forwardRef((props, ref) => ( ((props, ref) = boxShadow: '0 0 0 2px rgba(196, 130, 89, 0.6);', }} /> -)); +)) diff --git a/common/interface/src/ui/Checkbox.tsx b/packages/interface/src/ui/Checkbox.tsx similarity index 77% rename from common/interface/src/ui/Checkbox.tsx rename to packages/interface/src/ui/Checkbox.tsx index 00362389e..483d9e469 100644 --- a/common/interface/src/ui/Checkbox.tsx +++ b/packages/interface/src/ui/Checkbox.tsx @@ -1,7 +1,7 @@ -import { Checkbox as ChakraCheckbox, forwardRef } from '@chakra-ui/react'; -import type { CheckboxProps as ChakraCheckboxProps } from '@chakra-ui/react'; +import type { CheckboxProps as ChakraCheckboxProps } from '@chakra-ui/react' +import { Checkbox as ChakraCheckbox, forwardRef } from '@chakra-ui/react' -export interface CheckboxProps extends ChakraCheckboxProps {} +export type CheckboxProps = ChakraCheckboxProps export default forwardRef((props, ref) => ( ((props, ref) => ( boxShadow: '0 0 0 2px rgba(196, 130, 89, 0.6);', }} /> -)); +)) diff --git a/packages/interface/src/ui/ConfirmationModal.tsx b/packages/interface/src/ui/ConfirmationModal.tsx new file mode 100644 index 000000000..241ef134d --- /dev/null +++ b/packages/interface/src/ui/ConfirmationModal.tsx @@ -0,0 +1,42 @@ +import { + ButtonGroup, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + ModalProps, +} from '@chakra-ui/react' + +import Button from './Button' + +export type ConfirmationModalProps = { + title?: string + onConfirm: () => void +} & ModalProps + +export default function ConfirmationModal({ + title, + onConfirm, + children, + ...props +}: ConfirmationModalProps) { + return ( + + + + {title || 'Are you sure?'} + {children} + + + + + + + + + ) +} diff --git a/common/interface/src/ui/Form.tsx b/packages/interface/src/ui/Form.tsx similarity index 62% rename from common/interface/src/ui/Form.tsx rename to packages/interface/src/ui/Form.tsx index 9a4bcd6b5..cf10ff303 100644 --- a/common/interface/src/ui/Form.tsx +++ b/packages/interface/src/ui/Form.tsx @@ -1,14 +1,14 @@ -import { forwardRef, VStack, FormControl as ChakraFormControl } from '@chakra-ui/react'; -import type { FormControlProps as ChakraFormControlProps } from '@chakra-ui/react'; -import React from 'react'; -import { FormProvider, UseFormReturn, FieldValues, SubmitHandler } from 'react-hook-form'; +import type { FormControlProps as ChakraFormControlProps } from '@chakra-ui/react' +import { FormControl as ChakraFormControl, forwardRef, VStack } from '@chakra-ui/react' +import React from 'react' +import { FieldValues, FormProvider, SubmitHandler, UseFormReturn } from 'react-hook-form' interface FormProps { - form: UseFormReturn; - onSubmit: SubmitHandler; + form: UseFormReturn + onSubmit: SubmitHandler } -type Props = FormProps & React.ComponentProps<'form'>; +type Props = FormProps & React.ComponentProps<'form'> export default function Form({ form, onSubmit, children, ...props }: Props) { return ( @@ -19,7 +19,7 @@ export default function Form({ form, onSubmit, children, ...props }: Props) { - ); + ) } export const FormControl = forwardRef((props, ref) => ( @@ -30,4 +30,4 @@ export const FormControl = forwardRef((props, ref boxShadow: '0 0 0 2px rgba(196, 130, 89, 0.6);', }} /> -)); +)) diff --git a/common/interface/src/ui/Input.tsx b/packages/interface/src/ui/Input.tsx similarity index 66% rename from common/interface/src/ui/Input.tsx rename to packages/interface/src/ui/Input.tsx index d3262e9d8..34f08d4e6 100644 --- a/common/interface/src/ui/Input.tsx +++ b/packages/interface/src/ui/Input.tsx @@ -1,4 +1,3 @@ -import React, { useMemo, useState } from 'react'; import { forwardRef, Input as ChakraInput, @@ -6,12 +5,12 @@ import { InputProps, InputRightElement, useBoolean, -} from '@chakra-ui/react'; -import { Eye, EyeSlash } from 'phosphor-react'; +} from '@chakra-ui/react' +import { Eye, EyeSlash } from 'phosphor-react' +import React, { useMemo, useState } from 'react' interface Props extends InputProps { - // label?: string; - fullWidth?: boolean; + fullWidth?: boolean } const Input = forwardRef(({ fullWidth = true, ...props }, ref) => { @@ -24,7 +23,7 @@ const Input = forwardRef(({ fullWidth = true, ...props }, ref) = _focusVisible: { borderBottom: '1px rgba(196, 130, 89, 0.4);', }, - }; + } } return { @@ -34,8 +33,8 @@ const Input = forwardRef(({ fullWidth = true, ...props }, ref) = _focusVisible: { border: 'rgba(196, 130, 89, 0.4);', }, - }; - }, [props.variant]); + } + }, [props.variant]) return ( (({ fullWidth = true, ...props }, ref) = _focus={_focus} _focusVisible={_focusVisible} /> - ); -}); + ) +}) -export default Input; +export default Input export const PasswordInput = forwardRef(({ ...props }, ref) => { - const [showPass, { toggle }] = useBoolean(false); + const [showPass, { toggle }] = useBoolean(false) return ( - + - - {showPass ? ( - - ) : ( - - )} - - } - /> + + <> + {showPass ? ( + + ) : ( + + )} + + - ); -}); + ) +}) interface DebouncedProps extends Props { - delay?: number; - onInputStop(value?: string): void; + delay?: number + onInputStop(value?: string): void } export const DebouncedInput = forwardRef( ({ delay = 500, onChange, onInputStop, ...props }, ref) => { - const [timer, setTimer] = useState(null); + const [timer, setTimer] = useState(null) function onInputChange(e: React.ChangeEvent) { - onChange?.(e); + onChange?.(e) if (timer) { - clearTimeout(timer); + clearTimeout(timer) } - const newTimeout = setTimeout(() => onInputStop(e.target?.value), delay); + const newTimeout = setTimeout(() => onInputStop(e.target?.value), delay) - setTimer(newTimeout); + setTimer(newTimeout) } - return ; + return }, -); +) diff --git a/packages/interface/src/ui/Link.tsx b/packages/interface/src/ui/Link.tsx new file mode 100644 index 000000000..f70c0746d --- /dev/null +++ b/packages/interface/src/ui/Link.tsx @@ -0,0 +1,61 @@ +import clsx from 'clsx' +import { ArrowSquareOut } from 'phosphor-react' +import { ComponentProps } from 'react' +import { Link as RouterLink, LinkProps as RouterLinkProps } from 'react-router-dom' + +type LinkBaseProps = { + isExternal?: boolean + noUnderline?: boolean +} + +// either a react-router-dom link or an anchor tag +export type LinkProps = LinkBaseProps & (RouterLinkProps | ComponentProps<'a'>) + +export default function Link({ isExternal, noUnderline, ...props }: LinkProps) { + const { children, className, title, ...rest } = props + + const content = ( + <> + {children} + {isExternal && } + + ) + + const getLinkProps = () => { + return { + className: clsx(className, { 'hover:underline': !noUnderline }, 'flex items-center'), + rel: isExternal ? 'noopener noreferrer' : undefined, + target: isExternal ? '_blank' : undefined, + title, + } + } + + // if the props contain a `to` prop, it's a react-router-dom link + if ('to' in rest) { + return ( + + {content} + + ) + } + + return ( + + {content} + + ) + + // return ( + // + // {children} + + // {isExternal && } + // + // ); +} diff --git a/common/interface/src/ui/MoreLink.tsx b/packages/interface/src/ui/MoreLink.tsx similarity index 65% rename from common/interface/src/ui/MoreLink.tsx rename to packages/interface/src/ui/MoreLink.tsx index 003540e40..b80dbf2e8 100644 --- a/common/interface/src/ui/MoreLink.tsx +++ b/packages/interface/src/ui/MoreLink.tsx @@ -1,9 +1,8 @@ -import { HStack, useColorModeValue } from '@chakra-ui/react'; -import { ArrowRight } from 'phosphor-react'; -import React from 'react'; +import { HStack, useColorModeValue } from '@chakra-ui/react' +import { ArrowRight } from 'phosphor-react' interface Props { - href: string; + href: string } export default function MoreLink({ href }: Props) { @@ -17,5 +16,5 @@ export default function MoreLink({ href }: Props) { > More - ); + ) } diff --git a/common/interface/src/ui/Pagination.tsx b/packages/interface/src/ui/Pagination.tsx similarity index 73% rename from common/interface/src/ui/Pagination.tsx rename to packages/interface/src/ui/Pagination.tsx index 441a00065..661e7939c 100644 --- a/common/interface/src/ui/Pagination.tsx +++ b/packages/interface/src/ui/Pagination.tsx @@ -1,21 +1,23 @@ -import clsx from 'clsx'; -import { ArrowLeft, ArrowRight, DotsThree } from 'phosphor-react'; -import { useMemo } from 'react'; -import { Link, useLocation, useNavigate } from 'react-router-dom'; -import { useWindowSize } from 'rooks'; +import { Box, Flex, HStack, useColorModeValue } from '@chakra-ui/react' +import clsx from 'clsx' +import { ArrowLeft, ArrowRight, DotsThree } from 'phosphor-react' +import { useMemo } from 'react' +import { Link, useLocation, useNavigate } from 'react-router-dom' +import { useWindowSize } from 'rooks' -import { Box, Flex, HStack, useColorModeValue } from '@chakra-ui/react'; - -import { usePagination } from '../hooks/usePagination'; -import PagePopoverForm from '../components/PagePopoverForm'; +import PagePopoverForm from '../components/PagePopoverForm' +import { usePagination } from '../hooks/usePagination' interface PaginationArrowProps { - kind: 'previous' | 'next'; - isDisabled?: boolean; - href: string; + kind: 'previous' | 'next' + isDisabled?: boolean + href: string } function PaginationArrow({ kind, isDisabled, href }: PaginationArrowProps) { + const disabledText = useColorModeValue('gray.300', 'gray.500') + const textColor = useColorModeValue('gray.600', 'gray.300') + return ( {kind === 'previous' ? ( @@ -51,16 +49,18 @@ function PaginationArrow({ kind, isDisabled, href }: PaginationArrowProps) { )} - ); + ) } interface PaginationLinkProps { - href: string; - value: number; - isActive: boolean; + href: string + value: number + isActive: boolean } function PaginationLink({ value, href, isActive }: PaginationLinkProps) { + const nonActiveColor = useColorModeValue('gray.550', 'gray.300') + const nonActiveBorder = useColorModeValue('gray.300', 'gray.600') return ( {value} - ); + ) } export interface PaginationProps { - position?: 'top' | 'bottom'; - pages: number; - currentPage: number; + position?: 'top' | 'bottom' + pages: number + currentPage: number } export default function Pagination({ position = 'top', pages, currentPage }: PaginationProps) { - const navigate = useNavigate(); - const location = useLocation(); + const navigate = useNavigate() + const location = useLocation() - const { innerWidth: screenWidth } = useWindowSize(); + const { innerWidth: screenWidth } = useWindowSize() const numbersToShow = useMemo(() => { if (screenWidth != null) { if (screenWidth < 768) { - return 5; + return 5 } if (screenWidth < 992) { - return 7; + return 7 } } - return 10; - }, [screenWidth]); + return 10 + }, [screenWidth]) - const { pageRange } = usePagination({ totalPages: pages, currentPage, numbersToShow }); + const { pageRange } = usePagination({ currentPage, numbersToShow, totalPages: pages }) function handleEllipsisNavigate(page: number) { - navigate(`${location.pathname}?page=${page}`); + navigate(`${location.pathname}?page=${page}`) } return ( @@ -136,7 +136,7 @@ export default function Pagination({ position = 'top', pages, currentPage }: Pag isActive={page === currentPage} value={page} /> - ); + ) } return ( @@ -156,7 +156,7 @@ export default function Pagination({ position = 'top', pages, currentPage }: Pag } /> - ); + ) })} @@ -166,5 +166,5 @@ export default function Pagination({ position = 'top', pages, currentPage }: Pag isDisabled={currentPage >= pages} /> - ); + ) } diff --git a/packages/interface/src/ui/ReadMore.tsx b/packages/interface/src/ui/ReadMore.tsx new file mode 100644 index 000000000..98b838502 --- /dev/null +++ b/packages/interface/src/ui/ReadMore.tsx @@ -0,0 +1,44 @@ +import { Text, TextProps, useBoolean } from '@chakra-ui/react' + +import { DEBUG_ENV } from '..' + +interface Props extends Omit { + text?: string | null +} + +// TODO: markdown rendering... will probably fix below FIXME, as well. +// FIXME: does not render new lines properly, this is pretty basic and needs changing. +export default function ReadMore({ text, ...props }: Props) { + const [showingAll, { toggle }] = useBoolean(false) + + const resolvedText = text ?? DEBUG_ENV ? DEBUG_FAKE_TEXT : ('' as string) + const canReadMore = resolvedText.length > 250 + + if (!resolvedText && !DEBUG_ENV) { + return null + } + + if (!canReadMore) { + return {resolvedText} + } + + return ( + + {showingAll ? resolvedText : resolvedText.slice(0, 250)} + + {showingAll ? ' Read less' : '... Read more'} + + + ) +} + +const DEBUG_FAKE_TEXT = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed varius semper dolor, eget egestas velit porta ut. \ + Integer blandit lectus nisi, a suscipit eros malesuada eu. Praesent vel sodales ipsum, ut porttitor erat. Aliquam faucibus erat a ante \ + consectetur imperdiet. Curabitur in est ac nisi feugiat facilisis a in nisi. Ut auctor rutrum nibh a tincidunt. Proin non hendrerit risus, \ + sagittis malesuada odio. Phasellus condimentum hendrerit libero nec ultrices.\ + Praesent lacinia, magna vel sodales tempus, tellus metus ultricies odio, non porttitor lectus tortor ac ante. \ + Nullam malesuada nec massa eget facilisis. Aenean in nisi lacus. Etiam et tortor vel lacus maximus imperdiet. Fusce \ + scelerisque dapibus fermentum. Nunc non mauris rhoncus neque tincidunt convallis id et nisl. Donec lobortis at lectus quis venenatis. \ + Ut lacus urna, accumsan sed nisl eget, auctor auctor massa. Duis scelerisque aliquam scelerisque. In hac habitasse platea dictumst. Suspendisse \ + consequat nisi nec enim finibus, sit amet gravida sem ultrices. Vestibulum feugiat erat et tincidunt pellentesque. Sed interdum mi ac quam convallis lobortis.' diff --git a/common/interface/src/ui/Tabs.tsx b/packages/interface/src/ui/Tabs.tsx similarity index 79% rename from common/interface/src/ui/Tabs.tsx rename to packages/interface/src/ui/Tabs.tsx index 7db238acd..663a26f4f 100644 --- a/common/interface/src/ui/Tabs.tsx +++ b/packages/interface/src/ui/Tabs.tsx @@ -1,4 +1,4 @@ -import { forwardRef, TabProps, Tab as ChakraTab } from '@chakra-ui/react'; +import { forwardRef, Tab as ChakraTab, TabProps } from '@chakra-ui/react' export const Tab = forwardRef((props: TabProps, ref) => { // this worked like DOODY @@ -17,5 +17,5 @@ export const Tab = forwardRef((props: TabProps, ref) => { outline: 'none', }} /> - ); -}); + ) +}) diff --git a/common/interface/src/ui/TextArea.tsx b/packages/interface/src/ui/TextArea.tsx similarity index 92% rename from common/interface/src/ui/TextArea.tsx rename to packages/interface/src/ui/TextArea.tsx index 87dd41da8..bae6766fb 100644 --- a/common/interface/src/ui/TextArea.tsx +++ b/packages/interface/src/ui/TextArea.tsx @@ -1,7 +1,7 @@ -import { forwardRef, Textarea, TextareaProps } from '@chakra-ui/react'; +import { forwardRef, Textarea, TextareaProps } from '@chakra-ui/react' interface Props extends TextareaProps { - fullWidth?: boolean; + fullWidth?: boolean } export default forwardRef(({ fullWidth = true, ...props }, ref) => { @@ -18,5 +18,5 @@ export default forwardRef(({ fullWidth = true, ...props }, re border: 'rgba(196, 130, 89, 0.4);', }} /> - ); -}); + ) +}) diff --git a/common/interface/src/ui/ToolTip.tsx b/packages/interface/src/ui/ToolTip.tsx similarity index 80% rename from common/interface/src/ui/ToolTip.tsx rename to packages/interface/src/ui/ToolTip.tsx index 745ed14ae..ba37553ad 100644 --- a/common/interface/src/ui/ToolTip.tsx +++ b/packages/interface/src/ui/ToolTip.tsx @@ -2,9 +2,9 @@ import { Tooltip as ChakraToolTip, TooltipProps as ChakraToolTipProps, useColorModeValue, -} from '@chakra-ui/react'; +} from '@chakra-ui/react' -export interface ToolTipProps extends ChakraToolTipProps {} +export type ToolTipProps = ChakraToolTipProps export default function ToolTip(props: ToolTipProps) { return ( @@ -18,5 +18,5 @@ export default function ToolTip(props: ToolTipProps) { fontSize="xs" openDelay={300} /> - ); + ) } diff --git a/common/interface/src/ui/table/Pagination.tsx b/packages/interface/src/ui/table/Pagination.tsx similarity index 63% rename from common/interface/src/ui/table/Pagination.tsx rename to packages/interface/src/ui/table/Pagination.tsx index 0d1bf2ed8..3f71418c4 100644 --- a/common/interface/src/ui/table/Pagination.tsx +++ b/packages/interface/src/ui/table/Pagination.tsx @@ -1,13 +1,14 @@ -import { Button, ButtonGroup } from '@chakra-ui/react'; -import { ArrowLeft, ArrowRight, DotsThree } from 'phosphor-react'; -import { useMemo } from 'react'; -import { useWindowSize } from 'rooks'; -import PagePopoverForm from '../../components/PagePopoverForm'; -import { usePagination } from '../../hooks/usePagination'; -import { PaginationProps } from '../Pagination'; +import { Button, ButtonGroup } from '@chakra-ui/react' +import { ArrowLeft, ArrowRight, DotsThree } from 'phosphor-react' +import { useMemo } from 'react' +import { useWindowSize } from 'rooks' + +import PagePopoverForm from '../../components/PagePopoverForm' +import { usePagination } from '../../hooks/usePagination' +import { PaginationProps } from '../Pagination' interface TablePaginationProps extends Omit { - onPageChange: (page: number) => void; + onPageChange: (page: number) => void } export default function TablePagination({ @@ -15,23 +16,23 @@ export default function TablePagination({ currentPage, onPageChange, }: TablePaginationProps) { - const { innerWidth: screenWidth } = useWindowSize(); + const { innerWidth: screenWidth } = useWindowSize() const numbersToShow = useMemo(() => { if (screenWidth != null) { if (screenWidth < 768) { - return 5; + return 5 } if (screenWidth < 992) { - return 7; + return 7 } } - return 10; - }, [screenWidth]); + return 10 + }, [screenWidth]) - const { pageRange } = usePagination({ totalPages: pages, currentPage, numbersToShow }); + const { pageRange } = usePagination({ currentPage, numbersToShow, totalPages: pages }) return ( @@ -49,7 +50,7 @@ export default function TablePagination({ onClick={() => onPageChange(page)} page={page} /> - ); + ) } return ( @@ -64,23 +65,23 @@ export default function TablePagination({ } /> - ); + ) })} - ); + ) } interface PaginationNumberProps { - page: number; - active: boolean; - onClick: () => void; + page: number + active: boolean + onClick: () => void } // TODO: style -function PaginationNumber({ active, onClick, page }: PaginationNumberProps) { - return ; +function PaginationNumber({ onClick, page }: PaginationNumberProps) { + return } diff --git a/packages/interface/src/ui/table/Table.tsx b/packages/interface/src/ui/table/Table.tsx new file mode 100644 index 000000000..679f3f2ed --- /dev/null +++ b/packages/interface/src/ui/table/Table.tsx @@ -0,0 +1,203 @@ +import { Box, useColorModeValue } from '@chakra-ui/react' +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getFilteredRowModel, + getSortedRowModel, + SortDirection, + SortingState, + TableOptions, + useReactTable, +} from '@tanstack/react-table' +import clsx from 'clsx' +import { SortAscending, SortDescending } from 'phosphor-react' +import { useRef, useState } from 'react' + +import { DebouncedInput } from '../Input' +import TablePagination from './Pagination' + +export interface TableProps { + data: T[] + columns: ColumnDef[] + options: Omit, 'data' | 'columns'> + fullWidth?: boolean + searchable?: boolean + sortable?: boolean +} + +// TODO: loading state +export default function Table({ + data, + columns, + options, + searchable, + sortable, + ...props +}: TableProps) { + const [sorting, setSorting] = useState([]) + + const filterColRef = useRef(null) + const [columnFilters, setColumnFilters] = useState([]) + const [globalFilter, setGlobalFilter] = useState('') + + const table = useReactTable({ + ...options, + columns, + data, + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + onGlobalFilterChange: setGlobalFilter, + onSortingChange: setSorting, + state: { + ...options.state, + columnFilters, + globalFilter, + sorting, + }, + }) + + const headers = [{ header: 'All', id: 'GLOBAL_FILTER' }].concat( + table + .getAllColumns() + .map((col) => col.columns.map((c) => ({ header: c.columnDef.header as string, id: c.id }))) + .flat(), + ) + + const { pageSize, pageIndex } = table.getState().pagination + + const pageCount = table.getPageCount() + const lastIndex = (pageIndex + 1) * pageSize + const firstIndex = lastIndex - (pageSize - 1) + + function handleFilter(value?: string) { + const filterCol = filterColRef.current?.value + if (filterCol === 'GLOBAL_FILTER') { + setGlobalFilter(value || '') + } else if (filterCol) { + table.getColumn(filterCol).setFilterValue(value) + } + } + + return ( + +
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + return ( + + {row.getVisibleCells().map((cell) => { + return ( + + ) + })} + + ) + })} + +
+
+ {flexRender(header.column.columnDef.header, header.getContext())} + {sortable && ( + + )} +
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+
+
+ +
+ Showing {firstIndex} to {lastIndex} +
+ of {table.getPageCount() * pageSize} +
+ + + + {/* FIXME: scuffed */} + {searchable && ( +
+ handleFilter(value)} + size="sm" + rounded="md" + /> +
+ +
+
+ )} +
+ + table.setPageIndex(page)} + /> +
+ + ) +} + +function SortIcon({ direction }: { direction: 'asc' | 'desc' | null }) { + if (!direction) { + return null + } + + return ( + {direction === 'asc' ? : } + ) +} diff --git a/common/interface/src/utils/epubTheme.ts b/packages/interface/src/utils/epubTheme.ts similarity index 87% rename from common/interface/src/utils/epubTheme.ts rename to packages/interface/src/utils/epubTheme.ts index 5ffb37c4f..72b257830 100644 --- a/common/interface/src/utils/epubTheme.ts +++ b/packages/interface/src/utils/epubTheme.ts @@ -1,17 +1,15 @@ -import React from 'react'; - export interface EpubTheme { - [tag: string]: object; + [tag: string]: object } // Note: Not React CSS, has to be true CSS fields. E.g. font-size not fontSize. export const epubDarkTheme: EpubTheme = { - body: { background: '#212836' }, a: { color: '#4299E1' }, - p: { color: '#E8EDF4', 'font-size': 'unset' }, + body: { background: '#212836' }, h1: { color: '#E8EDF4' }, h2: { color: '#E8EDF4' }, h3: { color: '#E8EDF4' }, h4: { color: '#E8EDF4' }, h5: { color: '#E8EDF4' }, -}; + p: { color: '#E8EDF4', 'font-size': 'unset' }, +} diff --git a/common/interface/src/utils/format.ts b/packages/interface/src/utils/format.ts similarity index 69% rename from common/interface/src/utils/format.ts rename to packages/interface/src/utils/format.ts index 6125abf8e..df080b2d5 100644 --- a/common/interface/src/utils/format.ts +++ b/packages/interface/src/utils/format.ts @@ -1,38 +1,38 @@ // Note: ~technically~ 1024 is correct, but it always differed from what my computer natively reported. // I care more about matching a users reported byte conversion, and 1000 seems to do the trick // for me in my testing. -const KILOBYTE = 1000; -const BYTE_UNITS = ['B', 'KB', 'MiB', 'GB', 'TB']; +const KILOBYTE = 1000 +const BYTE_UNITS = ['B', 'KB', 'MiB', 'GB', 'TB'] /** * Returns a formatted string for converted bytes and unit of measurement. */ export function formatBytes(bytes?: number | bigint, decimals = 2, zeroUnit = 'GB') { - if (bytes == undefined) return null; + if (bytes == undefined) return null - let precision = decimals >= 0 ? decimals : 0; + const precision = decimals >= 0 ? decimals : 0 if (bytes === 0) { - return parseFloat((0).toFixed(precision)) + ' ' + zeroUnit; + return parseFloat((0).toFixed(precision)) + ' ' + zeroUnit } if (typeof bytes !== 'bigint') { - let threshold = Math.floor(Math.log(bytes) / Math.log(KILOBYTE)); + const threshold = Math.floor(Math.log(bytes) / Math.log(KILOBYTE)) return ( parseFloat((bytes / Math.pow(KILOBYTE, threshold)).toFixed(precision)) + ' ' + BYTE_UNITS[threshold] - ); + ) } else { // FIXME: I don't think this is safe!!! - let threshold = Math.floor(Math.log(Number(bytes)) / Math.log(KILOBYTE)); + const threshold = Math.floor(Math.log(Number(bytes)) / Math.log(KILOBYTE)) return ( parseFloat((Number(bytes) / Math.pow(KILOBYTE, threshold)).toFixed(precision)) + ' ' + BYTE_UNITS[threshold] - ); + ) } } @@ -40,31 +40,31 @@ export function formatBytes(bytes?: number | bigint, decimals = 2, zeroUnit = 'G * Returns an object containing the converted bytes and the unit of measurement. */ export function formatBytesSeparate(bytes?: number | bigint, decimals = 2, zeroUnit = 'GB') { - if (bytes == undefined) return null; + if (bytes == undefined) return null - let precision = decimals >= 0 ? decimals : 0; + const precision = decimals >= 0 ? decimals : 0 if (bytes === 0) { return { - value: parseFloat((0).toFixed(precision)), unit: zeroUnit, - }; + value: parseFloat((0).toFixed(precision)), + } } if (typeof bytes !== 'bigint') { - let threshold = Math.floor(Math.log(bytes) / Math.log(KILOBYTE)); + const threshold = Math.floor(Math.log(bytes) / Math.log(KILOBYTE)) return { - value: parseFloat((bytes / Math.pow(KILOBYTE, threshold)).toFixed(precision)), unit: BYTE_UNITS[threshold], - }; + value: parseFloat((bytes / Math.pow(KILOBYTE, threshold)).toFixed(precision)), + } } else { // FIXME: I don't think this is safe!!! - let threshold = Math.floor(Math.log(Number(bytes)) / Math.log(KILOBYTE)); + const threshold = Math.floor(Math.log(Number(bytes)) / Math.log(KILOBYTE)) return { - value: parseFloat((Number(bytes) / Math.pow(KILOBYTE, threshold)).toFixed(precision)), unit: BYTE_UNITS[threshold], - }; + value: parseFloat((Number(bytes) / Math.pow(KILOBYTE, threshold)).toFixed(precision)), + } } } diff --git a/common/interface/src/utils/misc.ts b/packages/interface/src/utils/misc.ts similarity index 53% rename from common/interface/src/utils/misc.ts rename to packages/interface/src/utils/misc.ts index 111384e0b..2153af0a3 100644 --- a/common/interface/src/utils/misc.ts +++ b/packages/interface/src/utils/misc.ts @@ -1,3 +1,3 @@ export async function copyTextToClipboard(text: string) { - return await navigator?.clipboard.writeText(text); + return await navigator?.clipboard.writeText(text) } diff --git a/packages/interface/src/utils/patterns.ts b/packages/interface/src/utils/patterns.ts new file mode 100644 index 000000000..8272aa0e9 --- /dev/null +++ b/packages/interface/src/utils/patterns.ts @@ -0,0 +1,3 @@ +export const ARCHIVE_EXTENSION = /cbr|cbz|zip|rar/ +// export const EBOOK_EXTENSION = /epub|mobi/; +export const EBOOK_EXTENSION = /epub/ diff --git a/common/interface/src/utils/pluralize.ts b/packages/interface/src/utils/pluralize.ts similarity index 55% rename from common/interface/src/utils/pluralize.ts rename to packages/interface/src/utils/pluralize.ts index e38bf973b..d86ff5fc5 100644 --- a/common/interface/src/utils/pluralize.ts +++ b/packages/interface/src/utils/pluralize.ts @@ -1,4 +1,4 @@ -import pluralize from 'pluralize'; +import pluralize from 'pluralize' /** * Creates a simple phrase for a statistic in the correct form, e.g. '1 item' or '2 items' @@ -6,6 +6,6 @@ import pluralize from 'pluralize'; * @param word the word to pluralize * @param count the number of items (word) to use in the stat */ -export default function pluralizeStat(word: string, count: number = 0): string { - return `${count} ${pluralize(word, count)}`; +export default function pluralizeStat(word: string, count = 0): string { + return `${count} ${pluralize(word, count)}` } diff --git a/packages/interface/src/utils/prefetch.ts b/packages/interface/src/utils/prefetch.ts new file mode 100644 index 000000000..18a5a9f7e --- /dev/null +++ b/packages/interface/src/utils/prefetch.ts @@ -0,0 +1,8 @@ +import { getMediaPage } from '@stump/api' + +export function prefetchMediaPage(mediaId: string, page: number): HTMLImageElement { + const img = new Image() + img.src = getMediaPage(mediaId, page) + + return img +} diff --git a/common/interface/src/utils/restricted.ts b/packages/interface/src/utils/restricted.ts similarity index 75% rename from common/interface/src/utils/restricted.ts rename to packages/interface/src/utils/restricted.ts index f40782ac4..63ffb64b9 100644 --- a/common/interface/src/utils/restricted.ts +++ b/packages/interface/src/utils/restricted.ts @@ -1,7 +1,7 @@ -import { toast } from 'react-hot-toast'; +import { toast } from 'react-hot-toast' -export const RESTRICTED_MODE = Boolean(import.meta.env.RESTRICTED_MODE); +export const RESTRICTED_MODE = Boolean(import.meta.env.RESTRICTED_MODE) export function restrictedToast() { - toast.error('This action is not available in restricted mode 😓'); + toast.error('This action is not available in restricted mode 😓') } diff --git a/packages/interface/tailwind.config.js b/packages/interface/tailwind.config.js new file mode 100644 index 000000000..d30a40a18 --- /dev/null +++ b/packages/interface/tailwind.config.js @@ -0,0 +1,2 @@ +// Note: this isn't used, but needed for intellisense. >:( +module.exports = require('../components/tailwind.js')('web') diff --git a/packages/interface/tsconfig.json b/packages/interface/tsconfig.json new file mode 100644 index 000000000..5cf102a6c --- /dev/null +++ b/packages/interface/tsconfig.json @@ -0,0 +1,47 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": [ + "vite/client", + "node" + ], + "outDir": "../../.moon/cache/types/packages/interface", + "skipLibCheck": true, + "jsx": "preserve", + "paths": { + "@stump/api": [ + "../api/src/index.ts" + ], + "@stump/api/*": [ + "../api/src/*" + ], + "@stump/client": [ + "../client/src/index.ts" + ], + "@stump/client/*": [ + "../client/src/*" + ], + "@stump/types": [ + "../types/index.ts" + ], + "@stump/types/*": [ + "../types/*" + ] + }, + "resolveJsonModule": true + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../api" + }, + { + "path": "../client" + }, + { + "path": "../types" + } + ] +} diff --git a/packages/prisma-cli/Cargo.toml b/packages/prisma-cli/Cargo.toml new file mode 100644 index 000000000..7d621ef7e --- /dev/null +++ b/packages/prisma-cli/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "prisma-cli" +version = "0.1.0" +edition = "2021" + +[dependencies] +prisma-client-rust-cli = { workspace = true } \ No newline at end of file diff --git a/core/prisma/src/main.rs b/packages/prisma-cli/src/main.rs similarity index 100% rename from core/prisma/src/main.rs rename to packages/prisma-cli/src/main.rs diff --git a/packages/types/core.ts b/packages/types/core.ts new file mode 100644 index 000000000..04595e00a --- /dev/null +++ b/packages/types/core.ts @@ -0,0 +1,88 @@ +// DO NOT MODIFY THIS FILE, IT IS AUTOGENERATED + +export interface StumpVersion { semver: string, rev: string | null, compile_time: string } + +export interface User { id: string, username: string, role: string, user_preferences: UserPreferences | null } + +export type UserRole = "SERVER_OWNER" | "MEMBER" + +export interface UserPreferences { id: string, locale: string, library_layout_mode: string, series_layout_mode: string, collection_layout_mode: string } + +export interface UpdateUserArgs { username: string, password: string | null } + +export interface UserPreferencesUpdate { id: string, locale: string, library_layout_mode: string, series_layout_mode: string, collection_layout_mode: string } + +export interface LoginOrRegisterArgs { username: string, password: string } + +export interface ClaimResponse { is_claimed: boolean } + +export type FileStatus = "UNKNOWN" | "READY" | "UNSUPPORTED" | "ERROR" | "MISSING" + +export interface Library { id: string, name: string, description: string | null, path: string, status: string, updated_at: string, series: Array | null, tags: Array | null, library_options: LibraryOptions } + +export type LibraryPattern = "SERIES_BASED" | "COLLECTION_BASED" + +export type LibraryScanMode = "SYNC" | "BATCHED" | "NONE" + +export interface LibraryOptions { id: string | null, convert_rar_to_zip: boolean, hard_delete_conversions: boolean, create_webp_thumbnails: boolean, library_pattern: LibraryPattern, library_id: string | null } + +export interface CreateLibraryArgs { name: string, path: string, description: string | null, tags: Array | null, scan_mode: LibraryScanMode | null, library_options: LibraryOptions | null } + +export interface UpdateLibraryArgs { id: string, name: string, path: string, description: string | null, tags: Array | null, removed_tags: Array | null, library_options: LibraryOptions, scan_mode: LibraryScanMode | null } + +export interface LibrariesStats { series_count: bigint, book_count: bigint, total_bytes: bigint } + +export interface Series { id: string, name: string, path: string, description: string | null, status: FileStatus, updated_at: string, created_at: string, library_id: string, library: Library | null, media: Array | null, media_count?: bigint, unread_media_count?: bigint, tags: Array | null } + +export interface Media { id: string, name: string, description: string | null, size: number, extension: string, pages: number, updated_at: string, created_at: string, modified_at: string, checksum: string | null, path: string, status: FileStatus, series_id: string, series?: Series, read_progresses?: Array, current_page?: number, is_completed?: boolean, tags?: Array } + +export interface MediaMetadata { Series: string | null, Number: number | null, Web: string | null, Summary: string | null, Publisher: string | null, Genre: string | null, PageCount: number | null } + +export interface ReadProgress { id: string, page: number, is_completed: boolean, media_id: string, media: Media | null, user_id: string, user: User | null } + +export interface Tag { id: string, name: string } + +export type LayoutMode = "GRID" | "LIST" + +export interface Epub { media_entity: Media, spine: Array, resources: Record, toc: Array, metadata: Record>, root_base: string, root_file: string, extra_css: Array } + +export interface EpubContent { label: string, content: string, play_order: number } + +export type JobStatus = "RUNNING" | "QUEUED" | "COMPLETED" | "CANCELLED" | "FAILED" + +export interface JobUpdate { runner_id: string, current_task: bigint | null, task_count: bigint, message: string | null, status: JobStatus | null } + +export interface JobReport { id: string | null, kind: string, details: string | null, status: JobStatus, task_count: number | null, completed_task_count: number | null, ms_elapsed: bigint | null, completed_at: string | null } + +export type CoreEvent = { key: "JobStarted", data: JobUpdate } | { key: "JobProgress", data: JobUpdate } | { key: "JobComplete", data: string } | { key: "JobFailed", data: { runner_id: string, message: string } } | { key: "CreateEntityFailed", data: { runner_id: string | null, path: string, message: string } } | { key: "CreatedMedia", data: Media } | { key: "CreatedMediaBatch", data: bigint } | { key: "CreatedSeries", data: Series } | { key: "CreatedSeriesBatch", data: bigint } + +export interface ReadingList { id: string, name: string, creating_user_id: string, description: string | null, media: Array | null } + +export interface CreateReadingList { id: string, media_ids: Array } + +export interface DirectoryListing { parent: string | null, files: Array } + +export interface DirectoryListingFile { is_directory: boolean, name: string, path: string } + +export interface DirectoryListingInput { path: string | null } + +export interface Log { id: string, level: LogLevel, message: string, created_at: string, job_id: string | null } + +export interface LogMetadata { path: string, size: bigint, modified: string } + +export type LogLevel = "ERROR" | "WARN" | "INFO" | "DEBUG" + +export type Direction = "asc" | "desc" + +export interface PageParams { zero_based: boolean, page: number, page_size: number } + +export interface QueryOrder { order_by: string, direction: Direction } + +export interface PageQuery { zero_based: boolean | null, page: number | null, page_size: number | null } + +export interface CursorQuery { cursor: string | null, limit: bigint | null } + +export interface PageInfo { total_pages: number, current_page: number, page_size: number, page_offset: number, zero_based: boolean } + +export type Pagination = null | PageQuery | CursorQuery + diff --git a/common/client/src/types/index.ts b/packages/types/index.ts similarity index 77% rename from common/client/src/types/index.ts rename to packages/types/index.ts index f30168de1..a256ada77 100644 --- a/common/client/src/types/index.ts +++ b/packages/types/index.ts @@ -1,7 +1,4 @@ -import { PageInfo, Media, Series, Library } from './core'; -import { ApiError } from './server'; - -export type ApiResult = import('axios').AxiosResponse>; +import { Library, Media, PageInfo, Series } from './core' export enum FileStatus { Unknown = 'UNKNOWN', @@ -13,58 +10,56 @@ export enum FileStatus { export interface Pageable { // The target data being returned. - data: T; + data: T // The pagination information (if paginated). - _page?: PageInfo; + _page?: PageInfo } -export type PageableApiResult = ApiResult>; - // Note: I am separating these options / exclusions in case I want to use either independently. export type MediaOrderByExclusions = Extract< keyof Media, 'currentPage' | 'series' | 'readProgresses' | 'tags' | 'id' ->; -export type MediaOrderByOptions = Partial>; +> +export type MediaOrderByOptions = Partial> // TODO: I HATE THIS export const mediaOrderByOptions: MediaOrderByOptions = { - name: undefined, + checksum: undefined, description: undefined, - size: undefined, extension: undefined, + name: undefined, pages: undefined, - updated_at: undefined, - checksum: undefined, path: undefined, - status: undefined, series_id: undefined, -}; + size: undefined, + status: undefined, + updated_at: undefined, +} export type SeriesOrderByExclusions = Extract< keyof Series, 'library' | 'media' | 'mediaCount' | 'tags' ->; -export type SeriesOrderByOptions = Partial>; +> +export type SeriesOrderByOptions = Partial> // TODO: I HATE THIS export const seriesOrderByOptions: SeriesOrderByOptions = { - name: undefined, description: undefined, - updated_at: undefined, + library_id: undefined, + name: undefined, path: undefined, status: undefined, - library_id: undefined, -}; + updated_at: undefined, +} -export type LibraryOrderByExclusions = Extract; -export type LibraryOrderByOptions = Partial>; +export type LibraryOrderByExclusions = Extract +export type LibraryOrderByOptions = Partial> // TODO: I HATE THIS export const libraryOrderByOptions: LibraryOrderByOptions = { - name: undefined, description: undefined, - updated_at: undefined, + name: undefined, path: undefined, status: undefined, -}; + updated_at: undefined, +} -export * from './core'; -export * from './server'; +export * from './core' +export * from './server' diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 000000000..ab8d7c6ba --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,14 @@ +{ + "name": "@stump/types", + "version": "0.0.0", + "description": "", + "main": "index.ts", + "exports": { + ".": "./index.ts", + "./server": "./server.ts" + }, + "license": "MIT", + "devDependencies": { + "typescript": "^4.9.5" + } +} diff --git a/common/client/src/types/server.ts b/packages/types/server.ts similarity index 91% rename from common/client/src/types/server.ts rename to packages/types/server.ts index 1f3c49477..0c4789abf 100644 --- a/common/client/src/types/server.ts +++ b/packages/types/server.ts @@ -10,4 +10,4 @@ export type ApiError = | { code: 'ServiceUnavailable'; details: string } | { code: 'BadGateway'; details: string } | { code: 'Unknown'; details: string } - | { code: 'Redirect'; details: string }; + | { code: 'Redirect'; details: string } diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json new file mode 100644 index 000000000..9682c03e3 --- /dev/null +++ b/packages/types/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "skipLibCheck": true, + "outDir": "../../.moon/cache/types/packages/types" + }, + "include": [ + "*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba3e2cc13..8eebedc8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,67 +1,100 @@ lockfileVersion: 5.4 +overrides: + esbuild: ^0.15.13 + importers: .: specifiers: - concurrently: ^7.4.0 + '@babel/core': ^7.20.12 + '@moonrepo/cli': ^0.21.4 + '@typescript-eslint/eslint-plugin': ^5.52.0 + '@typescript-eslint/parser': ^5.52.0 + babel-preset-moon: ^1.1.4 + concurrently: ^7.6.0 cpy-cli: ^4.2.0 - husky: ^8.0.1 - lint-staged: ^13.0.3 + eslint: ^8.34.0 + eslint-config-prettier: ^8.6.0 + eslint-plugin-prettier: ^4.2.1 + eslint-plugin-react: ^7.32.2 + eslint-plugin-react-hooks: ^4.6.0 + eslint-plugin-simple-import-sort: ^8.0.0 + eslint-plugin-sort-keys-fix: ^1.1.2 + husky: ^8.0.3 + lint-staged: ^13.1.2 move-cli: 2.0.0 - prettier: ^2.7.1 + prettier: ^2.8.4 + prettier-eslint: ^15.0.1 trash-cli: ^5.0.0 + tsconfig-moon: ^1.2.1 + typescript: ^4.9.5 devDependencies: - concurrently: 7.4.0 + '@babel/core': 7.20.12 + '@moonrepo/cli': 0.21.4 + '@typescript-eslint/eslint-plugin': 5.52.0_6cfvjsbua5ptj65675bqcn6oza + '@typescript-eslint/parser': 5.52.0_7kw3g6rralp5ps6mg3uyzz6azm + babel-preset-moon: 1.1.4_@babel+core@7.20.12 + concurrently: 7.6.0 cpy-cli: 4.2.0 - husky: 8.0.1 - lint-staged: 13.0.3 + eslint: 8.34.0 + eslint-config-prettier: 8.6.0_eslint@8.34.0 + eslint-plugin-prettier: 4.2.1_u5wnrdwibbfomslmnramz52buy + eslint-plugin-react: 7.32.2_eslint@8.34.0 + eslint-plugin-react-hooks: 4.6.0_eslint@8.34.0 + eslint-plugin-simple-import-sort: 8.0.0_eslint@8.34.0 + eslint-plugin-sort-keys-fix: 1.1.2 + husky: 8.0.3 + lint-staged: 13.1.2 move-cli: 2.0.0 - prettier: 2.7.1 + prettier: 2.8.4 + prettier-eslint: 15.0.1 trash-cli: 5.0.0 + tsconfig-moon: 1.2.1 + typescript: 4.9.5 apps/desktop: specifiers: '@stump/client': workspace:* '@stump/interface': workspace:* - '@tailwindcss/typography': ^0.5.7 - '@tanstack/react-query': ^4.10.3 - '@tauri-apps/api': ^1.1.0 - '@tauri-apps/cli': ^1.1.1 - '@types/react': ^18.0.21 - '@types/react-dom': ^18.0.6 + '@tailwindcss/typography': ^0.5.9 + '@tanstack/react-query': ^4.20.4 + '@tauri-apps/api': ^1.2.0 + '@tauri-apps/cli': ^1.2.3 + '@types/react': ^18.0.28 + '@types/react-dom': ^18.0.11 '@vitejs/plugin-react': ^2.0.0 autoprefixer: ^10.4.12 - postcss: ^8.4.17 + postcss: ^8.4.21 react: ^18.2.0 react-dom: ^18.2.0 tailwind: ^4.0.0 tailwind-scrollbar-hide: ^1.1.7 - tailwindcss: ^3.1.8 - typescript: ^4.8.4 - vite: ^3.1.6 + tailwindcss: ^3.2.7 + typescript: ^4.9.5 + vite: ^3.2.5 vite-plugin-tsconfig-paths: ^1.1.0 dependencies: - '@stump/client': link:../../common/client - '@stump/interface': link:../../common/interface - '@tanstack/react-query': 4.10.3_biqbaboplfbrettd7655fr4n2y - '@tauri-apps/api': 1.1.0 + '@stump/client': link:../../packages/client + '@stump/interface': link:../../packages/interface + '@tanstack/react-query': 4.24.9_biqbaboplfbrettd7655fr4n2y + '@tauri-apps/api': 1.2.0 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 devDependencies: - '@tailwindcss/typography': 0.5.7_tailwindcss@3.1.8 - '@tauri-apps/cli': 1.1.1 - '@types/react': 18.0.21 - '@types/react-dom': 18.0.6 - '@vitejs/plugin-react': 2.1.0_vite@3.1.6 - autoprefixer: 10.4.12_postcss@8.4.17 - postcss: 8.4.17 + '@tailwindcss/typography': 0.5.9_tailwindcss@3.2.7 + '@tauri-apps/cli': 1.2.3 + '@types/react': 18.0.28 + '@types/react-dom': 18.0.11 + '@vitejs/plugin-react': 2.2.0_vite@3.2.5 + autoprefixer: 10.4.13_postcss@8.4.21 + postcss: 8.4.21 tailwind: 4.0.0 tailwind-scrollbar-hide: 1.1.7 - tailwindcss: 3.1.8 - typescript: 4.8.4 - vite: 3.1.6 - vite-plugin-tsconfig-paths: 1.2.0_2qgi2qwv6eydpccu35h24komdm + tailwindcss: 3.2.7_postcss@8.4.21 + typescript: 4.9.5 + vite: 3.2.5 + vite-plugin-tsconfig-paths: 1.2.0_mmfldfnusamjexuwtlvii3fpxu apps/mobile: specifiers: {} @@ -69,431 +102,1496 @@ importers: apps/server: specifiers: {} - apps/tui: - specifiers: {} - apps/web: specifiers: '@stump/client': workspace:* '@stump/interface': workspace:* - '@tailwindcss/typography': ^0.5.7 - '@types/react': ^18.0.21 - '@types/react-dom': ^18.0.6 - '@vitejs/plugin-react': ^2.0.0 - autoprefixer: ^10.4.12 - postcss: ^8.4.17 + '@tailwindcss/typography': ^0.5.9 + '@types/react': ^18.0.28 + '@types/react-dom': ^18.0.11 + '@types/react-router-dom': ^5.3.3 + '@vitejs/plugin-react': ^2.2.0 + autoprefixer: ^10.4.13 + postcss: ^8.4.21 react: ^18.2.0 react-dom: ^18.2.0 + react-router: ^6.8.1 + react-router-dom: ^6.8.1 tailwind: ^4.0.0 tailwind-scrollbar-hide: ^1.1.7 - tailwindcss: ^3.1.8 - typescript: ^4.8.4 - vite: ^3.1.6 + tailwindcss: ^3.2.7 + typescript: ^4.9.5 + vite: ^3.2.5 vite-plugin-tsconfig-paths: ^1.1.0 dependencies: - '@stump/client': link:../../common/client - '@stump/interface': link:../../common/interface + '@stump/client': link:../../packages/client + '@stump/interface': link:../../packages/interface react: 18.2.0 react-dom: 18.2.0_react@18.2.0 + react-router: 6.8.1_react@18.2.0 + react-router-dom: 6.8.1_biqbaboplfbrettd7655fr4n2y devDependencies: - '@tailwindcss/typography': 0.5.7_tailwindcss@3.1.8 - '@types/react': 18.0.21 - '@types/react-dom': 18.0.6 - '@vitejs/plugin-react': 2.1.0_vite@3.1.6 - autoprefixer: 10.4.12_postcss@8.4.17 - postcss: 8.4.17 + '@tailwindcss/typography': 0.5.9_tailwindcss@3.2.7 + '@types/react': 18.0.28 + '@types/react-dom': 18.0.11 + '@types/react-router-dom': 5.3.3 + '@vitejs/plugin-react': 2.2.0_vite@3.2.5 + autoprefixer: 10.4.13_postcss@8.4.21 + postcss: 8.4.21 tailwind: 4.0.0 tailwind-scrollbar-hide: 1.1.7 - tailwindcss: 3.1.8 - typescript: 4.8.4 - vite: 3.1.6 - vite-plugin-tsconfig-paths: 1.2.0_2qgi2qwv6eydpccu35h24komdm + tailwindcss: 3.2.7_postcss@8.4.21 + typescript: 4.9.5 + vite: 3.2.5 + vite-plugin-tsconfig-paths: 1.2.0_mmfldfnusamjexuwtlvii3fpxu + + core: + specifiers: {} + + packages/api: + specifiers: + '@stump/types': workspace:* + axios: ^1.3.3 + typescript: ^4.9.5 + dependencies: + '@stump/types': link:../types + axios: 1.3.3 + devDependencies: + typescript: 4.9.5 - common/client: + packages/client: specifiers: - '@stump/config': workspace:* - '@tanstack/react-query': ^4.10.3 + '@stump/api': workspace:* + '@stump/types': workspace:* + '@tanstack/react-query': ^4.20.4 '@types/axios': ^0.14.0 - '@types/node': ^18.8.3 - '@types/react': ^18.0.21 - axios: ^1.1.2 - immer: ^9.0.15 - react-use-websocket: ^4.2.0 + '@types/node': ^18.14.0 + '@types/react': ^18.0.28 + axios: ^1.3.3 + immer: ^9.0.19 + prettier: ^2.8.4 + react: ^18.2.0 + react-use-websocket: ^4.3.1 tsconfig: '*' - typescript: ^4.8.4 - zustand: ^4.1.1 - dependencies: - '@stump/config': link:../config - '@tanstack/react-query': 4.10.3 - axios: 1.1.2 - immer: 9.0.15 - react-use-websocket: 4.2.0 - zustand: 4.1.1_immer@9.0.15 + typescript: ^4.9.5 + zustand: ^4.3.3 + dependencies: + '@stump/api': link:../api + '@stump/types': link:../types + '@tanstack/react-query': 4.24.9_react@18.2.0 + axios: 1.3.3 + immer: 9.0.19 + react-use-websocket: 4.3.1_react@18.2.0 + zustand: 4.3.3_immer@9.0.19+react@18.2.0 devDependencies: '@types/axios': 0.14.0 - '@types/node': 18.8.3 - '@types/react': 18.0.21 + '@types/node': 18.14.0 + '@types/react': 18.0.28 + prettier: 2.8.4 + react: 18.2.0 tsconfig: 7.0.0 - typescript: 4.8.4 + typescript: 4.9.5 - common/config: + packages/components: specifiers: + '@tailwindcss/typography': ^0.5.9 tailwind-scrollbar-hide: ^1.1.7 + typescript: ^4.9.5 devDependencies: + '@tailwindcss/typography': 0.5.9 tailwind-scrollbar-hide: 1.1.7 + typescript: 4.9.5 - common/interface: + packages/interface: specifiers: - '@chakra-ui/react': ^2.3.5 - '@emotion/react': ^11.10.4 - '@emotion/styled': ^11.10.4 - '@hookform/resolvers': ^2.9.8 + '@chakra-ui/react': ^2.5.1 + '@emotion/react': ^11.10.6 + '@emotion/styled': ^11.10.6 + '@hookform/resolvers': ^2.9.11 + '@stump/api': workspace:* '@stump/client': workspace:* - '@tanstack/react-query': ^4.10.3 - '@tanstack/react-query-devtools': ^4.10.4 - '@tanstack/react-table': ^8.5.15 - '@types/node': ^18.8.3 + '@stump/types': workspace:* + '@tanstack/react-query': ^4.20.4 + '@tanstack/react-query-devtools': ^4.20.4 + '@tanstack/react-table': ^8.7.9 + '@tanstack/react-virtual': 3.0.0-beta.18 + '@types/node': ^18.14.0 '@types/nprogress': ^0.2.0 '@types/pluralize': ^0.0.29 - '@types/react': ^18.0.21 - '@types/react-dom': ^18.0.6 - '@types/react-helmet': ^6.1.5 + '@types/react': ^18.0.28 + '@types/react-dom': ^18.0.11 + '@types/react-helmet': ^6.1.6 '@types/react-router-dom': ^5.3.3 - '@vitejs/plugin-react': ^2.1.0 - chakra-react-select: ^4.2.5 + '@vitejs/plugin-react': ^2.2.0 + chakra-react-select: ^4.4.3 + class-variance-authority: ^0.2.4 clsx: ^1.2.1 + dayjs: ^1.11.7 epubjs: ^0.3.93 - framer-motion: ^7.5.3 + framer-motion: ^7.10.3 i18next: ^21.10.0 - immer: ^9.0.15 + immer: ^9.0.19 nprogress: ^0.2.0 phosphor-react: ^1.4.1 pluralize: ^8.0.0 - react: ^18.2.0 react-dom: ^18.2.0 react-error-boundary: ^3.1.4 react-helmet: ^6.1.0 - react-hook-form: ^7.37.0 + react-hook-form: ^7.43.1 react-hot-toast: ^2.4.0 react-hotkeys-hook: ^3.4.7 react-i18next: ^11.18.6 - react-router: ^6.4.2 - react-router-dom: ^6.4.2 + react-router: ^6.8.1 + react-router-dom: ^6.8.1 react-swipeable: ^7.0.0 - rooks: ^7.4.0 - typescript: ^4.8.4 + rooks: ^7.4.3 + typescript: ^4.9.5 use-count-up: ^3.0.1 - vite: ^3.1.6 - zod: ^3.19.1 - zustand: ^4.1.1 - dependencies: - '@chakra-ui/react': 2.3.5_356kgaleomk4jdithf3bcp4v5i - '@emotion/react': 11.10.4_iapumuv4e6jcjznwuxpf4tt22e - '@emotion/styled': 11.10.4_g3tud4ene45llglqap74b5kkse - '@hookform/resolvers': 2.9.8_react-hook-form@7.37.0 + vite: ^3.2.5 + zod: ^3.20.6 + zustand: ^4.3.3 + dependencies: + '@chakra-ui/react': 2.5.1_ja2rgvpkvdku2qustoaipud6ya + '@emotion/react': 11.10.6_@types+react@18.0.28 + '@emotion/styled': 11.10.6_rtgl6lwupdrbo733hg3i5dx32q + '@hookform/resolvers': 2.9.11_react-hook-form@7.43.1 + '@stump/api': link:../api '@stump/client': link:../client - '@tanstack/react-query': 4.10.3_biqbaboplfbrettd7655fr4n2y - '@tanstack/react-query-devtools': 4.10.4_vhepussragssjkevgznguhxexq - '@tanstack/react-table': 8.5.15_biqbaboplfbrettd7655fr4n2y - chakra-react-select: 4.2.5_7ibnqr7ipkokiucezzal4ajjcm + '@stump/types': link:../types + '@tanstack/react-query': 4.24.9_react-dom@18.2.0 + '@tanstack/react-query-devtools': 4.24.9_33a23s46don7ducusvogyrr4cq + '@tanstack/react-table': 8.7.9_react-dom@18.2.0 + '@tanstack/react-virtual': 3.0.0-beta.18 + chakra-react-select: 4.4.3_guv77w3wtq2ixjrrapgt4j5nnm + class-variance-authority: 0.2.4_typescript@4.9.5 clsx: 1.2.1 + dayjs: 1.11.7 epubjs: 0.3.93 - framer-motion: 7.5.3_biqbaboplfbrettd7655fr4n2y + framer-motion: 7.10.3_react-dom@18.2.0 i18next: 21.10.0 - immer: 9.0.15 + immer: 9.0.19 nprogress: 0.2.0 - phosphor-react: 1.4.1_react@18.2.0 + phosphor-react: 1.4.1 pluralize: 8.0.0 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - react-error-boundary: 3.1.4_react@18.2.0 - react-helmet: 6.1.0_react@18.2.0 - react-hook-form: 7.37.0_react@18.2.0 - react-hot-toast: 2.4.0_biqbaboplfbrettd7655fr4n2y - react-hotkeys-hook: 3.4.7_biqbaboplfbrettd7655fr4n2y - react-i18next: 11.18.6_vfm63zmruocgezzfl2v26zlzpy - react-router: 6.4.2_react@18.2.0 - react-router-dom: 6.4.2_biqbaboplfbrettd7655fr4n2y - react-swipeable: 7.0.0_react@18.2.0 - rooks: 7.4.0_biqbaboplfbrettd7655fr4n2y - use-count-up: 3.0.1_react@18.2.0 - zod: 3.19.1 - zustand: 4.1.1_immer@9.0.15+react@18.2.0 + react-dom: 18.2.0 + react-error-boundary: 3.1.4 + react-helmet: 6.1.0 + react-hook-form: 7.43.1 + react-hot-toast: 2.4.0_react-dom@18.2.0 + react-hotkeys-hook: 3.4.7_react-dom@18.2.0 + react-i18next: 11.18.6_dh3esvl7t2cobrubf5ry42ti3i + react-router: 6.8.1 + react-router-dom: 6.8.1_react-dom@18.2.0 + react-swipeable: 7.0.0 + rooks: 7.4.3_react-dom@18.2.0 + use-count-up: 3.0.1 + zod: 3.20.6 + zustand: 4.3.3_immer@9.0.19 devDependencies: - '@types/node': 18.8.3 + '@types/node': 18.14.0 '@types/nprogress': 0.2.0 '@types/pluralize': 0.0.29 - '@types/react': 18.0.21 - '@types/react-dom': 18.0.6 - '@types/react-helmet': 6.1.5 + '@types/react': 18.0.28 + '@types/react-dom': 18.0.11 + '@types/react-helmet': 6.1.6 '@types/react-router-dom': 5.3.3 - '@vitejs/plugin-react': 2.1.0_vite@3.1.6 - typescript: 4.8.4 - vite: 3.1.6 + '@vitejs/plugin-react': 2.2.0_vite@3.2.5 + typescript: 4.9.5 + vite: 3.2.5_@types+node@18.14.0 + + packages/types: + specifiers: + typescript: ^4.9.5 + devDependencies: + typescript: 4.9.5 + +packages: + + /@ampproject/remapping/2.2.0: + resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.1.1 + '@jridgewell/trace-mapping': 0.3.17 + dev: true + + /@babel/code-frame/7.18.6: + resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.18.6 + + /@babel/compat-data/7.20.14: + resolution: {integrity: sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core/7.20.12: + resolution: {integrity: sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.0 + '@babel/code-frame': 7.18.6 + '@babel/generator': 7.20.14 + '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12 + '@babel/helper-module-transforms': 7.20.11 + '@babel/helpers': 7.20.13 + '@babel/parser': 7.20.15 + '@babel/template': 7.20.7 + '@babel/traverse': 7.20.13 + '@babel/types': 7.20.7 + convert-source-map: 1.9.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator/7.20.14: + resolution: {integrity: sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + '@jridgewell/gen-mapping': 0.3.2 + jsesc: 2.5.2 + dev: true + + /@babel/helper-annotate-as-pure/7.18.6: + resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-builder-binary-assignment-operator-visitor/7.18.9: + resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-explode-assignable-expression': 7.18.6 + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-compilation-targets/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.20.14 + '@babel/core': 7.20.12 + '@babel/helper-validator-option': 7.18.6 + browserslist: 4.21.5 + lru-cache: 5.1.1 + semver: 6.3.0 + dev: true + + /@babel/helper-create-class-features-plugin/7.20.12_@babel+core@7.20.12: + resolution: {integrity: sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.12 + '@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 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-create-regexp-features-plugin/7.20.5_@babel+core@7.20.12: + resolution: {integrity: sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-annotate-as-pure': 7.18.6 + regexpu-core: 5.3.1 + dev: true + + /@babel/helper-define-polyfill-provider/0.3.3_@babel+core@7.20.12: + resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} + peerDependencies: + '@babel/core': ^7.4.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + debug: 4.3.4 + lodash.debounce: 4.0.8 + resolve: 1.22.1 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-environment-visitor/7.18.9: + resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-explode-assignable-expression/7.18.6: + resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-function-name/7.19.0: + resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.20.7 + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-hoist-variables/7.18.6: + resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-member-expression-to-functions/7.20.7: + resolution: {integrity: sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-module-imports/7.18.6: + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + + /@babel/helper-module-transforms/7.20.11: + resolution: {integrity: sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==} + engines: {node: '>=6.9.0'} + dependencies: + '@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.13 + '@babel/types': 7.20.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-optimise-call-expression/7.18.6: + resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-plugin-utils/7.20.2: + resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-remap-async-to-generator/7.18.9_@babel+core@7.20.12: + resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-wrap-function': 7.20.5 + '@babel/types': 7.20.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-replace-supers/7.20.7: + resolution: {integrity: sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==} + engines: {node: '>=6.9.0'} + 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.13 + '@babel/types': 7.20.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-simple-access/7.20.2: + resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-skip-transparent-expression-wrappers/7.20.0: + resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-split-export-declaration/7.18.6: + resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-string-parser/7.19.4: + resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-identifier/7.19.1: + resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-option/7.18.6: + resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-wrap-function/7.20.5: + resolution: {integrity: sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-function-name': 7.19.0 + '@babel/template': 7.20.7 + '@babel/traverse': 7.20.13 + '@babel/types': 7.20.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helpers/7.20.13: + resolution: {integrity: sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.20.7 + '@babel/traverse': 7.20.13 + '@babel/types': 7.20.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/highlight/7.18.6: + resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.19.1 + chalk: 2.4.2 + js-tokens: 4.0.0 + + /@babel/parser/7.20.15: + resolution: {integrity: sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.20.12 + '@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+core@7.20.12 + dev: true + + /@babel/plugin-proposal-async-generator-functions/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.20.12 + '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.20.12 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-class-properties/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-create-class-features-plugin': 7.20.12_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-class-static-block/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-create-class-features-plugin': 7.20.12_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.20.12 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-decorators/7.20.13_@babel+core@7.20.12: + resolution: {integrity: sha512-7T6BKHa9Cpd7lCueHBBzP0nkXNina+h5giOZw+a8ZpMfPFY19VjJAjIxyFHuWkhCWgL6QMqRiY/wB1fLXzm6Mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-create-class-features-plugin': 7.20.12_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-replace-supers': 7.20.7 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/plugin-syntax-decorators': 7.19.0_@babel+core@7.20.12 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-dynamic-import/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-export-default-from/7.18.10_@babel+core@7.20.12: + resolution: {integrity: sha512-5H2N3R2aQFxkV4PIBUR/i7PUSwgTZjouJKzI8eKswfIjT0PhvzkPn0t0wIS5zn6maQuvtT0t1oHtMUz61LOuow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-export-default-from': 7.18.6_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-export-namespace-from/7.18.9_@babel+core@7.20.12: + resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-json-strings/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-logical-assignment-operators/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-nullish-coalescing-operator/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-numeric-separator/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-object-rest-spread/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.20.14 + '@babel/core': 7.20.12 + '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-transform-parameters': 7.20.7_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-optional-catch-binding/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-optional-chaining/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@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+core@7.20.12 + dev: true + + /@babel/plugin-proposal-private-methods/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-create-class-features-plugin': 7.20.12_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-private-property-in-object/7.20.5_@babel+core@7.20.12: + resolution: {integrity: sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-create-class-features-plugin': 7.20.12_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.20.12 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} + engines: {node: '>=4'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-async-generators/7.8.4_@babel+core@7.20.12: + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-class-properties/7.12.13_@babel+core@7.20.12: + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-class-static-block/7.14.5_@babel+core@7.20.12: + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-decorators/7.19.0_@babel+core@7.20.12: + resolution: {integrity: sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-dynamic-import/7.8.3_@babel+core@7.20.12: + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-export-default-from/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-Kr//z3ujSVNx6E9z9ih5xXXMqK07VVTuqPmqGe6Mss/zW5XPeLZeSDZoP9ab/hT4wPKqAgjl2PnhPrcpk8Seew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-export-namespace-from/7.8.3_@babel+core@7.20.12: + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-import-assertions/7.20.0_@babel+core@7.20.12: + resolution: {integrity: sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.20.12: + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.20.12: + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.20.12: + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.20.12: + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.20.12: + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.20.12: + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.20.12: + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-private-property-in-object/7.14.5_@babel+core@7.20.12: + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.20.12: + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-typescript/7.20.0_@babel+core@7.20.12: + resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-arrow-functions/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-async-to-generator/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.20.12 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-block-scoped-functions/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-block-scoping/7.20.15_@babel+core@7.20.12: + resolution: {integrity: sha512-Vv4DMZ6MiNOhu/LdaZsT/bsLRxgL94d269Mv4R/9sp6+Mp++X/JqypZYypJXLlM4mlL352/Egzbzr98iABH1CA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-classes/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-LWYbsiXTPKl+oBlXUGlwNlJZetXD5Am+CyBdqhPsDVjM9Jc8jwBJFrKhHf900Kfk2eZG1y9MAG3UNajol7A4VQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12 + '@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.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-computed-properties/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/template': 7.20.7 + dev: true + + /@babel/plugin-transform-destructuring/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true - core: - specifiers: {} + /@babel/plugin-transform-dotall-regex/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true -packages: + /@babel/plugin-transform-duplicate-keys/7.18.9_@babel+core@7.20.12: + resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true - /@ampproject/remapping/2.2.0: - resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} - engines: {node: '>=6.0.0'} + /@babel/plugin-transform-exponentiation-operator/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@jridgewell/gen-mapping': 0.1.1 - '@jridgewell/trace-mapping': 0.3.13 + '@babel/core': 7.20.12 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/code-frame/7.18.6: - resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} + /@babel/plugin-transform-for-of/7.18.8_@babel+core@7.20.12: + resolution: {integrity: sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/highlight': 7.18.6 + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-function-name/7.18.9_@babel+core@7.20.12: + resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12 + '@babel/helper-function-name': 7.19.0 + '@babel/helper-plugin-utils': 7.20.2 + dev: true - /@babel/compat-data/7.19.0: - resolution: {integrity: sha512-y5rqgTTPTmaF5e2nVhOxw+Ur9HDJLsWb6U/KpgUzRZEdPfE6VOubXBKLdbcUTijzRptednSBDQbYZBOSqJxpJw==} + /@babel/plugin-transform-literals/7.18.9_@babel+core@7.20.12: + resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/core/7.19.0: - resolution: {integrity: sha512-reM4+U7B9ss148rh2n1Qs9ASS+w94irYXga7c2jaQv9RVzpS7Mv1a9rnYYwuDa45G+DkORt9g6An2k/V4d9LbQ==} + /@babel/plugin-transform-member-expression-literals/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@ampproject/remapping': 2.2.0 - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.19.0 - '@babel/helper-compilation-targets': 7.19.0_@babel+core@7.19.0 - '@babel/helper-module-transforms': 7.19.0 - '@babel/helpers': 7.19.0 - '@babel/parser': 7.19.0 - '@babel/template': 7.18.10 - '@babel/traverse': 7.19.0 - '@babel/types': 7.19.0 - convert-source-map: 1.8.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.1 - semver: 6.3.0 + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-modules-amd/7.20.11_@babel+core@7.20.12: + resolution: {integrity: sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-module-transforms': 7.20.11 + '@babel/helper-plugin-utils': 7.20.2 transitivePeerDependencies: - supports-color dev: true - /@babel/generator/7.19.0: - resolution: {integrity: sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg==} + /@babel/plugin-transform-modules-commonjs/7.20.11_@babel+core@7.20.12: + resolution: {integrity: sha512-S8e1f7WQ7cimJQ51JkAaDrEtohVEitXjgCGAS2N8S31Y42E+kWwfSz83LYz57QdBm7q9diARVqanIaH2oVgQnw==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.19.0 - '@jridgewell/gen-mapping': 0.3.2 - jsesc: 2.5.2 + '@babel/core': 7.20.12 + '@babel/helper-module-transforms': 7.20.11 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-simple-access': 7.20.2 + transitivePeerDependencies: + - supports-color dev: true - /@babel/helper-annotate-as-pure/7.18.6: - resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} + /@babel/plugin-transform-modules-systemjs/7.20.11_@babel+core@7.20.12: + resolution: {integrity: sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@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 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-umd/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.19.0 + '@babel/core': 7.20.12 + '@babel/helper-module-transforms': 7.20.11 + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color dev: true - /@babel/helper-compilation-targets/7.19.0_@babel+core@7.19.0: - resolution: {integrity: sha512-Ai5bNWXIvwDvWM7njqsG3feMlL9hCVQsPYXodsZyLwshYkZVJt59Gftau4VrE8S9IT9asd2uSP1hG6wCNw+sXA==} + /@babel/plugin-transform-named-capturing-groups-regex/7.20.5_@babel+core@7.20.12: + resolution: {integrity: sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/compat-data': 7.19.0 - '@babel/core': 7.19.0 - '@babel/helper-validator-option': 7.18.6 - browserslist: 4.21.3 - semver: 6.3.0 + '@babel/core': 7.20.12 + '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/helper-environment-visitor/7.18.9: - resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} + /@babel/plugin-transform-new-target/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/helper-function-name/7.19.0: - resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==} + /@babel/plugin-transform-object-super/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/template': 7.18.10 - '@babel/types': 7.19.0 + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-replace-supers': 7.20.7 + transitivePeerDependencies: + - supports-color dev: true - /@babel/helper-hoist-variables/7.18.6: - resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} + /@babel/plugin-transform-parameters/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.19.0 + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/helper-module-imports/7.18.6: - resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + /@babel/plugin-transform-property-literals/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.18.10 + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true - /@babel/helper-module-transforms/7.19.0: - resolution: {integrity: sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==} + /@babel/plugin-transform-react-display-name/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-simple-access': 7.18.6 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/helper-validator-identifier': 7.18.6 - '@babel/template': 7.18.10 - '@babel/traverse': 7.19.0 - '@babel/types': 7.19.0 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/helper-plugin-utils/7.18.9: - resolution: {integrity: sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==} + /@babel/plugin-transform-react-jsx-development/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/plugin-transform-react-jsx': 7.20.13_@babel+core@7.20.12 + dev: true - /@babel/helper-plugin-utils/7.19.0: - resolution: {integrity: sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==} + /@babel/plugin-transform-react-jsx-self/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/helper-simple-access/7.18.6: - resolution: {integrity: sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==} + /@babel/plugin-transform-react-jsx-source/7.19.6_@babel+core@7.20.12: + resolution: {integrity: sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.19.0 + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/helper-split-export-declaration/7.18.6: - resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} + /@babel/plugin-transform-react-jsx/7.20.13_@babel+core@7.20.12: + resolution: {integrity: sha512-MmTZx/bkUrfJhhYAYt3Urjm+h8DQGrPrnKQ94jLo7NLuOU+T89a7IByhKmrb8SKhrIYIQ0FN0CHMbnFRen4qNw==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.19.0 + '@babel/core': 7.20.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+core@7.20.12 + '@babel/types': 7.20.7 dev: true - /@babel/helper-string-parser/7.18.10: - resolution: {integrity: sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==} + /@babel/plugin-transform-react-pure-annotations/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-plugin-utils': 7.20.2 + dev: true - /@babel/helper-validator-identifier/7.18.6: - resolution: {integrity: sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==} + /@babel/plugin-transform-regenerator/7.20.5_@babel+core@7.20.12: + resolution: {integrity: sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + regenerator-transform: 0.15.1 + dev: true - /@babel/helper-validator-option/7.18.6: - resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} + /@babel/plugin-transform-reserved-words/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/helpers/7.19.0: - resolution: {integrity: sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==} + /@babel/plugin-transform-shorthand-properties/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/template': 7.18.10 - '@babel/traverse': 7.19.0 - '@babel/types': 7.19.0 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/highlight/7.18.6: - resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} + /@babel/plugin-transform-spread/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/helper-validator-identifier': 7.18.6 - chalk: 2.4.2 - js-tokens: 4.0.0 + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + dev: true - /@babel/parser/7.19.0: - resolution: {integrity: sha512-74bEXKX2h+8rrfQUfsBfuZZHzsEs6Eql4pqy/T4Nn6Y9wNPggQOqD6z6pn5Bl8ZfysKouFZT/UXEH94ummEeQw==} - engines: {node: '>=6.0.0'} - hasBin: true + /@babel/plugin-transform-sticky-regex/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.19.0 + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/plugin-syntax-jsx/7.18.6: - resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} + /@babel/plugin-transform-template-literals/7.18.9_@babel+core@7.20.12: + resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/helper-plugin-utils': 7.18.9 - dev: false + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true - /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.19.0: - resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} + /@babel/plugin-transform-typeof-symbol/7.18.9_@babel+core@7.20.12: + resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.19.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/plugin-transform-react-jsx-development/7.18.6_@babel+core@7.19.0: - resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} + /@babel/plugin-transform-typescript/7.20.13_@babel+core@7.20.12: + resolution: {integrity: sha512-O7I/THxarGcDZxkgWKMUrk7NK1/WbHAg3Xx86gqS6x9MTrNL6AwIluuZ96ms4xeDe6AVx6rjHbWHP7x26EPQBA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.19.0 - '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.19.0 + '@babel/core': 7.20.12 + '@babel/helper-create-class-features-plugin': 7.20.12_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-typescript': 7.20.0_@babel+core@7.20.12 + transitivePeerDependencies: + - supports-color dev: true - /@babel/plugin-transform-react-jsx-self/7.18.6_@babel+core@7.19.0: - resolution: {integrity: sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig==} + /@babel/plugin-transform-unicode-escapes/7.18.10_@babel+core@7.20.12: + resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.19.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/plugin-transform-react-jsx-source/7.18.6_@babel+core@7.19.0: - resolution: {integrity: sha512-utZmlASneDfdaMh0m/WausbjUjEdGrQJz0vFK93d7wD3xf5wBtX219+q6IlCNZeguIcxS2f/CvLZrlLSvSHQXw==} + /@babel/plugin-transform-unicode-regex/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.19.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/core': 7.20.12 + '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/plugin-transform-react-jsx/7.19.0_@babel+core@7.19.0: - resolution: {integrity: sha512-UVEvX3tXie3Szm3emi1+G63jyw1w5IcMY0FSKM+CRnKRI5Mr1YbCNgsSTwoTwKphQEG9P+QqmuRFneJPZuHNhg==} + /@babel/preset-env/7.20.2_@babel+core@7.20.12: + resolution: {integrity: sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.19.0 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.19.0 - '@babel/types': 7.19.0 + '@babel/compat-data': 7.20.14 + '@babel/core': 7.20.12 + '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12 + '@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+core@7.20.12 + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-proposal-async-generator-functions': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-proposal-class-static-block': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-proposal-dynamic-import': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-proposal-export-namespace-from': 7.18.9_@babel+core@7.20.12 + '@babel/plugin-proposal-json-strings': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-proposal-logical-assignment-operators': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-proposal-numeric-separator': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-proposal-object-rest-spread': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-proposal-optional-catch-binding': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-proposal-optional-chaining': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-proposal-private-property-in-object': 7.20.5_@babel+core@7.20.12 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.20.12 + '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.20.12 + '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.20.12 + '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-syntax-import-assertions': 7.20.0_@babel+core@7.20.12 + '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.20.12 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.20.12 + '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.20.12 + '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.20.12 + '@babel/plugin-transform-arrow-functions': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-transform-async-to-generator': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-transform-block-scoped-functions': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-block-scoping': 7.20.15_@babel+core@7.20.12 + '@babel/plugin-transform-classes': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-transform-computed-properties': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-transform-destructuring': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-duplicate-keys': 7.18.9_@babel+core@7.20.12 + '@babel/plugin-transform-exponentiation-operator': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.20.12 + '@babel/plugin-transform-function-name': 7.18.9_@babel+core@7.20.12 + '@babel/plugin-transform-literals': 7.18.9_@babel+core@7.20.12 + '@babel/plugin-transform-member-expression-literals': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-modules-amd': 7.20.11_@babel+core@7.20.12 + '@babel/plugin-transform-modules-commonjs': 7.20.11_@babel+core@7.20.12 + '@babel/plugin-transform-modules-systemjs': 7.20.11_@babel+core@7.20.12 + '@babel/plugin-transform-modules-umd': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-named-capturing-groups-regex': 7.20.5_@babel+core@7.20.12 + '@babel/plugin-transform-new-target': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-object-super': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-parameters': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-transform-property-literals': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-regenerator': 7.20.5_@babel+core@7.20.12 + '@babel/plugin-transform-reserved-words': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-spread': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-transform-sticky-regex': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.20.12 + '@babel/plugin-transform-typeof-symbol': 7.18.9_@babel+core@7.20.12 + '@babel/plugin-transform-unicode-escapes': 7.18.10_@babel+core@7.20.12 + '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.20.12 + '@babel/preset-modules': 0.1.5_@babel+core@7.20.12 + '@babel/types': 7.20.7 + babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.20.12 + babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.20.12 + babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.20.12 + core-js-compat: 3.28.0 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color dev: true - /@babel/runtime/7.1.2: - resolution: {integrity: sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg==} + /@babel/preset-modules/0.1.5_@babel+core@7.20.12: + resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - regenerator-runtime: 0.12.1 + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.20.12 + '@babel/types': 7.20.7 + esutils: 2.0.3 dev: true - /@babel/runtime/7.18.9: - resolution: {integrity: sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==} + /@babel/preset-react/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - regenerator-runtime: 0.13.9 - dev: false + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-validator-option': 7.18.6 + '@babel/plugin-transform-react-display-name': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-react-jsx': 7.20.13_@babel+core@7.20.12 + '@babel/plugin-transform-react-jsx-development': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-react-pure-annotations': 7.18.6_@babel+core@7.20.12 + dev: true - /@babel/runtime/7.19.0: - resolution: {integrity: sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==} + /@babel/preset-typescript/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - regenerator-runtime: 0.13.9 - dev: false + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-validator-option': 7.18.6 + '@babel/plugin-transform-typescript': 7.20.13_@babel+core@7.20.12 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/regjsgen/0.8.0: + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} + dev: true + + /@babel/runtime/7.1.2: + resolution: {integrity: sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg==} + dependencies: + regenerator-runtime: 0.12.1 + dev: true /@babel/runtime/7.2.0: resolution: {integrity: sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==} @@ -501,742 +1599,707 @@ packages: regenerator-runtime: 0.12.1 dev: true + /@babel/runtime/7.20.13: + resolution: {integrity: sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 + /@babel/runtime/7.3.4: resolution: {integrity: sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==} dependencies: regenerator-runtime: 0.12.1 dev: true - /@babel/template/7.18.10: - resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==} + /@babel/template/7.20.7: + resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.18.6 - '@babel/parser': 7.19.0 - '@babel/types': 7.19.0 + '@babel/parser': 7.20.15 + '@babel/types': 7.20.7 dev: true - /@babel/traverse/7.19.0: - resolution: {integrity: sha512-4pKpFRDh+utd2mbRC8JLnlsMUii3PMHjpL6a0SZ4NMZy7YFP9aXORxEhdMVOc9CpWtDF09IkciQLEhK7Ml7gRA==} + /@babel/traverse/7.20.13: + resolution: {integrity: sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==} engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.18.6 - '@babel/generator': 7.19.0 + '@babel/generator': 7.20.14 '@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.19.0 - '@babel/types': 7.19.0 + '@babel/parser': 7.20.15 + '@babel/types': 7.20.7 debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color dev: true - /@babel/types/7.18.10: - resolution: {integrity: sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.18.10 - '@babel/helper-validator-identifier': 7.18.6 - to-fast-properties: 2.0.0 - - /@babel/types/7.19.0: - resolution: {integrity: sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==} + /@babel/types/7.20.7: + resolution: {integrity: sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-string-parser': 7.18.10 - '@babel/helper-validator-identifier': 7.18.6 + '@babel/helper-string-parser': 7.19.4 + '@babel/helper-validator-identifier': 7.19.1 to-fast-properties: 2.0.0 - dev: true - /@chakra-ui/accordion/2.1.1_yxtycg2t66eeo2pchgyrvsgrfm: - resolution: {integrity: sha512-5f4QBl/0EgU/9EVvzlj8ZU7SWwG6nUHCE9moGBCbgiIOVBEySxZ5Robsk6+T7sXmzQ41db04GcUE9NRKdalgIA==} + /@chakra-ui/accordion/2.1.9_q4wvhr4lnobfisc2b3szkx75cu: + resolution: {integrity: sha512-a9CKIAUHezc0f5FR/SQ4GVxnWuIb2HbDTxTEKTp58w/J9pecIbJaNrJ5TUZ0MVbDU9jkgO9RsZ29jkja8PomAw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/descendant': 3.0.10_react@18.2.0 - '@chakra-ui/icon': 3.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-controllable-state': 2.0.5_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - '@chakra-ui/transition': 2.0.10_3scsim3kjm5bnetripeelcaw6u - framer-motion: 7.5.3_biqbaboplfbrettd7655fr4n2y - react: 18.2.0 + '@chakra-ui/descendant': 3.0.13 + '@chakra-ui/icon': 3.0.16_@chakra-ui+system@2.5.1 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-use-controllable-state': 2.0.8 + '@chakra-ui/react-use-merge-refs': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq + '@chakra-ui/transition': 2.0.15_framer-motion@7.10.3 + framer-motion: 7.10.3_react-dom@18.2.0 dev: false - /@chakra-ui/alert/2.0.11_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-n40KHU3j1H6EbIdgptjEad92V7Fpv7YD++ZBjy2g1h4w9ay9nw4kGHib3gaIkBupLf52CfLqySEc8w0taoIlXQ==} + /@chakra-ui/alert/2.0.17_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-0Y5vw+HkeXpwbL1roVpSSNM6luMRmUbwduUSHEA4OnX1ismvsDb1ZBfpi4Vxp6w8euJ2Uj6df3krbd5tbCP6tg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/spinner': 2.0.10_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/icon': 3.0.16_@chakra-ui+system@2.5.1 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/spinner': 2.0.13_@chakra-ui+system@2.5.1 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/anatomy/2.0.7: - resolution: {integrity: sha512-vzcB2gcsGCxhrKbldQQV6LnBPys4eSSsH2UA2mLsT+J3WlXw0aodZw0eE/nH7yLxe4zaQ4Gnc0KjkFW4EWNKSg==} + /@chakra-ui/anatomy/2.1.2: + resolution: {integrity: sha512-pKfOS/mztc4sUXHNc8ypJ1gPWSolWT770jrgVRfolVbYlki8y5Y+As996zMF6k5lewTu6j9DQequ7Cc9a69IVQ==} dev: false - /@chakra-ui/avatar/2.1.1_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-lTZPUq4Pefxgv3ndyJMxIHgFrXwdz2VZFCLF/aKcuGaUlB7TBYaCurQ7TNbME8j8VkJWNX+vKiVHPYvxsrITwQ==} + /@chakra-ui/avatar/2.2.5_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-TEHXuGE79+fEn61qJ7J/A0Ec+WjyNwobrDTATcLg9Zx2/WEMmZNfrWIAlI5ANQAwVbdSWeGVbyoLAK5mbcrE0A==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/image': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/react-children-utils': 2.0.2 - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/image': 2.0.15_@chakra-ui+system@2.5.1 + '@chakra-ui/react-children-utils': 2.0.6 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/breadcrumb/2.0.10_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-roKFA7nheq18eWNAdrHV6w8A9vZMSQTEEsbL6eU0lhUkolW9RlDjBl1bZvE7icFkNFXlJ33n8+0QAezLI+mMrQ==} + /@chakra-ui/breadcrumb/2.1.4_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-vyBx5TAxPnHhb0b8nyRGfqyjleD//9mySFhk96c9GL+T6YDO4swHw5y/kvDv3Ngc/iRwJ9hdI49PZKwPxLqsEg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-children-utils': 2.0.2 - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/react-children-utils': 2.0.6 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/breakpoint-utils/2.0.4: - resolution: {integrity: sha512-SUUEYnA/FCIKYDHMuEXcnBMwet+6RAAjQ+CqGD1hlwKPTfh7EK9fS8FoVAJa9KpRKAc/AawzPkgwvorzPj8NSg==} + /@chakra-ui/breakpoint-utils/2.0.8: + resolution: {integrity: sha512-Pq32MlEX9fwb5j5xx8s18zJMARNHlQZH2VH1RZgfgRDpp7DcEgtRW5AInfN5CfqdHLO1dGxA7I3MqEuL5JnIsA==} + dependencies: + '@chakra-ui/shared-utils': 2.0.5 dev: false - /@chakra-ui/button/2.0.11_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-J6iMRITqxTxa0JexHUY9c7BXUrTZtSkl3jZ2hxiFybB4MQL8J2wZ24O846B6M+WTYqy7XVuHRuVURnH4czWesw==} + /@chakra-ui/button/2.0.16_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-NjuTKa7gNhnGSUutKuTc8HoAOe9WWIigpciBG7yj3ok67kg8bXtSzPyQFZlgTY6XGdAckWTT+Do4tvhwa5LA+g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - '@chakra-ui/spinner': 2.0.10_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-use-merge-refs': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/spinner': 2.0.13_@chakra-ui+system@2.5.1 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/checkbox/2.2.1_yxtycg2t66eeo2pchgyrvsgrfm: - resolution: {integrity: sha512-soTeXEI+4UZSA4B4rRLpdh3cIW/gdhY6k0eXF4ZWExPb+dJ5Giv497S96vS4IGE7SJ7Ugw9kaWS+do2lSiPJew==} + /@chakra-ui/card/2.1.6_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-fFd/WAdRNVY/WOSQv4skpy0WeVhhI0f7dTY1Sm0jVl0KLmuP/GnpsWtKtqWjNcV00K963EXDyhlk6+9oxbP4gw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/form-control': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/react-types': 2.0.3_react@18.2.0 - '@chakra-ui/react-use-callback-ref': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-controllable-state': 2.0.5_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-safe-layout-effect': 2.0.2_react@18.2.0 - '@chakra-ui/react-use-update-effect': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - '@chakra-ui/visually-hidden': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@zag-js/focus-visible': 0.1.0 - framer-motion: 7.5.3_biqbaboplfbrettd7655fr4n2y - react: 18.2.0 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/clickable/2.0.10_react@18.2.0: - resolution: {integrity: sha512-G6JdR6yAMlXpfjOJ70W2FL7aUwNuomiMFtkneeTpk7Q42bJ5iGHfYlbZEx5nJd8iB+UluXVM4xlhMv2MyytjGw==} + /@chakra-ui/checkbox/2.2.10_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-vzxEjw99qj7loxAdP1WuHNt4EAvj/t6cc8oxyOB2mEvkAzhxI34rLR+3zWDuHWsmhyUO+XEDh4FiWdR+DK5Siw==} peerDependencies: + '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - react: 18.2.0 + '@chakra-ui/form-control': 2.0.17_@chakra-ui+system@2.5.1 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-types': 2.0.7 + '@chakra-ui/react-use-callback-ref': 2.0.7 + '@chakra-ui/react-use-controllable-state': 2.0.8 + '@chakra-ui/react-use-merge-refs': 2.0.7 + '@chakra-ui/react-use-safe-layout-effect': 2.0.5 + '@chakra-ui/react-use-update-effect': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq + '@chakra-ui/visually-hidden': 2.0.15_@chakra-ui+system@2.5.1 + '@zag-js/focus-visible': 0.2.1 dev: false - /@chakra-ui/close-button/2.0.11_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-9WF/nwwK9BldS89WQ5PtXK2nFS4r8QOgKls2BOwXfE+rGmOUZtOsu8ne/drXRjgkiBRETR6CxdyUjm7EPzXllw==} + /@chakra-ui/clickable/2.0.14: + resolution: {integrity: sha512-jfsM1qaD74ZykLHmvmsKRhDyokLUxEfL8Il1VoZMNX5RBI0xW/56vKpLTFF/v/+vLPLS+Te2cZdD4+2O+G6ulA==} + peerDependencies: + react: '>=18' + dependencies: + '@chakra-ui/react-use-merge-refs': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + dev: false + + /@chakra-ui/close-button/2.0.17_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-05YPXk456t1Xa3KpqTrvm+7smx+95dmaPiwjiBN3p7LHUQVHJd8ZXSDB0V+WKi419k3cVQeJUdU/azDO2f40sw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/icon': 3.0.16_@chakra-ui+system@2.5.1 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/color-mode/2.1.9_react@18.2.0: - resolution: {integrity: sha512-0kx0I+AQon8oS23/X+qMtnhsv/1BUulyJvU56p3Uh8CRaBfgJ7Ly9CerShoUL+5kadu6hN1M9oty4cugaCwv2w==} + /@chakra-ui/color-mode/2.1.12: + resolution: {integrity: sha512-sYyfJGDoJSLYO+V2hxV9r033qhte5Nw/wAn5yRGGZnEEN1dKPEdWQ3XZvglWSDTNd0w9zkoH2w6vP4FBBYb/iw==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-safe-layout-effect': 2.0.2_react@18.2.0 - react: 18.2.0 + '@chakra-ui/react-use-safe-layout-effect': 2.0.5 dev: false - /@chakra-ui/control-box/2.0.10_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-sHmZanFLEv4IDATl19ZTxq8Bi8PtjfvnsN6xF4k7JGSYUnk1YXUf1coyW7WKdcsczOASrMikfsLc3iEVAzx4Ng==} + /@chakra-ui/control-box/2.0.13_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-FEyrU4crxati80KUF/+1Z1CU3eZK6Sa0Yv7Z/ydtz9/tvGblXW9NFanoomXAOvcIFLbaLQPPATm9Gmpr7VG05A==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/counter/2.0.10_react@18.2.0: - resolution: {integrity: sha512-MZK8UKUZp4nFMd+GlV/cq0NIARS7UdlubTuCx+wockw9j2JI5OHzsyK0XiWuJiq5psegSTzpbtT99QfAUm3Yiw==} + /@chakra-ui/counter/2.0.14: + resolution: {integrity: sha512-KxcSRfUbb94dP77xTip2myoE7P2HQQN4V5fRJmNAGbzcyLciJ+aDylUU/UxgNcEjawUp6Q242NbWb1TSbKoqog==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/number-utils': 2.0.4 - '@chakra-ui/react-use-callback-ref': 2.0.4_react@18.2.0 - react: 18.2.0 + '@chakra-ui/number-utils': 2.0.7 + '@chakra-ui/react-use-callback-ref': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 dev: false - /@chakra-ui/css-reset/2.0.8_gyryel6m34lsxtsejhafetjriq: - resolution: {integrity: sha512-VuDD1rk1pFc+dItk4yUcstyoC9D2B35hatHDBtlPMqTczFAzpbgVJJYgEHANatXGfulM5SdckmYEIJ3Tac1Rtg==} + /@chakra-ui/css-reset/2.0.12_@emotion+react@11.10.6: + resolution: {integrity: sha512-Q5OYIMvqTl2vZ947kIYxcS5DhQXeStB84BzzBd6C10wOx1gFUu9pL+jLpOnHR3hhpWRMdX5o7eT+gMJWIYUZ0Q==} peerDependencies: '@emotion/react': '>=10.0.35' react: '>=18' dependencies: - '@emotion/react': 11.10.4_iapumuv4e6jcjznwuxpf4tt22e - react: 18.2.0 + '@emotion/react': 11.10.6_@types+react@18.0.28 dev: false - /@chakra-ui/descendant/3.0.10_react@18.2.0: - resolution: {integrity: sha512-MHH0Qdm0fGllGP2xgx4WOycmrpctyyEdGw6zxcfs2VqZNlrwmjG3Yb9eVY+Q7UmEv5rwAq6qRn7BhQxgSPn3Cg==} + /@chakra-ui/descendant/3.0.13: + resolution: {integrity: sha512-9nzxZVxUSMc4xPL5fSaRkEOQjDQWUGjGvrZI7VzWk9eq63cojOtIxtWMSW383G9148PzWJjJYt30Eud5tdZzlg==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - react: 18.2.0 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-use-merge-refs': 2.0.7 dev: false - /@chakra-ui/dom-utils/2.0.3: - resolution: {integrity: sha512-aeGlRmTxcv0cvW44DyeZHru1i68ZDQsXpfX2dnG1I1yBlT6GlVx1xYjCULis9mjhgvd2O3NfcYPRTkjNWTDUbA==} + /@chakra-ui/dom-utils/2.0.6: + resolution: {integrity: sha512-PVtDkPrDD5b8aoL6Atg7SLjkwhWb7BwMcLOF1L449L3nZN+DAO3nyAh6iUhZVJyunELj9d0r65CDlnMREyJZmA==} dev: false - /@chakra-ui/editable/2.0.12_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-37bDqm+j2JTN2XR443KRK9MmHHIQuS6fN+2TRuFgjfG8TomxxCJnhJ3GIfQSKh5Yjtnt4sXDmL4L0tyDpNrrrw==} + /@chakra-ui/editable/2.0.19_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-YxRJsJ2JQd42zfPBgTKzIhg1HugT+gfQz1ZosmUN+IZT9YZXL2yodHTUz6Lee04Vc/CdEqgBFLuREXEUNBfGtA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/react-types': 2.0.3_react@18.2.0 - '@chakra-ui/react-use-callback-ref': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-controllable-state': 2.0.5_react@18.2.0 - '@chakra-ui/react-use-focus-on-pointer-down': 2.0.3_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-safe-layout-effect': 2.0.2_react@18.2.0 - '@chakra-ui/react-use-update-effect': 2.0.4_react@18.2.0 - '@chakra-ui/shared-utils': 2.0.2 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-types': 2.0.7 + '@chakra-ui/react-use-callback-ref': 2.0.7 + '@chakra-ui/react-use-controllable-state': 2.0.8 + '@chakra-ui/react-use-focus-on-pointer-down': 2.0.6 + '@chakra-ui/react-use-merge-refs': 2.0.7 + '@chakra-ui/react-use-safe-layout-effect': 2.0.5 + '@chakra-ui/react-use-update-effect': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/event-utils/2.0.5: - resolution: {integrity: sha512-VXoOAIsM0PFKDlhm+EZxkWlUXd5UFTb/LTux3y3A+S9G5fDxLRvpiLWByPUgTFTCDFcgTCF+YnQtdWJB4DLyxg==} + /@chakra-ui/event-utils/2.0.8: + resolution: {integrity: sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw==} dev: false - /@chakra-ui/focus-lock/2.0.12_iapumuv4e6jcjznwuxpf4tt22e: - resolution: {integrity: sha512-NvIP59A11ZNbxXZ3qwxSiQ5npjABkpSbTIjK0uZ9bZm5LMfepRnuuA19VsVlq31/BYV9nHFAy6xzIuG+Qf9xMA==} + /@chakra-ui/focus-lock/2.0.16_@types+react@18.0.28: + resolution: {integrity: sha512-UuAdGCPVrCa1lecoAvpOQD7JFT7a9RdmhKWhFt5ioIcekSLJcerdLHuuL3w0qz//8kd1/SOt7oP0aJqdAJQrCw==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/dom-utils': 2.0.3 - react: 18.2.0 - react-focus-lock: 2.9.1_iapumuv4e6jcjznwuxpf4tt22e + '@chakra-ui/dom-utils': 2.0.6 + react-focus-lock: 2.9.4_@types+react@18.0.28 transitivePeerDependencies: - '@types/react' dev: false - /@chakra-ui/form-control/2.0.11_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-MVhIe0xY4Zn06IXRXFmS9tCa93snppK1SdUQb1P99Ipo424RrL5ykzLnJ8CAkQrhoVP3sxF7z3eOSzk8/iRfow==} + /@chakra-ui/form-control/2.0.17_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-34ptCaJ2LNvQNOlB6MAKsmH1AkT1xo7E+3Vw10Urr81yTOjDTM/iU6vG3JKPfRDMyXeowPjXmutlnuk72SSjRg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/react-types': 2.0.3_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/icon': 3.0.16_@chakra-ui+system@2.5.1 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-types': 2.0.7 + '@chakra-ui/react-use-merge-refs': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/hooks/2.0.11_react@18.2.0: - resolution: {integrity: sha512-mYN4u9lbjDjEr/VucrVcLGg/sIO6gA9ZprcT3n9CBGSWt3xih7fCOJmE+yRcCNbL7335AMrv7a/M5Q30aRArcA==} + /@chakra-ui/hooks/2.1.6: + resolution: {integrity: sha512-oMSOeoOF6/UpwTVlDFHSROAA4hPY8WgJ0erdHs1ZkuwAwHv7UzjDkvrb6xYzAAH9qHoFzc5RIBm6jVoh3LCc+Q==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-utils': 2.0.8_react@18.2.0 - '@chakra-ui/utils': 2.0.11 - compute-scroll-into-view: 1.0.14 - copy-to-clipboard: 3.3.1 - react: 18.2.0 + '@chakra-ui/react-utils': 2.0.12 + '@chakra-ui/utils': 2.0.15 + compute-scroll-into-view: 1.0.20 + copy-to-clipboard: 3.3.3 dev: false - /@chakra-ui/icon/3.0.11_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-RG4jf/XmBdaxOYI5J5QstEtTCPoVlmrQ/XiWhvN0LTgAnmZIqVwFl3Uw+satArdStHAs0GmJZg/E/soFTWuFmw==} + /@chakra-ui/icon/3.0.16_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-RpA1X5Ptz8Mt39HSyEIW1wxAz2AXyf9H0JJ5HVx/dBdMZaGMDJ0HyyPBVci0m4RCoJuyG1HHG/DXJaVfUTVAeg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/shared-utils': 2.0.2 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/image/2.0.11_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-S6NqAprPcbHnck/J+2wg06r9SSol62v5A01O8Kke2PnAyjalMcS+6P59lDRO7wvPqsdxq4PPbSTZP6Dww2CvcA==} + /@chakra-ui/image/2.0.15_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-w2rElXtI3FHXuGpMCsSklus+pO1Pl2LWDwsCGdpBQUvGFbnHfl7MftQgTlaGHeD5OS95Pxva39hKrA2VklKHiQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-use-safe-layout-effect': 2.0.2_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/react-use-safe-layout-effect': 2.0.5 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/input/2.0.11_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-kaV0VCz6/yzoCKQnh/tMUVgh+Rp6EnM+WzJ37SVX1gDvErON2bmmVLU45BiRoWUcd50wOhDlpsNVUWP0sLlCDA==} + /@chakra-ui/input/2.0.20_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-ypmsy4n4uNBVgn6Gd24Zrpi+qRf/T9WEzWkysuYC9Qfxo+i7yuf3snp7XmBy8KSGVSiXE11eO8ZN5oCg6Xg0jg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/form-control': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/object-utils': 2.0.4 - '@chakra-ui/react-children-utils': 2.0.2 - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/shared-utils': 2.0.2 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/form-control': 2.0.17_@chakra-ui+system@2.5.1 + '@chakra-ui/object-utils': 2.0.8 + '@chakra-ui/react-children-utils': 2.0.6 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/layout/2.1.8_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-pcNUNgMh+e4wepNOlCg5iDrxGg4VFBpqZPmSHoP4TyPN2ddEnDRLoMLaREMoX7gEVyTsqEFOFg+wa3JZK32H4A==} + /@chakra-ui/layout/2.1.16_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-QFS3feozIGsvB0H74lUocev55aRF26eNrdmhfJifwikZAiq+zzZAMdBdNU9UJhHClnMOU8/iGZ0MF7ti4zQS1A==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/breakpoint-utils': 2.0.4 - '@chakra-ui/icon': 3.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/object-utils': 2.0.4 - '@chakra-ui/react-children-utils': 2.0.2 - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/shared-utils': 2.0.2 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/breakpoint-utils': 2.0.8 + '@chakra-ui/icon': 3.0.16_@chakra-ui+system@2.5.1 + '@chakra-ui/object-utils': 2.0.8 + '@chakra-ui/react-children-utils': 2.0.6 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/lazy-utils/2.0.2: - resolution: {integrity: sha512-MTxutBJZvqNNqrrS0722cI7qrnGu0yUQpIebmTxYwI+F3cOnPEKf5Ni+hrA8hKcw4XJhSY4npAPPYu1zJbOV4w==} + /@chakra-ui/lazy-utils/2.0.5: + resolution: {integrity: sha512-UULqw7FBvcckQk2n3iPO56TMJvDsNv0FKZI6PlUNJVaGsPbsYxK/8IQ60vZgaTVPtVcjY6BE+y6zg8u9HOqpyg==} dev: false - /@chakra-ui/live-region/2.0.10_react@18.2.0: - resolution: {integrity: sha512-eQ2ZIreR/plzi/KGszDYTi1TvIyGEBcPiWP52BQOS7xwpzb1vsoR1FgFAIELxAGJvKnMUs+9qVogfyRBX8PdOg==} + /@chakra-ui/live-region/2.0.13: + resolution: {integrity: sha512-Ja+Slk6ZkxSA5oJzU2VuGU7TpZpbMb/4P4OUhIf2D30ctmIeXkxTWw1Bs1nGJAVtAPcGS5sKA+zb89i8g+0cTQ==} peerDependencies: react: '>=18' - dependencies: - react: 18.2.0 dev: false - /@chakra-ui/media-query/3.2.7_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-hbgm6JCe0kYU3PAhxASYYDopFQI26cW9kZnbp+5tRL1fykkVWNMPwoGC8FEZPur9JjXp7aoL6H4Jk7nrxY/XWw==} + /@chakra-ui/media-query/3.2.12_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-8pSLDf3oxxhFrhd40rs7vSeIBfvOmIKHA7DJlGUC/y+9irD24ZwgmCtFnn+y3gI47hTJsopbSX+wb8nr7XPswA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/breakpoint-utils': 2.0.4 - '@chakra-ui/react-env': 2.0.10_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/breakpoint-utils': 2.0.8 + '@chakra-ui/react-env': 3.0.0 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/menu/2.1.1_yxtycg2t66eeo2pchgyrvsgrfm: - resolution: {integrity: sha512-9fpCyV3vufLV5Rvv/oYC3LyCIkNqh0bEdYFVOLiqCZ6mt6NLFxL2jgE25nROYfDXQuBkY0qPC9IopYU198G4nw==} + /@chakra-ui/menu/2.1.9_q4wvhr4lnobfisc2b3szkx75cu: + resolution: {integrity: sha512-ue5nD4QJcl3H3UwN0zZNJmH89XUebnvEdW6THAUL41hDjJ0J/Fjpg9Sgzwug2aBbBXBNbVMsUuhcCj6x91d+IQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/clickable': 2.0.10_react@18.2.0 - '@chakra-ui/descendant': 3.0.10_react@18.2.0 - '@chakra-ui/lazy-utils': 2.0.2 - '@chakra-ui/popper': 3.0.8_react@18.2.0 - '@chakra-ui/react-children-utils': 2.0.2 - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-animation-state': 2.0.5_react@18.2.0 - '@chakra-ui/react-use-controllable-state': 2.0.5_react@18.2.0 - '@chakra-ui/react-use-disclosure': 2.0.5_react@18.2.0 - '@chakra-ui/react-use-focus-effect': 2.0.5_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-outside-click': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-update-effect': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - '@chakra-ui/transition': 2.0.10_3scsim3kjm5bnetripeelcaw6u - framer-motion: 7.5.3_biqbaboplfbrettd7655fr4n2y - react: 18.2.0 - dev: false - - /@chakra-ui/modal/2.2.1_fhvivh6d3co4nbcuohiwtac254: - resolution: {integrity: sha512-+zfiUG/yZqUQ0wY7syoZg01cpBf54lbKUe7+ANEx558UQGbsI4bbcHSkY9l5lsprQ8teInvhjb6BekeCA0e7TA==} + '@chakra-ui/clickable': 2.0.14 + '@chakra-ui/descendant': 3.0.13 + '@chakra-ui/lazy-utils': 2.0.5 + '@chakra-ui/popper': 3.0.13 + '@chakra-ui/react-children-utils': 2.0.6 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-use-animation-state': 2.0.8 + '@chakra-ui/react-use-controllable-state': 2.0.8 + '@chakra-ui/react-use-disclosure': 2.0.8 + '@chakra-ui/react-use-focus-effect': 2.0.9 + '@chakra-ui/react-use-merge-refs': 2.0.7 + '@chakra-ui/react-use-outside-click': 2.0.7 + '@chakra-ui/react-use-update-effect': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq + '@chakra-ui/transition': 2.0.15_framer-motion@7.10.3 + framer-motion: 7.10.3_react-dom@18.2.0 + dev: false + + /@chakra-ui/modal/2.2.9_xmdybzwbyd2chziouvdso5oidm: + resolution: {integrity: sha512-nTfNp7XsVwn5+xJOtstoFA8j0kq/9sJj7KesyYzjEDaMKvCZvIOntRYowoydho43jb4+YC7ebKhp0KOIINS0gg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/close-button': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/focus-lock': 2.0.12_iapumuv4e6jcjznwuxpf4tt22e - '@chakra-ui/portal': 2.0.10_biqbaboplfbrettd7655fr4n2y - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/react-types': 2.0.3_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - '@chakra-ui/transition': 2.0.10_3scsim3kjm5bnetripeelcaw6u - aria-hidden: 1.2.1_iapumuv4e6jcjznwuxpf4tt22e - framer-motion: 7.5.3_biqbaboplfbrettd7655fr4n2y - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - react-remove-scroll: 2.5.5_iapumuv4e6jcjznwuxpf4tt22e + '@chakra-ui/close-button': 2.0.17_@chakra-ui+system@2.5.1 + '@chakra-ui/focus-lock': 2.0.16_@types+react@18.0.28 + '@chakra-ui/portal': 2.0.15_react-dom@18.2.0 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-types': 2.0.7 + '@chakra-ui/react-use-merge-refs': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq + '@chakra-ui/transition': 2.0.15_framer-motion@7.10.3 + aria-hidden: 1.2.2_@types+react@18.0.28 + framer-motion: 7.10.3_react-dom@18.2.0 + react-dom: 18.2.0 + react-remove-scroll: 2.5.5_@types+react@18.0.28 transitivePeerDependencies: - '@types/react' dev: false - /@chakra-ui/number-input/2.0.12_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-3owLjl01sCYpTd3xbq//fJo9QJ0Q3PVYSx9JeOzlXnnTW8ws+yHPrqQzPe7G+tO4yOYynWuUT+NJ9oyCeAJIxA==} + /@chakra-ui/number-input/2.0.18_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-cPkyAFFHHzeFBselrT1BtjlzMkJ6TKrTDUnHFlzqXy6aqeXuhrjFhMfXucjedSpOqedsP9ZbKFTdIAhu9DdL/A==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/counter': 2.0.10_react@18.2.0 - '@chakra-ui/form-control': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/icon': 3.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/react-types': 2.0.3_react@18.2.0 - '@chakra-ui/react-use-callback-ref': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-event-listener': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-interval': 2.0.2_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-safe-layout-effect': 2.0.2_react@18.2.0 - '@chakra-ui/react-use-update-effect': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/counter': 2.0.14 + '@chakra-ui/form-control': 2.0.17_@chakra-ui+system@2.5.1 + '@chakra-ui/icon': 3.0.16_@chakra-ui+system@2.5.1 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-types': 2.0.7 + '@chakra-ui/react-use-callback-ref': 2.0.7 + '@chakra-ui/react-use-event-listener': 2.0.7 + '@chakra-ui/react-use-interval': 2.0.5 + '@chakra-ui/react-use-merge-refs': 2.0.7 + '@chakra-ui/react-use-safe-layout-effect': 2.0.5 + '@chakra-ui/react-use-update-effect': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/number-utils/2.0.4: - resolution: {integrity: sha512-MdYd29GboBoKaXY9jhbY0Wl+0NxG1t/fa32ZSIbU6VrfMsZuAMl4NEJsz7Xvhy50fummLdKn5J6HFS7o5iyIgw==} + /@chakra-ui/number-utils/2.0.7: + resolution: {integrity: sha512-yOGxBjXNvLTBvQyhMDqGU0Oj26s91mbAlqKHiuw737AXHt0aPllOthVUqQMeaYLwLCjGMg0jtI7JReRzyi94Dg==} dev: false - /@chakra-ui/object-utils/2.0.4: - resolution: {integrity: sha512-sY98L4v2wcjpwRX8GCXqT+WzpL0i5FHVxT1Okxw0360T2tGnZt7toAwpMfIOR3dzkemP9LfXMCyBmWR5Hi2zpQ==} + /@chakra-ui/object-utils/2.0.8: + resolution: {integrity: sha512-2upjT2JgRuiupdrtBWklKBS6tqeGMA77Nh6Q0JaoQuH/8yq+15CGckqn3IUWkWoGI0Fg3bK9LDlbbD+9DLw95Q==} dev: false - /@chakra-ui/pin-input/2.0.14_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-gFNlTUjU1xIuOErR/d/HrNNh1mS0erjNJSt5C6RU/My4lShzgCczmwnil7TuEx3k7lPqHKLEf/CGeCxBSUjaGA==} + /@chakra-ui/pin-input/2.0.19_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-6O7s4vWz4cqQ6zvMov9sYj6ZqWAsTxR/MNGe3DNgu1zWQg8veNCYtj1rNGhNS3eZNUMAa8uM2dXIphGTP53Xow==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/descendant': 3.0.10_react@18.2.0 - '@chakra-ui/react-children-utils': 2.0.2 - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-controllable-state': 2.0.5_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/descendant': 3.0.13 + '@chakra-ui/react-children-utils': 2.0.6 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-use-controllable-state': 2.0.8 + '@chakra-ui/react-use-merge-refs': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/popover/2.1.1_yxtycg2t66eeo2pchgyrvsgrfm: - resolution: {integrity: sha512-j09NsesfT+eaYITkITYJXDlRcPoOeQUM80neJZKOBgul2iHkVsEoii8dwS5Ip5ONeu4ane1b6zEOlYvYj2SrkA==} + /@chakra-ui/popover/2.1.8_q4wvhr4lnobfisc2b3szkx75cu: + resolution: {integrity: sha512-ob7fAz+WWmXIq7iGHVB3wDKzZTj+T+noYBT/U1Q+jIf+jMr2WOpJLTfb0HTZcfhvn4EBFlfBg7Wk5qbXNaOn7g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/close-button': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/lazy-utils': 2.0.2 - '@chakra-ui/popper': 3.0.8_react@18.2.0 - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/react-types': 2.0.3_react@18.2.0 - '@chakra-ui/react-use-animation-state': 2.0.5_react@18.2.0 - '@chakra-ui/react-use-disclosure': 2.0.5_react@18.2.0 - '@chakra-ui/react-use-focus-effect': 2.0.5_react@18.2.0 - '@chakra-ui/react-use-focus-on-pointer-down': 2.0.3_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - framer-motion: 7.5.3_biqbaboplfbrettd7655fr4n2y - react: 18.2.0 + '@chakra-ui/close-button': 2.0.17_@chakra-ui+system@2.5.1 + '@chakra-ui/lazy-utils': 2.0.5 + '@chakra-ui/popper': 3.0.13 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-types': 2.0.7 + '@chakra-ui/react-use-animation-state': 2.0.8 + '@chakra-ui/react-use-disclosure': 2.0.8 + '@chakra-ui/react-use-focus-effect': 2.0.9 + '@chakra-ui/react-use-focus-on-pointer-down': 2.0.6 + '@chakra-ui/react-use-merge-refs': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq + framer-motion: 7.10.3_react-dom@18.2.0 dev: false - /@chakra-ui/popper/3.0.8_react@18.2.0: - resolution: {integrity: sha512-246eUwuCRsLpTPxn5T8D8T9/6ODqmmz6pRRJAjGnLlUB0gNHgjisBn0UDBic5Gbxcg0sqKvxOMY3uurbW5lXTA==} + /@chakra-ui/popper/3.0.13: + resolution: {integrity: sha512-FwtmYz80Ju8oK3Z1HQfisUE7JIMmDsCQsRBu6XuJ3TFQnBHit73yjZmxKjuRJ4JgyT4WBnZoTF3ATbRKSagBeg==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-types': 2.0.3_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 + '@chakra-ui/react-types': 2.0.7 + '@chakra-ui/react-use-merge-refs': 2.0.7 '@popperjs/core': 2.11.6 - react: 18.2.0 dev: false - /@chakra-ui/portal/2.0.10_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-VRYvVAggIuqIZ3IQ6XZ1b5ujjjOUgPk9PPdc9jssUngZa7RG+5NXNhgoM8a5TsXv6aPEolBOlDNWuxzRQ4RSSg==} + /@chakra-ui/portal/2.0.15_react-dom@18.2.0: + resolution: {integrity: sha512-z8v7K3j1/nMuBzp2+wRIIw7s/eipVtnXLdjK5yqbMxMRa44E8Mu5VNJLz3aQFLHXEUST+ifqrjImQeli9do6LQ==} peerDependencies: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-safe-layout-effect': 2.0.2_react@18.2.0 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-use-safe-layout-effect': 2.0.5 + react-dom: 18.2.0 dev: false - /@chakra-ui/progress/2.0.11_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-2OwxGxI6W757QpDB6b++B4b2+t0oBgaQdHnd4/y35n3+mOFj++Wg7XpW4/iDHn+x3LkM+X3NIgdBWQFlmGx+6w==} + /@chakra-ui/progress/2.1.5_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-jj5Vp4lxUchuwp4RPCepM0yAyKi344bgsOd3Apd+ldxclDcewPc82fbwDu7g/Xv27LqJkT+7E/SlQy04wGrk0g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/provider/2.0.19_6xnkn2aqnlmdvuspwqrjexfduy: - resolution: {integrity: sha512-V+p0OePre0OgYmNxLbfiPWWbzaJ/EM2sfaRtD7E6ZA4TjUl2m4pWdC6OPMOiklu7EALfSgVk9X6Jh5pc+moH1g==} + /@chakra-ui/provider/2.1.2_kgmkmudaz6zokiyel5u2vpzy34: + resolution: {integrity: sha512-4lLlz8QuJv00BhfyKzWpzfoti9MDOdJ/MqXixJV/EZ02RMBOdE9qy9bSz/WckPC2MVhtRUuwMkxH+0QY21PXuw==} peerDependencies: '@emotion/react': ^11.0.0 '@emotion/styled': ^11.0.0 react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/css-reset': 2.0.8_gyryel6m34lsxtsejhafetjriq - '@chakra-ui/portal': 2.0.10_biqbaboplfbrettd7655fr4n2y - '@chakra-ui/react-env': 2.0.10_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - '@chakra-ui/utils': 2.0.11 - '@emotion/react': 11.10.4_iapumuv4e6jcjznwuxpf4tt22e - '@emotion/styled': 11.10.4_g3tud4ene45llglqap74b5kkse - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 + '@chakra-ui/css-reset': 2.0.12_@emotion+react@11.10.6 + '@chakra-ui/portal': 2.0.15_react-dom@18.2.0 + '@chakra-ui/react-env': 3.0.0 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq + '@chakra-ui/utils': 2.0.15 + '@emotion/react': 11.10.6_@types+react@18.0.28 + '@emotion/styled': 11.10.6_rtgl6lwupdrbo733hg3i5dx32q + react-dom: 18.2.0 dev: false - /@chakra-ui/radio/2.0.12_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-871hqAGQaufxyUzPP3aautPBIRZQmpi3fw5XPZ6SbY62dV61M4sjcttd46HfCf5SrAonoOADFQLMGQafznjhaA==} + /@chakra-ui/radio/2.0.19_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-PlJiV59eGSmeKP4v/4+ccQUWGRd0cjPKkj/p3L+UbOf8pl9dWm8y9kIeL5TYbghQSDv0nzkrH4+yMnnDTZjdMQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/form-control': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/react-types': 2.0.3_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - '@zag-js/focus-visible': 0.1.0 - react: 18.2.0 + '@chakra-ui/form-control': 2.0.17_@chakra-ui+system@2.5.1 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-types': 2.0.7 + '@chakra-ui/react-use-merge-refs': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq + '@zag-js/focus-visible': 0.2.1 dev: false - /@chakra-ui/react-children-utils/2.0.2: - resolution: {integrity: sha512-mRTGAZ3DBXB3hgVwS2DVJe3Nlc0qGvMN0PAo4tD/3fj2op2IwspLcYPAWC5D/rI2xj2JlwE6szAtbvxdAfLCNw==} + /@chakra-ui/react-children-utils/2.0.6: + resolution: {integrity: sha512-QVR2RC7QsOsbWwEnq9YduhpqSFnZGvjjGREV8ygKi8ADhXh93C8azLECCUVgRJF2Wc+So1fgxmjLcbZfY2VmBA==} + peerDependencies: + react: '>=18' dev: false - /@chakra-ui/react-context/2.0.4_react@18.2.0: - resolution: {integrity: sha512-eBITFkf7fLSiMZrSdhweK4fYr41WUNMEeIEOP2dCWolE7WgKxNYaYleC+iRGY0GeXkFM2KYywUtixjJe29NuVA==} + /@chakra-ui/react-context/2.0.7: + resolution: {integrity: sha512-i7EGmSU+h2GB30cwrKB4t1R5BMHyGoJM5L2Zz7b+ZUX4aAqyPcfe97wPiQB6Rgr1ImGXrUeov4CDVrRZ2FPgLQ==} peerDependencies: react: '>=18' - dependencies: - react: 18.2.0 dev: false - /@chakra-ui/react-env/2.0.10_react@18.2.0: - resolution: {integrity: sha512-3Yab5EbFcCGYzEsoijy4eA3354Z/JoXyk9chYIuW7Uwd+K6g/R8C0mUSAHeTmfp6Fix9kzDgerO5MWNM87b8cA==} + /@chakra-ui/react-env/3.0.0: + resolution: {integrity: sha512-tfMRO2v508HQWAqSADFrwZgR9oU10qC97oV6zGbjHh9ALP0/IcFR+Bi71KRTveDTm85fMeAzZYGj57P3Dsipkw==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + '@chakra-ui/react-use-safe-layout-effect': 2.0.5 dev: false - /@chakra-ui/react-types/2.0.3_react@18.2.0: - resolution: {integrity: sha512-1mJYOQldFTALE0Wr3j6tk/MYvgQIp6CKkJulNzZrI8QN+ox/bJOh8OVP4vhwqvfigdLTui0g0k8M9h+j2ub/Mw==} + /@chakra-ui/react-types/2.0.7: + resolution: {integrity: sha512-12zv2qIZ8EHwiytggtGvo4iLT0APris7T0qaAWqzpUGS0cdUtR8W+V1BJ5Ocq+7tA6dzQ/7+w5hmXih61TuhWQ==} peerDependencies: react: '>=18' - dependencies: - react: 18.2.0 dev: false - /@chakra-ui/react-use-animation-state/2.0.5_react@18.2.0: - resolution: {integrity: sha512-8gZIqZpMS5yTGlC+IqYoSrV13joiAYoeI0YR2t68WuDagcZ459OrjE57+gF04NLxfdV7eUgwqnpuv7IOLbJX/A==} + /@chakra-ui/react-use-animation-state/2.0.8: + resolution: {integrity: sha512-xv9zSF2Rd1mHWQ+m5DLBWeh4atF8qrNvsOs3MNrvxKYBS3f79N3pqcQGrWAEvirXWXfiCeje2VAkEggqFRIo+Q==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/dom-utils': 2.0.3 - '@chakra-ui/react-use-event-listener': 2.0.4_react@18.2.0 - react: 18.2.0 + '@chakra-ui/dom-utils': 2.0.6 + '@chakra-ui/react-use-event-listener': 2.0.7 dev: false - /@chakra-ui/react-use-callback-ref/2.0.4_react@18.2.0: - resolution: {integrity: sha512-he7EQfwMA4mwiDDKvX7cHIJaboCqf7UD3KYHGUcIjsF4dSc2Y8X5Ze4w+hmVZoJWIe4DWUzb3ili2SUm8eTgPg==} + /@chakra-ui/react-use-callback-ref/2.0.7: + resolution: {integrity: sha512-YjT76nTpfHAK5NxplAlZsQwNju5KmQExnqsWNPFeOR6vvbC34+iPSTr+r91i1Hdy7gBSbevsOsd5Wm6RN3GuMw==} peerDependencies: react: '>=18' - dependencies: - react: 18.2.0 dev: false - /@chakra-ui/react-use-controllable-state/2.0.5_react@18.2.0: - resolution: {integrity: sha512-JrZZpMX24CUyfDuyqDczw9Z9IMvjH8ujETHK0Zu4M0SIsX/q4EqOwwngUFL03I2gx/O38HfSdeX8hMu4zbTAGA==} + /@chakra-ui/react-use-controllable-state/2.0.8: + resolution: {integrity: sha512-F7rdCbLEmRjwwODqWZ3y+mKgSSHPcLQxeUygwk1BkZPXbKkJJKymOIjIynil2cbH7ku3hcSIWRvuhpCcfQWJ7Q==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.0.4_react@18.2.0 - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.0.7 dev: false - /@chakra-ui/react-use-disclosure/2.0.5_react@18.2.0: - resolution: {integrity: sha512-kPLB9oxImASRhAbKfvfc03/lbAJbsXndEVRzd+nvvL+QZm2RRfnel3k6OIkWvGFOXXYOPE2+slLe8ZPwbTGg9g==} + /@chakra-ui/react-use-disclosure/2.0.8: + resolution: {integrity: sha512-2ir/mHe1YND40e+FyLHnDsnDsBQPwzKDLzfe9GZri7y31oU83JSbHdlAXAhp3bpjohslwavtRCp+S/zRxfO9aQ==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.0.4_react@18.2.0 - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.0.7 dev: false - /@chakra-ui/react-use-event-listener/2.0.4_react@18.2.0: - resolution: {integrity: sha512-VqmalfKWMO8D21XuZO19WUtcP5xhbHXKzkggApTChZUN02UC5TC4pe0pYbDygoeUuNBhY+9lJKHeS08vYsljRg==} + /@chakra-ui/react-use-event-listener/2.0.7: + resolution: {integrity: sha512-4wvpx4yudIO3B31pOrXuTHDErawmwiXnvAN7gLEOVREi16+YGNcFnRJ5X5nRrmB7j2MDUtsEDpRBFfw5Z9xQ5g==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.0.4_react@18.2.0 - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.0.7 dev: false - /@chakra-ui/react-use-focus-effect/2.0.5_react@18.2.0: - resolution: {integrity: sha512-sbe1QnsXXfjukM+laxbKnT0UnMpHe/7kTzEPG/BYM6/ZDUUmrC1Nz+8l+3H/52iWIaruikDBdif/Xd37Yvu3Kg==} + /@chakra-ui/react-use-focus-effect/2.0.9: + resolution: {integrity: sha512-20nfNkpbVwyb41q9wxp8c4jmVp6TUGAPE3uFTDpiGcIOyPW5aecQtPmTXPMJH+2aa8Nu1wyoT1btxO+UYiQM3g==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/dom-utils': 2.0.3 - '@chakra-ui/react-use-event-listener': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-update-effect': 2.0.4_react@18.2.0 - react: 18.2.0 + '@chakra-ui/dom-utils': 2.0.6 + '@chakra-ui/react-use-event-listener': 2.0.7 + '@chakra-ui/react-use-safe-layout-effect': 2.0.5 + '@chakra-ui/react-use-update-effect': 2.0.7 dev: false - /@chakra-ui/react-use-focus-on-pointer-down/2.0.3_react@18.2.0: - resolution: {integrity: sha512-8cKmpv26JnblexNaekWxEDI7M+MZnJcp1PJUz6lByjfQ1m4YjFr1cdbdhG4moaqzzYs7vTmO/qL8KVq8ZLUwyQ==} + /@chakra-ui/react-use-focus-on-pointer-down/2.0.6: + resolution: {integrity: sha512-OigXiLRVySn3tyVqJ/rn57WGuukW8TQe8fJYiLwXbcNyAMuYYounvRxvCy2b53sQ7QIZamza0N0jhirbH5FNoQ==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-event-listener': 2.0.4_react@18.2.0 - react: 18.2.0 + '@chakra-ui/react-use-event-listener': 2.0.7 dev: false - /@chakra-ui/react-use-interval/2.0.2_react@18.2.0: - resolution: {integrity: sha512-5U1c0pEB5n0Yri0E4RdFXWx2RVBZBBhD8Uu49dM33jkIguCbIPmZ+YgVry5DDzCHyz4RgDg4yZKOPK0PI8lEUg==} + /@chakra-ui/react-use-interval/2.0.5: + resolution: {integrity: sha512-1nbdwMi2K87V6p5f5AseOKif2CkldLaJlq1TOqaPRwb7v3aU9rltBtYdf+fIyuHSToNJUV6wd9budCFdLCl3Fg==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.0.4_react@18.2.0 - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.0.7 dev: false - /@chakra-ui/react-use-latest-ref/2.0.2_react@18.2.0: - resolution: {integrity: sha512-Ra/NMV+DSQ3n0AdKsyIqdgnFzls5UntabtIRfDXLrqmJ4tI0a1tDdop2qop0Ue87AcqD9P1KtQue4KPx7wCElw==} + /@chakra-ui/react-use-latest-ref/2.0.5: + resolution: {integrity: sha512-3mIuFzMyIo3Ok/D8uhV9voVg7KkrYVO/pwVvNPJOHsDQqCA6DpYE4WDsrIx+fVcwad3Ta7SupexR5PoI+kq6QQ==} peerDependencies: react: '>=18' - dependencies: - react: 18.2.0 dev: false - /@chakra-ui/react-use-merge-refs/2.0.4_react@18.2.0: - resolution: {integrity: sha512-aoWvtE5tDQNaLCiNUI6WV+MA2zVcCLR5mHSCISmowlTXyXOqOU5Fo9ZoUftzrmgCJpDu5x1jfUOivxuHUueb0g==} + /@chakra-ui/react-use-merge-refs/2.0.7: + resolution: {integrity: sha512-zds4Uhsc+AMzdH8JDDkLVet9baUBgtOjPbhC5r3A0ZXjZvGhCztFAVE3aExYiVoMPoHLKbLcqvCWE6ioFKz1lw==} peerDependencies: react: '>=18' - dependencies: - react: 18.2.0 dev: false - /@chakra-ui/react-use-outside-click/2.0.4_react@18.2.0: - resolution: {integrity: sha512-uerJKS8dqg2kHs1xozA5vcCqW0UInuwrfCPb+rDWBTpu7aEqxABMw9W3e4gfOABrAjhKz2I0a/bu2i8zbVwdLw==} + /@chakra-ui/react-use-outside-click/2.0.7: + resolution: {integrity: sha512-MsAuGLkwYNxNJ5rb8lYNvXApXxYMnJ3MzqBpQj1kh5qP/+JSla9XMjE/P94ub4fSEttmNSqs43SmPPrmPuihsQ==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.0.4_react@18.2.0 - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.0.7 dev: false - /@chakra-ui/react-use-pan-event/2.0.5_react@18.2.0: - resolution: {integrity: sha512-nhE3b85++EEmBD2v6m46TLoA4LehSCZ349P8kvEjw/RC0K6XDOZndaBucIeAlnpEENSSUpczFfMSOLxSHdu0oA==} + /@chakra-ui/react-use-pan-event/2.0.9: + resolution: {integrity: sha512-xu35QXkiyrgsHUOnctl+SwNcwf9Rl62uYE5y8soKOZdBm8E+FvZIt2hxUzK1EoekbJCMzEZ0Yv1ZQCssVkSLaQ==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/event-utils': 2.0.5 - '@chakra-ui/react-use-latest-ref': 2.0.2_react@18.2.0 - framesync: 5.3.0 - react: 18.2.0 + '@chakra-ui/event-utils': 2.0.8 + '@chakra-ui/react-use-latest-ref': 2.0.5 + framesync: 6.1.2 dev: false - /@chakra-ui/react-use-previous/2.0.2_react@18.2.0: - resolution: {integrity: sha512-ap/teLRPKopaHYD80fnf0TR/NpTWHJO5VdKg6sPyF1y5ediYLAzPT1G2OqMCj4QfJsYDctioT142URDYe0Nn7w==} + /@chakra-ui/react-use-previous/2.0.5: + resolution: {integrity: sha512-BIZgjycPE4Xr+MkhKe0h67uHXzQQkBX/u5rYPd65iMGdX1bCkbE0oorZNfOHLKdTmnEb4oVsNvfN6Rfr+Mnbxw==} peerDependencies: react: '>=18' - dependencies: - react: 18.2.0 dev: false - /@chakra-ui/react-use-safe-layout-effect/2.0.2_react@18.2.0: - resolution: {integrity: sha512-gl5HDq9RVeDJiT8udtpx12KRV8JPLJHDIUX8f/yZcKpXow0C7FFGg5Yy5I9397NQog5ZjKMuOg+AUq9TLJxsyQ==} + /@chakra-ui/react-use-safe-layout-effect/2.0.5: + resolution: {integrity: sha512-MwAQBz3VxoeFLaesaSEN87reVNVbjcQBDex2WGexAg6hUB6n4gc1OWYH/iXp4tzp4kuggBNhEHkk9BMYXWfhJQ==} peerDependencies: react: '>=18' - dependencies: - react: 18.2.0 dev: false - /@chakra-ui/react-use-size/2.0.4_react@18.2.0: - resolution: {integrity: sha512-W6rgTLuoSC4ovZtqYco8cG+yBadH3bhlg92T5lgpKDakSDr0mXcZdbGx6g0AOkgxXm0V1jWNGO1743wudtF7ew==} + /@chakra-ui/react-use-size/2.0.9: + resolution: {integrity: sha512-Jce7QmO1jlQZq+Y77VKckWzroRnajChzUQ8xhLQZO6VbYvrpg3cu+X2QCz3G+MZzB+1/hnvvAqmZ+uJLd8rEJg==} peerDependencies: react: '>=18' dependencies: - '@zag-js/element-size': 0.1.0 - react: 18.2.0 + '@zag-js/element-size': 0.3.1 dev: false - /@chakra-ui/react-use-timeout/2.0.2_react@18.2.0: - resolution: {integrity: sha512-n6zb3OmxtDmRMxYkDgILqKh15aDOa8jNLHBlqHzmlL6mEGNKmMFPW9j/KvpAqSgKjUTDRnnXcpneprTMKy/yrw==} + /@chakra-ui/react-use-timeout/2.0.5: + resolution: {integrity: sha512-QqmB+jVphh3h/CS60PieorpY7UqSPkrQCB7f7F+i9vwwIjtP8fxVHMmkb64K7VlzQiMPzv12nlID5dqkzlv0mw==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.0.4_react@18.2.0 - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.0.7 dev: false - /@chakra-ui/react-use-update-effect/2.0.4_react@18.2.0: - resolution: {integrity: sha512-F/I9LVnGAQyvww+x7tQb47wCwjhMYjpxtM1dTg1U3oCEXY0yF1Ts3NJLUAlsr3nAW6epJIwWx61niC7KWpam1w==} + /@chakra-ui/react-use-update-effect/2.0.7: + resolution: {integrity: sha512-vBM2bmmM83ZdDtasWv3PXPznpTUd+FvqBC8J8rxoRmvdMEfrxTiQRBJhiGHLpS9BPLLPQlosN6KdFU97csB6zg==} peerDependencies: react: '>=18' - dependencies: - react: 18.2.0 dev: false - /@chakra-ui/react-utils/2.0.8_react@18.2.0: - resolution: {integrity: sha512-OSHHBKZlJWTi2NZcPnBx1PyZvLQY+n5RPBtcri7/89EDdAwz2NdEhp2Dz1yQRctOSCF1kB/rnCYDP1U0oRk9RQ==} + /@chakra-ui/react-utils/2.0.12: + resolution: {integrity: sha512-GbSfVb283+YA3kA8w8xWmzbjNWk14uhNpntnipHCftBibl0lxtQ9YqMFQLwuFOO0U2gYVocszqqDWX+XNKq9hw==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/utils': 2.0.11 - react: 18.2.0 + '@chakra-ui/utils': 2.0.15 dev: false - /@chakra-ui/react/2.3.5_356kgaleomk4jdithf3bcp4v5i: - resolution: {integrity: sha512-bQDRV23M3IvF0+AorTvqJmG/4T6KKQIb+1XGA2RyxonoSHVt89HbN3qnygHJw06Sdgpclxdbr/1qZ4o8+SMbpA==} + /@chakra-ui/react/2.5.1_ja2rgvpkvdku2qustoaipud6ya: + resolution: {integrity: sha512-ugkaqfcNMb9L4TkalWiF3rnqfr0TlUUD46JZaDIZiORVisaSwXTZTQrVfG40VghhaJT28rnC5WtiE8kd567ZBQ==} peerDependencies: '@emotion/react': ^11.0.0 '@emotion/styled': ^11.0.0 @@ -1244,353 +2307,348 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/accordion': 2.1.1_yxtycg2t66eeo2pchgyrvsgrfm - '@chakra-ui/alert': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/avatar': 2.1.1_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/breadcrumb': 2.0.10_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/button': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/checkbox': 2.2.1_yxtycg2t66eeo2pchgyrvsgrfm - '@chakra-ui/close-button': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/control-box': 2.0.10_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/counter': 2.0.10_react@18.2.0 - '@chakra-ui/css-reset': 2.0.8_gyryel6m34lsxtsejhafetjriq - '@chakra-ui/editable': 2.0.12_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/form-control': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/hooks': 2.0.11_react@18.2.0 - '@chakra-ui/icon': 3.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/image': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/input': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/layout': 2.1.8_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/live-region': 2.0.10_react@18.2.0 - '@chakra-ui/media-query': 3.2.7_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/menu': 2.1.1_yxtycg2t66eeo2pchgyrvsgrfm - '@chakra-ui/modal': 2.2.1_fhvivh6d3co4nbcuohiwtac254 - '@chakra-ui/number-input': 2.0.12_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/pin-input': 2.0.14_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/popover': 2.1.1_yxtycg2t66eeo2pchgyrvsgrfm - '@chakra-ui/popper': 3.0.8_react@18.2.0 - '@chakra-ui/portal': 2.0.10_biqbaboplfbrettd7655fr4n2y - '@chakra-ui/progress': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/provider': 2.0.19_6xnkn2aqnlmdvuspwqrjexfduy - '@chakra-ui/radio': 2.0.12_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/react-env': 2.0.10_react@18.2.0 - '@chakra-ui/select': 2.0.12_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/skeleton': 2.0.17_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/slider': 2.0.12_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/spinner': 2.0.10_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/stat': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/styled-system': 2.3.4 - '@chakra-ui/switch': 2.0.13_yxtycg2t66eeo2pchgyrvsgrfm - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - '@chakra-ui/table': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/tabs': 2.1.3_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/tag': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/textarea': 2.0.12_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/theme': 2.1.13_kzevwk2hxgy6fnrjqv25jdmqp4 - '@chakra-ui/toast': 3.0.13_svvprik4kmnbjwafrq4cgb4wou - '@chakra-ui/tooltip': 2.2.0_svvprik4kmnbjwafrq4cgb4wou - '@chakra-ui/transition': 2.0.10_3scsim3kjm5bnetripeelcaw6u - '@chakra-ui/utils': 2.0.11 - '@chakra-ui/visually-hidden': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@emotion/react': 11.10.4_iapumuv4e6jcjznwuxpf4tt22e - '@emotion/styled': 11.10.4_g3tud4ene45llglqap74b5kkse - framer-motion: 7.5.3_biqbaboplfbrettd7655fr4n2y - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 + '@chakra-ui/accordion': 2.1.9_q4wvhr4lnobfisc2b3szkx75cu + '@chakra-ui/alert': 2.0.17_@chakra-ui+system@2.5.1 + '@chakra-ui/avatar': 2.2.5_@chakra-ui+system@2.5.1 + '@chakra-ui/breadcrumb': 2.1.4_@chakra-ui+system@2.5.1 + '@chakra-ui/button': 2.0.16_@chakra-ui+system@2.5.1 + '@chakra-ui/card': 2.1.6_@chakra-ui+system@2.5.1 + '@chakra-ui/checkbox': 2.2.10_@chakra-ui+system@2.5.1 + '@chakra-ui/close-button': 2.0.17_@chakra-ui+system@2.5.1 + '@chakra-ui/control-box': 2.0.13_@chakra-ui+system@2.5.1 + '@chakra-ui/counter': 2.0.14 + '@chakra-ui/css-reset': 2.0.12_@emotion+react@11.10.6 + '@chakra-ui/editable': 2.0.19_@chakra-ui+system@2.5.1 + '@chakra-ui/focus-lock': 2.0.16_@types+react@18.0.28 + '@chakra-ui/form-control': 2.0.17_@chakra-ui+system@2.5.1 + '@chakra-ui/hooks': 2.1.6 + '@chakra-ui/icon': 3.0.16_@chakra-ui+system@2.5.1 + '@chakra-ui/image': 2.0.15_@chakra-ui+system@2.5.1 + '@chakra-ui/input': 2.0.20_@chakra-ui+system@2.5.1 + '@chakra-ui/layout': 2.1.16_@chakra-ui+system@2.5.1 + '@chakra-ui/live-region': 2.0.13 + '@chakra-ui/media-query': 3.2.12_@chakra-ui+system@2.5.1 + '@chakra-ui/menu': 2.1.9_q4wvhr4lnobfisc2b3szkx75cu + '@chakra-ui/modal': 2.2.9_xmdybzwbyd2chziouvdso5oidm + '@chakra-ui/number-input': 2.0.18_@chakra-ui+system@2.5.1 + '@chakra-ui/pin-input': 2.0.19_@chakra-ui+system@2.5.1 + '@chakra-ui/popover': 2.1.8_q4wvhr4lnobfisc2b3szkx75cu + '@chakra-ui/popper': 3.0.13 + '@chakra-ui/portal': 2.0.15_react-dom@18.2.0 + '@chakra-ui/progress': 2.1.5_@chakra-ui+system@2.5.1 + '@chakra-ui/provider': 2.1.2_kgmkmudaz6zokiyel5u2vpzy34 + '@chakra-ui/radio': 2.0.19_@chakra-ui+system@2.5.1 + '@chakra-ui/react-env': 3.0.0 + '@chakra-ui/select': 2.0.18_@chakra-ui+system@2.5.1 + '@chakra-ui/skeleton': 2.0.24_@chakra-ui+system@2.5.1 + '@chakra-ui/slider': 2.0.21_@chakra-ui+system@2.5.1 + '@chakra-ui/spinner': 2.0.13_@chakra-ui+system@2.5.1 + '@chakra-ui/stat': 2.0.17_@chakra-ui+system@2.5.1 + '@chakra-ui/styled-system': 2.6.1 + '@chakra-ui/switch': 2.0.22_q4wvhr4lnobfisc2b3szkx75cu + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq + '@chakra-ui/table': 2.0.16_@chakra-ui+system@2.5.1 + '@chakra-ui/tabs': 2.1.8_@chakra-ui+system@2.5.1 + '@chakra-ui/tag': 2.0.17_@chakra-ui+system@2.5.1 + '@chakra-ui/textarea': 2.0.18_@chakra-ui+system@2.5.1 + '@chakra-ui/theme': 2.2.5_es2flcfvdj7o2v4vs237ptvmhy + '@chakra-ui/theme-utils': 2.0.11 + '@chakra-ui/toast': 6.0.1_4ifpbvy5hyqik5cnhcwdcgtjca + '@chakra-ui/tooltip': 2.2.6_4ifpbvy5hyqik5cnhcwdcgtjca + '@chakra-ui/transition': 2.0.15_framer-motion@7.10.3 + '@chakra-ui/utils': 2.0.15 + '@chakra-ui/visually-hidden': 2.0.15_@chakra-ui+system@2.5.1 + '@emotion/react': 11.10.6_@types+react@18.0.28 + '@emotion/styled': 11.10.6_rtgl6lwupdrbo733hg3i5dx32q + framer-motion: 7.10.3_react-dom@18.2.0 + react-dom: 18.2.0 transitivePeerDependencies: - '@types/react' dev: false - /@chakra-ui/select/2.0.12_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-NCDMb0w48GYCHmazVSQ7/ysEpbnri+Up6n+v7yytf6g43TPRkikvK5CsVgLnAEj0lIdCJhWXTcZer5wG5KOEgA==} + /@chakra-ui/select/2.0.18_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-1d2lUT5LM6oOs5x4lzBh4GFDuXX62+lr+sgV7099g951/5UNbb0CS2hSZHsO7yZThLNbr7QTWZvAOAayVcGzdw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/form-control': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/form-control': 2.0.17_@chakra-ui+system@2.5.1 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/shared-utils/2.0.2: - resolution: {integrity: sha512-wC58Fh6wCnFFQyiebVZ0NI7PFW9+Vch0QE6qN7iR+bLseOzQY9miYuzPJ1kMYiFd6QTOmPJkI39M3wHqrPYiOg==} + /@chakra-ui/shared-utils/2.0.5: + resolution: {integrity: sha512-4/Wur0FqDov7Y0nCXl7HbHzCg4aq86h+SXdoUeuCMD3dSj7dpsVnStLYhng1vxvlbUnLpdF4oz5Myt3i/a7N3Q==} dev: false - /@chakra-ui/skeleton/2.0.17_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-dL7viXEKDEzmAJGbHMj+QbGl9PAd0VWztEcWcz5wOGfmAcJllA0lVh6NmG/yqLb6iXPCX4Y1Y0Yurm459TEYWg==} + /@chakra-ui/skeleton/2.0.24_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-1jXtVKcl/jpbrJlc/TyMsFyI651GTXY5ma30kWyTXoby2E+cxbV6OR8GB/NMZdGxbQBax8/VdtYVjI0n+OBqWA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/media-query': 3.2.7_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/react-use-previous': 2.0.2_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/media-query': 3.2.12_@chakra-ui+system@2.5.1 + '@chakra-ui/react-use-previous': 2.0.5 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/slider/2.0.12_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-Cna04J7e4+F3tJNb7tRNfPP+koicbDsKJBp+f1NpR32JbRzIfrf2Vdr4hfD5/uOfC4RGxnVInNZzZLGBelLtLw==} + /@chakra-ui/slider/2.0.21_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-Mm76yJxEqJl21+3waEcKg3tM8Y4elJ7mcViN6Brj35PTfzUJfSJxeBGo1nLPJ+X5jLj7o/L4kfBmUk3lY4QYEQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/number-utils': 2.0.4 - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/react-types': 2.0.3_react@18.2.0 - '@chakra-ui/react-use-callback-ref': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-controllable-state': 2.0.5_react@18.2.0 - '@chakra-ui/react-use-latest-ref': 2.0.2_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-pan-event': 2.0.5_react@18.2.0 - '@chakra-ui/react-use-size': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-update-effect': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/number-utils': 2.0.7 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-types': 2.0.7 + '@chakra-ui/react-use-callback-ref': 2.0.7 + '@chakra-ui/react-use-controllable-state': 2.0.8 + '@chakra-ui/react-use-latest-ref': 2.0.5 + '@chakra-ui/react-use-merge-refs': 2.0.7 + '@chakra-ui/react-use-pan-event': 2.0.9 + '@chakra-ui/react-use-size': 2.0.9 + '@chakra-ui/react-use-update-effect': 2.0.7 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/spinner/2.0.10_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-SwId1xPaaFAaEYrR9eHkQHAuB66CbxwjWaQonEjeEUSh9ecxkd5WbXlsQSyf2hVRIqXJg0m3HIYblcKUsQt9Rw==} + /@chakra-ui/spinner/2.0.13_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-T1/aSkVpUIuiYyrjfn1+LsQEG7Onbi1UE9ccS/evgf61Dzy4GgTXQUnDuWFSgpV58owqirqOu6jn/9eCwDlzlg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/stat/2.0.11_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-ZPFK2fKufDSHD8bp/KhO3jLgW/b3PzdG4zV+7iTO7OYjxm5pkBfBAeMqfXGx4cl51rtWUKzsY0HV4vLLjcSjHw==} + /@chakra-ui/stat/2.0.17_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-PhD+5oVLWjQmGLfeZSmexp3AtLcaggWBwoMZ4z8QMZIQzf/fJJWMk0bMqxlpTv8ORDkfY/4ImuFB/RJHvcqlcA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/icon': 3.0.16_@chakra-ui+system@2.5.1 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/styled-system/2.3.4: - resolution: {integrity: sha512-Lozbedu+GBj4EbHB/eGv475SFDLApsIEN9gNKiZJBJAE1HIhHn3Seh1iZQSrHC/Beq+D5cQq3Z+yPn3bXtFU7w==} + /@chakra-ui/styled-system/2.6.1: + resolution: {integrity: sha512-jy/1dVi1LxjoRCm+Eo5mqBgvPy5SCWMlIcz6GbIZBDpkGeKZwtqrZLjekxxLBCy8ORY+kJlUB0FT6AzVR/1tjw==} dependencies: + '@chakra-ui/shared-utils': 2.0.5 csstype: 3.1.1 lodash.mergewith: 4.6.2 dev: false - /@chakra-ui/switch/2.0.13_yxtycg2t66eeo2pchgyrvsgrfm: - resolution: {integrity: sha512-Ikj0L+SLLs/SnfGCiUChaldLIr/aizA1Q9D5+X6LtxpBnixFK/+fNXU+3juPDi9G1IFuNz2IAG51souO7C4nSQ==} + /@chakra-ui/switch/2.0.22_q4wvhr4lnobfisc2b3szkx75cu: + resolution: {integrity: sha512-+/Yy6y7VFD91uSPruF8ZvePi3tl5D8UNVATtWEQ+QBI92DLSM+PtgJ2F0Y9GMZ9NzMxpZ80DqwY7/kqcPCfLvw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/checkbox': 2.2.1_yxtycg2t66eeo2pchgyrvsgrfm - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - framer-motion: 7.5.3_biqbaboplfbrettd7655fr4n2y - react: 18.2.0 + '@chakra-ui/checkbox': 2.2.10_@chakra-ui+system@2.5.1 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq + framer-motion: 7.10.3_react-dom@18.2.0 dev: false - /@chakra-ui/system/2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq: - resolution: {integrity: sha512-I7hFQswp8tG6ogjEMFs5TsCItdCYuNxpLAULgUrLdOlsQeNnHNQhlL4zpIfD+VzguhsNy5lisbegAMKjdghTYg==} + /@chakra-ui/system/2.5.1_4b65gej4s4ehqmzxoznt4rfucq: + resolution: {integrity: sha512-4+86OrcSoq7lGkm5fh+sJ3IWXSTzjz+HOllRbCW2Rtnmcg7ritiXVNV2VygEg2DrCcx5+tNqRHDM764zW+AEug==} peerDependencies: '@emotion/react': ^11.0.0 '@emotion/styled': ^11.0.0 react: '>=18' dependencies: - '@chakra-ui/color-mode': 2.1.9_react@18.2.0 - '@chakra-ui/react-utils': 2.0.8_react@18.2.0 - '@chakra-ui/styled-system': 2.3.4 - '@chakra-ui/utils': 2.0.11 - '@emotion/react': 11.10.4_iapumuv4e6jcjznwuxpf4tt22e - '@emotion/styled': 11.10.4_g3tud4ene45llglqap74b5kkse - react: 18.2.0 + '@chakra-ui/color-mode': 2.1.12 + '@chakra-ui/object-utils': 2.0.8 + '@chakra-ui/react-utils': 2.0.12 + '@chakra-ui/styled-system': 2.6.1 + '@chakra-ui/theme-utils': 2.0.11 + '@chakra-ui/utils': 2.0.15 + '@emotion/react': 11.10.6_@types+react@18.0.28 + '@emotion/styled': 11.10.6_rtgl6lwupdrbo733hg3i5dx32q react-fast-compare: 3.2.0 dev: false - /@chakra-ui/table/2.0.11_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-zQTiqPKEgjdeO/PG0FByn0fH4sPF7dLJF+YszrIzDc6wvpD96iY6MYLeV+CSelbH1g0/uibcJ10PSaFStfGUZg==} + /@chakra-ui/table/2.0.16_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-vWDXZ6Ad3Aj66curp1tZBHvCfQHX2FJ4ijLiqGgQszWFIchfhJ5vMgEBJaFMZ+BN1draAjuRTZqaQefOApzvRg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/tabs/2.1.3_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-9gUcj49LMt5QQnfJHR/ctr9VPttJ97CtQWuH2Irjb3RXkq1TRrz6wjythPImNQUv1/DYyXp2jsUhoEQc4Oz14Q==} + /@chakra-ui/tabs/2.1.8_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-B7LeFN04Ny2jsSy5TFOQxnbZ6ITxGxLxsB2PE0vvQjMSblBrUryOxdjw80HZhfiw6od0ikK9CeKQOIt9QCguSw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/clickable': 2.0.10_react@18.2.0 - '@chakra-ui/descendant': 3.0.10_react@18.2.0 - '@chakra-ui/lazy-utils': 2.0.2 - '@chakra-ui/react-children-utils': 2.0.2 - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-controllable-state': 2.0.5_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-safe-layout-effect': 2.0.2_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/clickable': 2.0.14 + '@chakra-ui/descendant': 3.0.13 + '@chakra-ui/lazy-utils': 2.0.5 + '@chakra-ui/react-children-utils': 2.0.6 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-use-controllable-state': 2.0.8 + '@chakra-ui/react-use-merge-refs': 2.0.7 + '@chakra-ui/react-use-safe-layout-effect': 2.0.5 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/tag/2.0.11_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-iJJcX+4hl+6Se/8eCRzG+xxDwZfiYgc4Ly/8s93M0uW2GLb+ybbfSE2DjeKSyk3mQVeGzuxGkBfDHH2c2v26ew==} + /@chakra-ui/tag/2.0.17_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-A47zE9Ft9qxOJ+5r1cUseKRCoEdqCRzFm0pOtZgRcckqavglk75Xjgz8HbBpUO2zqqd49MlqdOwR8o87fXS1vg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/react-context': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/icon': 3.0.16_@chakra-ui+system@2.5.1 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/textarea/2.0.12_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-msR9YMynRXwZIqR6DgjQ2MogA/cW1syBx/R0v3es+9Zx8zlbuKdoLhYqajHteCup8dUzTeIH2Vs2vAwgq4wu5A==} + /@chakra-ui/textarea/2.0.18_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-aGHHb29vVifO0OtcK/k8cMykzjOKo/coDTU0NJqz7OOLAWIMNV2eGenvmO1n9tTZbmbqHiX+Sa1nPRX+pd14lg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/form-control': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 + '@chakra-ui/form-control': 2.0.17_@chakra-ui+system@2.5.1 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@chakra-ui/theme-tools/2.0.12_kzevwk2hxgy6fnrjqv25jdmqp4: - resolution: {integrity: sha512-mnMlKSmXkCjHUJsKWmJbgBTGF2vnLaMLv1ihkBn5eQcCubMQrBLTiMAEFl5pZdzuHItU6QdnLGA10smcXbNl0g==} + /@chakra-ui/theme-tools/2.0.17_es2flcfvdj7o2v4vs237ptvmhy: + resolution: {integrity: sha512-Auu38hnihlJZQcPok6itRDBbwof3TpXGYtDPnOvrq4Xp7jnab36HLt7KEXSDPXbtOk3ZqU99pvI1en5LbDrdjg==} peerDependencies: '@chakra-ui/styled-system': '>=2.0.0' dependencies: - '@chakra-ui/anatomy': 2.0.7 - '@chakra-ui/styled-system': 2.3.4 - '@ctrl/tinycolor': 3.4.1 + '@chakra-ui/anatomy': 2.1.2 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/styled-system': 2.6.1 + color2k: 2.0.2 + dev: false + + /@chakra-ui/theme-utils/2.0.11: + resolution: {integrity: sha512-lBAay6Sq3/fl7exd3mFxWAbzgdQowytor0fnlHrpNStn1HgFjXukwsf6356XQOie2Vd8qaMM7qZtMh4AiC0dcg==} + dependencies: + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/styled-system': 2.6.1 + '@chakra-ui/theme': 2.2.5_es2flcfvdj7o2v4vs237ptvmhy + lodash.mergewith: 4.6.2 dev: false - /@chakra-ui/theme/2.1.13_kzevwk2hxgy6fnrjqv25jdmqp4: - resolution: {integrity: sha512-qbrrvn9JstyLFV2qyhwgnhwzVs4zSJ4PcS3ScL8kafXJazTRU6onbCcjEZ5mVCw6z8uEz3jcE8Y5KIhVzaB+Xw==} + /@chakra-ui/theme/2.2.5_es2flcfvdj7o2v4vs237ptvmhy: + resolution: {integrity: sha512-hYASZMwu0NqEv6PPydu+F3I+kMNd44yR4TwjR/lXBz/LEh64L6UPY6kQjebCfgdVtsGdl3HKg+eLlfa7SvfRgw==} peerDependencies: '@chakra-ui/styled-system': '>=2.0.0' dependencies: - '@chakra-ui/anatomy': 2.0.7 - '@chakra-ui/styled-system': 2.3.4 - '@chakra-ui/theme-tools': 2.0.12_kzevwk2hxgy6fnrjqv25jdmqp4 + '@chakra-ui/anatomy': 2.1.2 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/styled-system': 2.6.1 + '@chakra-ui/theme-tools': 2.0.17_es2flcfvdj7o2v4vs237ptvmhy dev: false - /@chakra-ui/toast/3.0.13_svvprik4kmnbjwafrq4cgb4wou: - resolution: {integrity: sha512-5GADso5l5Tv1PAL1iocEneLs6U7I8HHMHSMvWdPFSmmJJh0XCH3fboh0C9LiFNIcnEGJmn+A5yGc4vjedA0h2A==} + /@chakra-ui/toast/6.0.1_4ifpbvy5hyqik5cnhcwdcgtjca: + resolution: {integrity: sha512-ej2kJXvu/d2h6qnXU5D8XTyw0qpsfmbiU7hUffo/sPxkz89AUOQ08RUuUmB1ssW/FZcQvNMJ5WgzCTKHGBxtxw==} peerDependencies: - '@chakra-ui/system': 2.2.12 + '@chakra-ui/system': 2.5.1 framer-motion: '>=4.0.0' react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/alert': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/close-button': 2.0.11_e25o37k4ko3jrmpkbvnp56dnfq - '@chakra-ui/portal': 2.0.10_biqbaboplfbrettd7655fr4n2y - '@chakra-ui/react-use-timeout': 2.0.2_react@18.2.0 - '@chakra-ui/react-use-update-effect': 2.0.4_react@18.2.0 - '@chakra-ui/styled-system': 2.3.4 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - '@chakra-ui/theme': 2.1.13_kzevwk2hxgy6fnrjqv25jdmqp4 - framer-motion: 7.5.3_biqbaboplfbrettd7655fr4n2y - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 + '@chakra-ui/alert': 2.0.17_@chakra-ui+system@2.5.1 + '@chakra-ui/close-button': 2.0.17_@chakra-ui+system@2.5.1 + '@chakra-ui/portal': 2.0.15_react-dom@18.2.0 + '@chakra-ui/react-context': 2.0.7 + '@chakra-ui/react-use-timeout': 2.0.5 + '@chakra-ui/react-use-update-effect': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/styled-system': 2.6.1 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq + '@chakra-ui/theme': 2.2.5_es2flcfvdj7o2v4vs237ptvmhy + framer-motion: 7.10.3_react-dom@18.2.0 + react-dom: 18.2.0 dev: false - /@chakra-ui/tooltip/2.2.0_svvprik4kmnbjwafrq4cgb4wou: - resolution: {integrity: sha512-oB97aQJBW+U3rRIt1ct7NaDRMnbW16JQ5ZBCl3BzN1VJWO3djiNuscpjVdZSceb+FdGSFo+GoDozp1ZwqdfFeQ==} + /@chakra-ui/tooltip/2.2.6_4ifpbvy5hyqik5cnhcwdcgtjca: + resolution: {integrity: sha512-4cbneidZ5+HCWge3OZzewRQieIvhDjSsl+scrl4Scx7E0z3OmqlTIESU5nGIZDBLYqKn/UirEZhqaQ33FOS2fw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/popper': 3.0.8_react@18.2.0 - '@chakra-ui/portal': 2.0.10_biqbaboplfbrettd7655fr4n2y - '@chakra-ui/react-types': 2.0.3_react@18.2.0 - '@chakra-ui/react-use-disclosure': 2.0.5_react@18.2.0 - '@chakra-ui/react-use-event-listener': 2.0.4_react@18.2.0 - '@chakra-ui/react-use-merge-refs': 2.0.4_react@18.2.0 - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - framer-motion: 7.5.3_biqbaboplfbrettd7655fr4n2y - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 + '@chakra-ui/popper': 3.0.13 + '@chakra-ui/portal': 2.0.15_react-dom@18.2.0 + '@chakra-ui/react-types': 2.0.7 + '@chakra-ui/react-use-disclosure': 2.0.8 + '@chakra-ui/react-use-event-listener': 2.0.7 + '@chakra-ui/react-use-merge-refs': 2.0.7 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq + framer-motion: 7.10.3_react-dom@18.2.0 + react-dom: 18.2.0 dev: false - /@chakra-ui/transition/2.0.10_3scsim3kjm5bnetripeelcaw6u: - resolution: {integrity: sha512-Tkkne8pIIY7f95TKt2aH+IAuQqzHmEt+ICPqvg74QbmIpKE5ptX0cd+P3swBANw4ACnJcCc2vWIaKmVBQ9clLw==} + /@chakra-ui/transition/2.0.15_framer-motion@7.10.3: + resolution: {integrity: sha512-o9LBK/llQfUDHF/Ty3cQ6nShpekKTqHUoJlUOzNKhoTsNpoRerr9v0jwojrX1YI02KtVjfhFU6PiqXlDfREoNw==} peerDependencies: framer-motion: '>=4.0.0' react: '>=18' dependencies: - framer-motion: 7.5.3_biqbaboplfbrettd7655fr4n2y - react: 18.2.0 + '@chakra-ui/shared-utils': 2.0.5 + framer-motion: 7.10.3_react-dom@18.2.0 dev: false - /@chakra-ui/utils/2.0.11: - resolution: {integrity: sha512-4ZQdK6tbOuTrUCsAQBHWo7tw5/Q6pBV93ZbVpats61cSWMFGv32AIQw9/hA4un2zDeSWN9ZMVLNjAY2Dq/KQOA==} + /@chakra-ui/utils/2.0.15: + resolution: {integrity: sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA==} dependencies: - '@types/lodash.mergewith': 4.6.6 + '@types/lodash.mergewith': 4.6.7 css-box-model: 1.2.1 - framesync: 5.3.0 + framesync: 6.1.2 lodash.mergewith: 4.6.2 dev: false - /@chakra-ui/visually-hidden/2.0.11_e25o37k4ko3jrmpkbvnp56dnfq: - resolution: {integrity: sha512-e+5amYvnsmEQdiWH4XMyvrtGTdwz//+48vwj5CsNWWcselzkwqodmciy5rIrT71/SCQDOtmgnL7ZWAUOffxfsQ==} + /@chakra-ui/visually-hidden/2.0.15_@chakra-ui+system@2.5.1: + resolution: {integrity: sha512-WWULIiucYRBIewHKFA7BssQ2ABLHLVd9lrUo3N3SZgR0u4ZRDDVEUNOy+r+9ruDze8+36dGbN9wsN1IdELtdOw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/system': 2.2.12_hfzxdiydbrbhhfpkwuv3jhvwmq - react: 18.2.0 - dev: false - - /@ctrl/tinycolor/3.4.1: - resolution: {integrity: sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw==} - engines: {node: '>=10'} + '@chakra-ui/system': 2.5.1_4b65gej4s4ehqmzxoznt4rfucq dev: false - /@emotion/babel-plugin/11.10.0: - resolution: {integrity: sha512-xVnpDAAbtxL1dsuSelU5A7BnY/lftws0wUexNJZTPsvX/1tM4GZJbclgODhvW4E+NH7E5VFcH0bBn30NvniPJA==} - peerDependencies: - '@babel/core': ^7.0.0 + /@emotion/babel-plugin/11.10.6: + resolution: {integrity: sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==} dependencies: '@babel/helper-module-imports': 7.18.6 - '@babel/plugin-syntax-jsx': 7.18.6 - '@babel/runtime': 7.18.9 + '@babel/runtime': 7.20.13 '@emotion/hash': 0.9.0 '@emotion/memoize': 0.8.0 - '@emotion/serialize': 1.1.0 + '@emotion/serialize': 1.1.1 babel-plugin-macros: 3.1.0 - convert-source-map: 1.8.0 + convert-source-map: 1.9.0 escape-string-regexp: 4.0.0 find-root: 1.1.0 source-map: 0.5.7 - stylis: 4.0.13 - dev: false - - /@emotion/cache/11.10.1: - resolution: {integrity: sha512-uZTj3Yz5D69GE25iFZcIQtibnVCFsc/6+XIozyL3ycgWvEdif2uEw9wlUt6umjLr4Keg9K6xRPHmD8LGi+6p1A==} - dependencies: - '@emotion/memoize': 0.8.0 - '@emotion/sheet': 1.2.0 - '@emotion/utils': 1.2.0 - '@emotion/weak-memoize': 0.3.0 - stylis: 4.0.13 + stylis: 4.1.3 dev: false - /@emotion/cache/11.10.3: - resolution: {integrity: sha512-Psmp/7ovAa8appWh3g51goxu/z3iVms7JXOreq136D8Bbn6dYraPnmL6mdM8GThEx9vwSn92Fz+mGSjBzN8UPQ==} + /@emotion/cache/11.10.5: + resolution: {integrity: sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==} dependencies: '@emotion/memoize': 0.8.0 - '@emotion/sheet': 1.2.0 + '@emotion/sheet': 1.2.1 '@emotion/utils': 1.2.0 '@emotion/weak-memoize': 0.3.0 - stylis: 4.0.13 + stylis: 4.1.3 dev: false /@emotion/hash/0.9.0: @@ -1620,78 +2678,68 @@ packages: resolution: {integrity: sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==} dev: false - /@emotion/react/11.10.4_iapumuv4e6jcjznwuxpf4tt22e: - resolution: {integrity: sha512-j0AkMpr6BL8gldJZ6XQsQ8DnS9TxEQu1R+OGmDZiWjBAJtCcbt0tS3I/YffoqHXxH6MjgI7KdMbYKw3MEiU9eA==} + /@emotion/react/11.10.6_@types+react@18.0.28: + resolution: {integrity: sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==} peerDependencies: - '@babel/core': ^7.0.0 '@types/react': '*' react: '>=16.8.0' peerDependenciesMeta: - '@babel/core': - optional: true '@types/react': optional: true dependencies: - '@babel/runtime': 7.18.9 - '@emotion/babel-plugin': 11.10.0 - '@emotion/cache': 11.10.1 - '@emotion/serialize': 1.1.0 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.0_react@18.2.0 + '@babel/runtime': 7.20.13 + '@emotion/babel-plugin': 11.10.6 + '@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 - '@types/react': 18.0.21 + '@types/react': 18.0.28 hoist-non-react-statics: 3.3.2 - react: 18.2.0 dev: false - /@emotion/serialize/1.1.0: - resolution: {integrity: sha512-F1ZZZW51T/fx+wKbVlwsfchr5q97iW8brAnXmsskz4d0hVB4O3M/SiA3SaeH06x02lSNzkkQv+n3AX3kCXKSFA==} + /@emotion/serialize/1.1.1: + resolution: {integrity: sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==} dependencies: '@emotion/hash': 0.9.0 '@emotion/memoize': 0.8.0 '@emotion/unitless': 0.8.0 '@emotion/utils': 1.2.0 - csstype: 3.1.0 + csstype: 3.1.1 dev: false - /@emotion/sheet/1.2.0: - resolution: {integrity: sha512-OiTkRgpxescko+M51tZsMq7Puu/KP55wMT8BgpcXVG2hqXc0Vo0mfymJ/Uj24Hp0i083ji/o0aLddh08UEjq8w==} + /@emotion/sheet/1.2.1: + resolution: {integrity: sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==} dev: false - /@emotion/styled/11.10.4_g3tud4ene45llglqap74b5kkse: - resolution: {integrity: sha512-pRl4R8Ez3UXvOPfc2bzIoV8u9P97UedgHS4FPX594ntwEuAMA114wlaHvOK24HB48uqfXiGlYIZYCxVJ1R1ttQ==} + /@emotion/styled/11.10.6_rtgl6lwupdrbo733hg3i5dx32q: + resolution: {integrity: sha512-OXtBzOmDSJo5Q0AFemHCfl+bUueT8BIcPSxu0EGTpGk6DmI5dnhSzQANm1e1ze0YZL7TDyAyy6s/b/zmGOS3Og==} peerDependencies: - '@babel/core': ^7.0.0 '@emotion/react': ^11.0.0-rc.0 '@types/react': '*' react: '>=16.8.0' peerDependenciesMeta: - '@babel/core': - optional: true '@types/react': optional: true dependencies: - '@babel/runtime': 7.18.9 - '@emotion/babel-plugin': 11.10.0 + '@babel/runtime': 7.20.13 + '@emotion/babel-plugin': 11.10.6 '@emotion/is-prop-valid': 1.2.0 - '@emotion/react': 11.10.4_iapumuv4e6jcjznwuxpf4tt22e - '@emotion/serialize': 1.1.0 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.0_react@18.2.0 + '@emotion/react': 11.10.6_@types+react@18.0.28 + '@emotion/serialize': 1.1.1 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.0 '@emotion/utils': 1.2.0 - '@types/react': 18.0.21 - react: 18.2.0 + '@types/react': 18.0.28 dev: false /@emotion/unitless/0.8.0: resolution: {integrity: sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==} dev: false - /@emotion/use-insertion-effect-with-fallbacks/1.0.0_react@18.2.0: + /@emotion/use-insertion-effect-with-fallbacks/1.0.0: resolution: {integrity: sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==} peerDependencies: react: '>=16.8.0' - dependencies: - react: 18.2.0 dev: false /@emotion/utils/1.2.0: @@ -1702,8 +2750,8 @@ packages: resolution: {integrity: sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==} dev: false - /@esbuild/android-arm/0.15.10: - resolution: {integrity: sha512-FNONeQPy/ox+5NBkcSbYJxoXj9GWu8gVGJTVmUyoOCKQFDTrHVKgNSzChdNt0I8Aj/iKcsDf2r9BFwv+FSNUXg==} + /@esbuild/android-arm/0.15.18: + resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==} engines: {node: '>=12'} cpu: [arm] os: [android] @@ -1711,8 +2759,8 @@ packages: dev: true optional: true - /@esbuild/linux-loong64/0.15.10: - resolution: {integrity: sha512-w0Ou3Z83LOYEkwaui2M8VwIp+nLi/NA60lBLMvaJ+vXVMcsARYdEzLNE7RSm4+lSg4zq4d7fAVuzk7PNQ5JFgg==} + /@esbuild/linux-loong64/0.15.18: + resolution: {integrity: sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==} engines: {node: '>=12'} cpu: [loong64] os: [linux] @@ -1720,95 +2768,214 @@ packages: dev: true optional: true - /@hookform/resolvers/2.9.8_react-hook-form@7.37.0: - resolution: {integrity: sha512-iVVjH0USq+1TqDdGkWe2M1x7Wn5OFPgVRo7CbWFsXTqqXqCaZtZcnzJu+UhljCWbthFWxWGXKLGYUDPZ04oVvQ==} + /@eslint/eslintrc/1.4.1: + resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.4.1 + globals: 13.20.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@floating-ui/core/1.2.1: + resolution: {integrity: sha512-LSqwPZkK3rYfD7GKoIeExXOyYx6Q1O4iqZWwIehDNuv3Dv425FIAE8PRwtAx1imEolFTHgBEcoFHm9MDnYgPCg==} + dev: false + + /@floating-ui/dom/1.2.1: + resolution: {integrity: sha512-Rt45SmRiV8eU+xXSB9t0uMYiQ/ZWGE/jumse2o3i5RGlyvcbqOF4q+1qBnzLE2kZ5JGhq0iMkcGXUKbFe7MpTA==} + dependencies: + '@floating-ui/core': 1.2.1 + dev: false + + /@hookform/resolvers/2.9.11_react-hook-form@7.43.1: + resolution: {integrity: sha512-bA3aZ79UgcHj7tFV7RlgThzwSSHZgvfbt2wprldRkYBcMopdMvHyO17Wwp/twcJasNFischFfS7oz8Katz8DdQ==} peerDependencies: react-hook-form: ^7.0.0 dependencies: - react-hook-form: 7.37.0_react@18.2.0 + react-hook-form: 7.43.1 dev: false + /@humanwhocodes/config-array/0.11.8: + resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer/1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema/1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true + /@jridgewell/gen-mapping/0.1.1: resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} engines: {node: '>=6.0.0'} dependencies: - '@jridgewell/set-array': 1.1.1 - '@jridgewell/sourcemap-codec': 1.4.13 + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.14 dev: true /@jridgewell/gen-mapping/0.3.2: resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} engines: {node: '>=6.0.0'} dependencies: - '@jridgewell/set-array': 1.1.1 - '@jridgewell/sourcemap-codec': 1.4.13 - '@jridgewell/trace-mapping': 0.3.13 + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/trace-mapping': 0.3.17 dev: true - /@jridgewell/resolve-uri/3.0.7: - resolution: {integrity: sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==} + /@jridgewell/resolve-uri/3.1.0: + resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} dev: true - /@jridgewell/set-array/1.1.1: - resolution: {integrity: sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==} + /@jridgewell/set-array/1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} dev: true - /@jridgewell/sourcemap-codec/1.4.13: - resolution: {integrity: sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==} + /@jridgewell/sourcemap-codec/1.4.14: + resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + dev: true + + /@jridgewell/trace-mapping/0.3.17: + resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 dev: true - /@jridgewell/trace-mapping/0.3.13: - resolution: {integrity: sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==} + /@moonrepo/cli/0.21.4: + resolution: {integrity: sha512-UnGLqCYvC9RpTxx5o8wt0SNxvcuoL3RQKTwKA7GIUUwUIIiGL7yCtOdGVrJ2yGOV8VLX/kc3xRLeWlfs3M98vg==} + hasBin: true + requiresBuild: true dependencies: - '@jridgewell/resolve-uri': 3.0.7 - '@jridgewell/sourcemap-codec': 1.4.13 + detect-libc: 2.0.1 + optionalDependencies: + '@moonrepo/core-linux-arm64-gnu': 0.21.4 + '@moonrepo/core-linux-arm64-musl': 0.21.4 + '@moonrepo/core-linux-x64-gnu': 0.21.4 + '@moonrepo/core-linux-x64-musl': 0.21.4 + '@moonrepo/core-macos-arm64': 0.21.4 + '@moonrepo/core-macos-x64': 0.21.4 + '@moonrepo/core-windows-x64-msvc': 0.21.4 + dev: true + + /@moonrepo/core-linux-arm64-gnu/0.21.4: + resolution: {integrity: sha512-krCDgcBi81mqMBlRQRUKCtDbYCxWqoLIghDPW/TjEiE8fhi+LTtMzmK2gmrkufbOwL4BCQBAy+m5rqvJmtmRGg==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@moonrepo/core-linux-arm64-musl/0.21.4: + resolution: {integrity: sha512-N5/7Q0yCn8MOUvds8K+wTTTQ/GB9GOPQ8mDeV05SnNEDnO5HJkzaasrmyaPDxeN4d1zzO4p72m6stNI0uN1DnA==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@moonrepo/core-linux-x64-gnu/0.21.4: + resolution: {integrity: sha512-xdr35Qlcw5dyvBGDqvBNIrzwSj++/3z6DcXnzVFqn0sToP9h4sNiqM2c3ov03f4fE/3cu6kztCkuKzHtMCabfg==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@moonrepo/core-linux-x64-musl/0.21.4: + resolution: {integrity: sha512-VPM3FfJ6IeQIBjJUl0SmLoAq7Sk5JOsoRsQBjCYWr841TZFtDuGdIQmUvsR0EQ12H2Vx6S3q95gC20Q1drPUCQ==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@moonrepo/core-macos-arm64/0.21.4: + resolution: {integrity: sha512-1f2M3m3efPJqhJPsOpPi17OmCvdYNBmNnA+XOqxfctNN4Pg2EfIp88cEV3gFo0daxEyXrbaBN+kZn53wc+trNw==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@moonrepo/core-macos-x64/0.21.4: + resolution: {integrity: sha512-njqux/WIjzeHyKsOQfjEkTxmIFoxghaFqTGAhv6cmfY1P4y9GHdd/tZNFBr4Vztn+gztJmUqrbbEdzhTWkeveA==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@moonrepo/core-windows-x64-msvc/0.21.4: + resolution: {integrity: sha512-IWipN/awOr0tLxLup0ANdsAo+P6RX8Egcg5jRM5vVP2LFBNpCjFl7ZxJT6jnTqGvrYyLcx87sJffLZLjzl5Zjg==} + cpu: [x64] + os: [win32] + requiresBuild: true dev: true + optional: true - /@motionone/animation/10.14.0: - resolution: {integrity: sha512-h+1sdyBP8vbxEBW5gPFDnj+m2DCqdlAuf2g6Iafb1lcMnqjsRXWlPw1AXgvUMXmreyhqmPbJqoNfIKdytampRQ==} + /@motionone/animation/10.15.1: + resolution: {integrity: sha512-mZcJxLjHor+bhcPuIFErMDNyrdb2vJur8lSfMCsuCB4UyV8ILZLvK+t+pg56erv8ud9xQGK/1OGPt10agPrCyQ==} dependencies: - '@motionone/easing': 10.14.0 - '@motionone/types': 10.14.0 - '@motionone/utils': 10.14.0 + '@motionone/easing': 10.15.1 + '@motionone/types': 10.15.1 + '@motionone/utils': 10.15.1 tslib: 2.4.0 dev: false - /@motionone/dom/10.13.1: - resolution: {integrity: sha512-zjfX+AGMIt/fIqd/SL1Lj93S6AiJsEA3oc5M9VkUr+Gz+juRmYN1vfvZd6MvEkSqEjwPQgcjN7rGZHrDB9APfQ==} + /@motionone/dom/10.15.5: + resolution: {integrity: sha512-Xc5avlgyh3xukU9tydh9+8mB8+2zAq+WlLsC3eEIp7Ax7DnXgY7Bj/iv0a4X2R9z9ZFZiaXK3BO0xMYHKbAAdA==} dependencies: - '@motionone/animation': 10.14.0 - '@motionone/generators': 10.14.0 - '@motionone/types': 10.14.0 - '@motionone/utils': 10.14.0 + '@motionone/animation': 10.15.1 + '@motionone/generators': 10.15.1 + '@motionone/types': 10.15.1 + '@motionone/utils': 10.15.1 hey-listen: 1.0.8 tslib: 2.4.0 dev: false - /@motionone/easing/10.14.0: - resolution: {integrity: sha512-2vUBdH9uWTlRbuErhcsMmt1jvMTTqvGmn9fHq8FleFDXBlHFs5jZzHJT9iw+4kR1h6a4SZQuCf72b9ji92qNYA==} + /@motionone/easing/10.15.1: + resolution: {integrity: sha512-6hIHBSV+ZVehf9dcKZLT7p5PEKHGhDwky2k8RKkmOvUoYP3S+dXsKupyZpqx5apjd9f+php4vXk4LuS+ADsrWw==} dependencies: - '@motionone/utils': 10.14.0 + '@motionone/utils': 10.15.1 tslib: 2.4.0 dev: false - /@motionone/generators/10.14.0: - resolution: {integrity: sha512-6kRHezoFfIjFN7pPpaxmkdZXD36tQNcyJe3nwVqwJ+ZfC0e3rFmszR8kp9DEVFs9QL/akWjuGPSLBI1tvz+Vjg==} + /@motionone/generators/10.15.1: + resolution: {integrity: sha512-67HLsvHJbw6cIbLA/o+gsm7h+6D4Sn7AUrB/GPxvujse1cGZ38F5H7DzoH7PhX+sjvtDnt2IhFYF2Zp1QTMKWQ==} dependencies: - '@motionone/types': 10.14.0 - '@motionone/utils': 10.14.0 + '@motionone/types': 10.15.1 + '@motionone/utils': 10.15.1 tslib: 2.4.0 dev: false - /@motionone/types/10.14.0: - resolution: {integrity: sha512-3bNWyYBHtVd27KncnJLhksMFQ5o2MSdk1cA/IZqsHtA9DnRM1SYgN01CTcJ8Iw8pCXF5Ocp34tyAjY7WRpOJJQ==} + /@motionone/types/10.15.1: + resolution: {integrity: sha512-iIUd/EgUsRZGrvW0jqdst8st7zKTzS9EsKkP+6c6n4MPZoQHwiHuVtTQLD6Kp0bsBLhNzKIBlHXponn/SDT4hA==} dev: false - /@motionone/utils/10.14.0: - resolution: {integrity: sha512-sLWBLPzRqkxmOTRzSaD3LFQXCPHvDzyHJ1a3VP9PRzBxyVd2pv51/gMOsdAcxQ9n+MIeGJnxzXBYplUHKj4jkw==} + /@motionone/utils/10.15.1: + resolution: {integrity: sha512-p0YncgU+iklvYr/Dq4NobTRdAPv9PveRDUXabPEeOjBLSO/1FNB2phNTZxOxpi1/GZwYpAoECEa0Wam+nsmhSw==} dependencies: - '@motionone/types': 10.14.0 + '@motionone/types': 10.15.1 hey-listen: 1.0.8 tslib: 2.4.0 dev: false @@ -1831,15 +2998,15 @@ packages: engines: {node: '>= 8'} dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.13.0 + fastq: 1.15.0 dev: true /@popperjs/core/2.11.6: resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} dev: false - /@remix-run/router/1.0.2: - resolution: {integrity: sha512-GRSOFhJzjGN+d4sKHTMSvNeUPoZiDHWmRnXfzaxrqe7dE/Nzlc8BiMSJdLDESZlndM7jIUrZ/F4yWqVYlI0rwQ==} + /@remix-run/router/1.3.2: + resolution: {integrity: sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA==} engines: {node: '>=14'} dev: false @@ -1865,8 +3032,19 @@ packages: engines: {node: '>=8'} dev: true - /@tailwindcss/typography/0.5.7_tailwindcss@3.1.8: - resolution: {integrity: sha512-JTTSTrgZfp6Ki4svhPA4mkd9nmQ/j9EfE7SbHJ1cLtthKkpW2OxsFXzSmxbhYbEkfNIyAyhle5p4SYyKRbz/jg==} + /@tailwindcss/typography/0.5.9: + resolution: {integrity: sha512-t8Sg3DyynFysV9f4JDOVISGsjazNb48AeIYQwcL+Bsq5uf4RYL75C1giZ43KISjeDGBaTN3Kxh7Xj/vRSMJUUg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + dependencies: + lodash.castarray: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + postcss-selector-parser: 6.0.10 + dev: true + + /@tailwindcss/typography/0.5.9_tailwindcss@3.2.7: + resolution: {integrity: sha512-t8Sg3DyynFysV9f4JDOVISGsjazNb48AeIYQwcL+Bsq5uf4RYL75C1giZ43KISjeDGBaTN3Kxh7Xj/vRSMJUUg==} peerDependencies: tailwindcss: '>=3.0.0 || insiders' dependencies: @@ -1874,37 +3052,54 @@ packages: lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 3.1.8 + tailwindcss: 3.2.7_postcss@8.4.21 dev: true - /@tanstack/match-sorter-utils/8.5.14: - resolution: {integrity: sha512-lVNhzTcOJ2bZ4IU+PeCPQ36vowBHvviJb2ZfdRFX5uhy7G0jM8N34zAMbmS5ZmVH8D2B7oU82OWo0e/5ZFzQrw==} + /@tanstack/match-sorter-utils/8.7.6: + resolution: {integrity: sha512-2AMpRiA6QivHOUiBpQAVxjiHAA68Ei23ZUMNaRJrN6omWiSFLoYrxGcT6BXtuzp0Jw4h6HZCmGGIM/gbwebO2A==} engines: {node: '>=12'} dependencies: remove-accents: 0.4.2 dev: false - /@tanstack/query-core/4.10.3: - resolution: {integrity: sha512-+ME02sUmBfx3Pjd+0XtEthK8/4rVMD2jcxvnW9DSgFONpKtpjzfRzjY4ykzpDw1QEo2eoPvl7NS8J5mNI199aA==} + /@tanstack/query-core/4.24.9: + resolution: {integrity: sha512-pZQ2NpdaHzx8gPPkAPh06d6zRkjfonUzILSYBXrdHDapP2eaBbGsx5L4/dMF+fyAglFzQZdDDzZgAykbM20QVw==} + dev: false + + /@tanstack/react-query-devtools/4.24.9_33a23s46don7ducusvogyrr4cq: + resolution: {integrity: sha512-NPsVf3pLMjH/XNTT5iP1q6isEBNE4kWF/IBSbxNUt0tDWK1nS1qgWr9ySTXwimsAHOWMjHkFuUF8VBRmz+axKg==} + peerDependencies: + '@tanstack/react-query': 4.24.9 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@tanstack/match-sorter-utils': 8.7.6 + '@tanstack/react-query': 4.24.9_react-dom@18.2.0 + react-dom: 18.2.0 + superjson: 1.12.2 + use-sync-external-store: 1.2.0 dev: false - /@tanstack/react-query-devtools/4.10.4_vhepussragssjkevgznguhxexq: - resolution: {integrity: sha512-+V2wxeDyXYAUTSP/kUy6JsuFXDDHBouexBINaRCNTgPe0xcFP34syWAATVTvyarpqgYhIYLJtkYzT1JL2wM8TQ==} + /@tanstack/react-query/4.24.9_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-6WLwUT9mrngIinRtcZjrWOUENOuLbWvQpKmU6DZCo2iPQVA+qvv3Ji90Amme4AkUyWQ8ZSSRTnAFq8V2tj2ACg==} peerDependencies: - '@tanstack/react-query': 4.10.3 react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true dependencies: - '@tanstack/match-sorter-utils': 8.5.14 - '@tanstack/react-query': 4.10.3_biqbaboplfbrettd7655fr4n2y + '@tanstack/query-core': 4.24.9 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 - superjson: 1.10.0 use-sync-external-store: 1.2.0_react@18.2.0 dev: false - /@tanstack/react-query/4.10.3: - resolution: {integrity: sha512-4OEJjkcsCTmG3ui7RjsVzsXerWQvInTe95CBKFyOV/GchMUlNztoFnnYmlMhX7hLUqJMhbG9l7M507V7+xU8Hw==} + /@tanstack/react-query/4.24.9_react-dom@18.2.0: + resolution: {integrity: sha512-6WLwUT9mrngIinRtcZjrWOUENOuLbWvQpKmU6DZCo2iPQVA+qvv3Ji90Amme4AkUyWQ8ZSSRTnAFq8V2tj2ACg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -1915,12 +3110,13 @@ packages: react-native: optional: true dependencies: - '@tanstack/query-core': 4.10.3 + '@tanstack/query-core': 4.24.9 + react-dom: 18.2.0 use-sync-external-store: 1.2.0 dev: false - /@tanstack/react-query/4.10.3_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-4OEJjkcsCTmG3ui7RjsVzsXerWQvInTe95CBKFyOV/GchMUlNztoFnnYmlMhX7hLUqJMhbG9l7M507V7+xU8Hw==} + /@tanstack/react-query/4.24.9_react@18.2.0: + resolution: {integrity: sha512-6WLwUT9mrngIinRtcZjrWOUENOuLbWvQpKmU6DZCo2iPQVA+qvv3Ji90Amme4AkUyWQ8ZSSRTnAFq8V2tj2ACg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -1931,36 +3127,48 @@ packages: react-native: optional: true dependencies: - '@tanstack/query-core': 4.10.3 + '@tanstack/query-core': 4.24.9 react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 use-sync-external-store: 1.2.0_react@18.2.0 dev: false - /@tanstack/react-table/8.5.15_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-9rSvhIFeMpfXksFgQNTWnVoJbkae/U8CkHnHYGWAIB/O0Ca51IKap0Rjp5WkIUVBWxJ7Wfl2y13oY+aWcyM6Rg==} + /@tanstack/react-table/8.7.9_react-dom@18.2.0: + resolution: {integrity: sha512-6MbbQn5AupSOkek1+6IYu+1yZNthAKTRZw9tW92Vi6++iRrD1GbI3lKTjJalf8lEEKOqapPzQPE20nywu0PjCA==} engines: {node: '>=12'} peerDependencies: react: '>=16' react-dom: '>=16' dependencies: - '@tanstack/table-core': 8.5.15 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 + '@tanstack/table-core': 8.7.9 + react-dom: 18.2.0 + dev: false + + /@tanstack/react-virtual/3.0.0-beta.18: + resolution: {integrity: sha512-mnyCZT6htcRNw1jVb+WyfMUMbd1UmXX/JWPuMf6Bmj92DB/V7Ogk5n5rby5Y5aste7c7mlsBeMF8HtpwERRvEQ==} + engines: {node: '>=12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@tanstack/virtual-core': 3.0.0-beta.18 dev: false - /@tanstack/table-core/8.5.15: - resolution: {integrity: sha512-k+BcCOAYD610Cij6p1BPyEqjMQjZIdAnVDoIUKVnA/tfHbF4JlDP7pKAftXPBxyyX5Z1yQPurPnOdEY007Snyg==} + /@tanstack/table-core/8.7.9: + resolution: {integrity: sha512-4RkayPMV1oS2SKDXfQbFoct1w5k+pvGpmX18tCXMofK/VDRdA2hhxfsQlMvsJ4oTX8b0CI4Y3GDKn5T425jBCw==} engines: {node: '>=12'} dev: false - /@tauri-apps/api/1.1.0: - resolution: {integrity: sha512-n13pIqdPd3KtaMmmAcrU7BTfdMtIlGNnfZD0dNX8L4p8dgmuNyikm6JAA+yCpl9gqq6I8x5cV2Y0muqdgD0cWw==} - engines: {node: '>= 12.22.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} + /@tanstack/virtual-core/3.0.0-beta.18: + resolution: {integrity: sha512-tcXutY05NpN9lp3+AXI9Sn85RxSPV0EJC0XMim9oeQj/E7bjXoL0qZ4Er4wwnvIbv/hZjC91EmbIQGjgdr6nZg==} + engines: {node: '>=12'} + dev: false + + /@tauri-apps/api/1.2.0: + resolution: {integrity: sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==} + engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} dev: false - /@tauri-apps/cli-darwin-arm64/1.1.1: - resolution: {integrity: sha512-qBG11ig525/qf0f5OQxn0ON3hT8YdpTfpa4Y4kVqBJhdW50R5fadPv6tv5Dpl2TS2X7nWh/zg5mEXYoCK3HZ9w==} + /@tauri-apps/cli-darwin-arm64/1.2.3: + resolution: {integrity: sha512-phJN3fN8FtZZwqXg08bcxfq1+X1JSDglLvRxOxB7VWPq+O5SuB8uLyssjJsu+PIhyZZnIhTGdjhzLSFhSXfLsw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -1968,8 +3176,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-darwin-x64/1.1.1: - resolution: {integrity: sha512-M3dMsp78OdxisbTwAWGvy3jIb3uqThtQcUYVvqOu9LeEOHyldOBFDSht+6PTBpaJLAHFMQK2rmNxiWgigklJaA==} + /@tauri-apps/cli-darwin-x64/1.2.3: + resolution: {integrity: sha512-jFZ/y6z8z6v4yliIbXKBXA7BJgtZVMsITmEXSuD6s5+eCOpDhQxbRkr6CA+FFfr+/r96rWSDSgDenDQuSvPAKw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -1977,8 +3185,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-linux-arm-gnueabihf/1.1.1: - resolution: {integrity: sha512-LYlvdAd73cq+yTi6rw7j/DWIvDpeApwgQkIn+HYsNNeFhyFmABU7tmw+pekK3W3nHAkYAJ69Rl4ZdoxdNGKmHg==} + /@tauri-apps/cli-linux-arm-gnueabihf/1.2.3: + resolution: {integrity: sha512-C7h5vqAwXzY0kRGSU00Fj8PudiDWFCiQqqUNI1N+fhCILrzWZB9TPBwdx33ZfXKt/U4+emdIoo/N34v3TiAOmQ==} engines: {node: '>= 10'} cpu: [arm] os: [linux] @@ -1986,8 +3194,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-linux-arm64-gnu/1.1.1: - resolution: {integrity: sha512-o/hbMQIKuFI7cTNpeQBHD/OCNJOBIci78faKms/t6AstLXx0QJuRHDk477Rg6VVy/I3BBKbyATALbmcTq+ti0A==} + /@tauri-apps/cli-linux-arm64-gnu/1.2.3: + resolution: {integrity: sha512-buf1c8sdkuUzVDkGPQpyUdAIIdn5r0UgXU6+H5fGPq/Xzt5K69JzXaeo6fHsZEZghbV0hOK+taKV4J0m30UUMQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1995,8 +3203,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-linux-arm64-musl/1.1.1: - resolution: {integrity: sha512-8Ci4qlDnXIp93XqUrtzFCBDatUzPHpZq7L3bociUbWpvy/bnlzxp1C/C+vwdc4uS1MiAp9v3BFgrU4i0f0Z3QQ==} + /@tauri-apps/cli-linux-arm64-musl/1.2.3: + resolution: {integrity: sha512-x88wPS9W5xAyk392vc4uNHcKBBvCp0wf4H9JFMF9OBwB7vfd59LbQCFcPSu8f0BI7bPrOsyHqspWHuFL8ojQEA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -2004,8 +3212,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-linux-x64-gnu/1.1.1: - resolution: {integrity: sha512-ES4Bkx2JAI8+dDNDJswhLS3yqt+yT/4C6UfGOPIHFxcXUh6fe36eUllrTt+HLRS9xTZbYnteJy7ebq2TqMkaxw==} + /@tauri-apps/cli-linux-x64-gnu/1.2.3: + resolution: {integrity: sha512-ZMz1jxEVe0B4/7NJnlPHmwmSIuwiD6ViXKs8F+OWWz2Y4jn5TGxWKFg7DLx5OwQTRvEIZxxT7lXHi5CuTNAxKg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -2013,8 +3221,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-linux-x64-musl/1.1.1: - resolution: {integrity: sha512-qrN1WOMAaDl+LE8P8iO0+DYlrWNTc9jIu/CsnVY/LImTn79ZPxEkcVBo0UGeKRI7f10TfvkVmLCBLxTz8QhEyA==} + /@tauri-apps/cli-linux-x64-musl/1.2.3: + resolution: {integrity: sha512-B/az59EjJhdbZDzawEVox0LQu2ZHCZlk8rJf85AMIktIUoAZPFbwyiUv7/zjzA/sY6Nb58OSJgaPL2/IBy7E0A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -2022,8 +3230,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-win32-ia32-msvc/1.1.1: - resolution: {integrity: sha512-vw7VOmrQlywHhFV3pf54udf2FRNj9dg9WP1gL0My55FnB+w+PWS9Ipm871kX5qepmChdnZHKq9fsqE2uTjX//Q==} + /@tauri-apps/cli-win32-ia32-msvc/1.2.3: + resolution: {integrity: sha512-ypdO1OdC5ugNJAKO2m3sb1nsd+0TSvMS9Tr5qN/ZSMvtSduaNwrcZ3D7G/iOIanrqu/Nl8t3LYlgPZGBKlw7Ng==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] @@ -2031,8 +3239,8 @@ packages: dev: true optional: true - /@tauri-apps/cli-win32-x64-msvc/1.1.1: - resolution: {integrity: sha512-OukxlLLi3AoCN4ABnqCDTiiC7xJGWukAjrKCIx7wFISrLjNfsrnH7/UOzuopfGpZChSe2c+AamVmcpBfVsEmJA==} + /@tauri-apps/cli-win32-x64-msvc/1.2.3: + resolution: {integrity: sha512-CsbHQ+XhnV/2csOBBDVfH16cdK00gNyNYUW68isedmqcn8j+s0e9cQ1xXIqi+Hue3awp8g3ImYN5KPepf3UExw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2040,35 +3248,50 @@ packages: dev: true optional: true - /@tauri-apps/cli/1.1.1: - resolution: {integrity: sha512-80kjMEMPBwLYCp0tTKSquy90PHHGGBvZsneNr3B/mWxNsvjzA1C0vOyGJGFrJuT2OmkvrdvuJZ5mch5hL8O1Xg==} + /@tauri-apps/cli/1.2.3: + resolution: {integrity: sha512-erxtXuPhMEGJPBtnhPILD4AjuT81GZsraqpFvXAmEJZ2p8P6t7MVBifCL8LznRknznM3jn90D3M8RNBP3wcXTw==} engines: {node: '>= 10'} hasBin: true optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 1.1.1 - '@tauri-apps/cli-darwin-x64': 1.1.1 - '@tauri-apps/cli-linux-arm-gnueabihf': 1.1.1 - '@tauri-apps/cli-linux-arm64-gnu': 1.1.1 - '@tauri-apps/cli-linux-arm64-musl': 1.1.1 - '@tauri-apps/cli-linux-x64-gnu': 1.1.1 - '@tauri-apps/cli-linux-x64-musl': 1.1.1 - '@tauri-apps/cli-win32-ia32-msvc': 1.1.1 - '@tauri-apps/cli-win32-x64-msvc': 1.1.1 + '@tauri-apps/cli-darwin-arm64': 1.2.3 + '@tauri-apps/cli-darwin-x64': 1.2.3 + '@tauri-apps/cli-linux-arm-gnueabihf': 1.2.3 + '@tauri-apps/cli-linux-arm64-gnu': 1.2.3 + '@tauri-apps/cli-linux-arm64-musl': 1.2.3 + '@tauri-apps/cli-linux-x64-gnu': 1.2.3 + '@tauri-apps/cli-linux-x64-musl': 1.2.3 + '@tauri-apps/cli-win32-ia32-msvc': 1.2.3 + '@tauri-apps/cli-win32-x64-msvc': 1.2.3 dev: true /@types/axios/0.14.0: resolution: {integrity: sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==} deprecated: This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed! dependencies: - axios: 1.1.2 + axios: 1.3.3 transitivePeerDependencies: - debug dev: true + /@types/eslint/8.21.1: + resolution: {integrity: sha512-rc9K8ZpVjNcLs8Fp0dkozd5Pt2Apk1glO4Vgz8ix1u6yFByxfqo5Yavpy65o+93TAe24jr7v+eSBtFLvOQtCRQ==} + dependencies: + '@types/estree': 1.0.0 + '@types/json-schema': 7.0.11 + dev: true + + /@types/estree/1.0.0: + resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} + dev: true + /@types/history/4.7.11: resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} dev: true + /@types/json-schema/7.0.11: + resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + dev: true + /@types/localforage/0.0.34: resolution: {integrity: sha512-tJxahnjm9dEI1X+hQSC5f2BSd/coZaqbIl1m3TCl0q9SVuC52XcXfV0XmoCU1+PmjyucuVITwoTnN8OlTbEXXA==} deprecated: This is a stub types definition for localforage (https://github.com/localForage/localForage). localforage provides its own type definitions, so you don't need @types/localforage installed! @@ -2076,22 +3299,22 @@ packages: localforage: 1.10.0 dev: false - /@types/lodash.mergewith/4.6.6: - resolution: {integrity: sha512-RY/8IaVENjG19rxTZu9Nukqh0W2UrYgmBj5sdns4hWRZaV8PqR7wIKHFKzvOTjo4zVRV7sVI+yFhAJql12Kfqg==} + /@types/lodash.mergewith/4.6.7: + resolution: {integrity: sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A==} dependencies: - '@types/lodash': 4.14.186 + '@types/lodash': 4.14.191 dev: false - /@types/lodash/4.14.186: - resolution: {integrity: sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw==} + /@types/lodash/4.14.191: + resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} dev: false /@types/minimist/1.2.2: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} dev: true - /@types/node/18.8.3: - resolution: {integrity: sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==} + /@types/node/18.14.0: + resolution: {integrity: sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A==} dev: true /@types/normalize-package-data/2.4.1: @@ -2110,44 +3333,48 @@ packages: resolution: {integrity: sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA==} dev: true + /@types/prettier/2.7.2: + resolution: {integrity: sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==} + dev: true + /@types/prop-types/15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} - /@types/react-dom/18.0.6: - resolution: {integrity: sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==} + /@types/react-dom/18.0.11: + resolution: {integrity: sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==} dependencies: - '@types/react': 18.0.21 + '@types/react': 18.0.28 dev: true - /@types/react-helmet/6.1.5: - resolution: {integrity: sha512-/ICuy7OHZxR0YCAZLNg9r7I9aijWUWvxaPR6uTuyxe8tAj5RL4Sw1+R6NhXUtOsarkGYPmaHdBDvuXh2DIN/uA==} + /@types/react-helmet/6.1.6: + resolution: {integrity: sha512-ZKcoOdW/Tg+kiUbkFCBtvDw0k3nD4HJ/h/B9yWxN4uDO8OkRksWTO+EL+z/Qu3aHTeTll3Ro0Cc/8UhwBCMG5A==} dependencies: - '@types/react': 18.0.21 + '@types/react': 18.0.28 dev: true /@types/react-router-dom/5.3.3: resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} dependencies: '@types/history': 4.7.11 - '@types/react': 18.0.21 - '@types/react-router': 5.1.18 + '@types/react': 18.0.28 + '@types/react-router': 5.1.20 dev: true - /@types/react-router/5.1.18: - resolution: {integrity: sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g==} + /@types/react-router/5.1.20: + resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} dependencies: '@types/history': 4.7.11 - '@types/react': 18.0.21 + '@types/react': 18.0.28 dev: true /@types/react-transition-group/4.4.5: resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==} dependencies: - '@types/react': 18.0.21 + '@types/react': 18.0.28 dev: false - /@types/react/18.0.21: - resolution: {integrity: sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA==} + /@types/react/18.0.28: + resolution: {integrity: sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==} dependencies: '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.2 @@ -2156,6 +3383,10 @@ packages: /@types/scheduler/0.16.2: resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} + /@types/semver/7.3.13: + resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} + dev: true + /@types/strip-bom/3.0.0: resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} dev: true @@ -2164,35 +3395,165 @@ packages: resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} dev: true - /@vitejs/plugin-react/2.1.0_vite@3.1.6: - resolution: {integrity: sha512-am6rPyyU3LzUYne3Gd9oj9c4Rzbq5hQnuGXSMT6Gujq45Il/+bunwq3lrB7wghLkiF45ygMwft37vgJ/NE8IAA==} + /@typescript-eslint/eslint-plugin/5.52.0_6cfvjsbua5ptj65675bqcn6oza: + resolution: {integrity: sha512-lHazYdvYVsBokwCdKOppvYJKaJ4S41CgKBcPvyd0xjZNbvQdhn/pnJlGtQksQ/NhInzdaeaSarlBjDXHuclEbg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/parser': 5.52.0_7kw3g6rralp5ps6mg3uyzz6azm + '@typescript-eslint/scope-manager': 5.52.0 + '@typescript-eslint/type-utils': 5.52.0_7kw3g6rralp5ps6mg3uyzz6azm + '@typescript-eslint/utils': 5.52.0_7kw3g6rralp5ps6mg3uyzz6azm + debug: 4.3.4 + eslint: 8.34.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 + natural-compare-lite: 1.4.0 + regexpp: 3.2.0 + semver: 7.3.8 + tsutils: 3.21.0_typescript@4.9.5 + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser/5.52.0_7kw3g6rralp5ps6mg3uyzz6azm: + resolution: {integrity: sha512-e2KiLQOZRo4Y0D/b+3y08i3jsekoSkOYStROYmPUnGMEoA0h+k2qOH5H6tcjIc68WDvGwH+PaOrP1XRzLJ6QlA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.52.0 + '@typescript-eslint/types': 5.52.0 + '@typescript-eslint/typescript-estree': 5.52.0_typescript@4.9.5 + debug: 4.3.4 + eslint: 8.34.0 + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager/5.52.0: + resolution: {integrity: sha512-AR7sxxfBKiNV0FWBSARxM8DmNxrwgnYMPwmpkC1Pl1n+eT8/I2NAUPuwDy/FmDcC6F8pBfmOcaxcxRHspgOBMw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.52.0 + '@typescript-eslint/visitor-keys': 5.52.0 + dev: true + + /@typescript-eslint/type-utils/5.52.0_7kw3g6rralp5ps6mg3uyzz6azm: + resolution: {integrity: sha512-tEKuUHfDOv852QGlpPtB3lHOoig5pyFQN/cUiZtpw99D93nEBjexRLre5sQZlkMoHry/lZr8qDAt2oAHLKA6Jw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.52.0_typescript@4.9.5 + '@typescript-eslint/utils': 5.52.0_7kw3g6rralp5ps6mg3uyzz6azm + debug: 4.3.4 + eslint: 8.34.0 + tsutils: 3.21.0_typescript@4.9.5 + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types/5.52.0: + resolution: {integrity: sha512-oV7XU4CHYfBhk78fS7tkum+/Dpgsfi91IIDy7fjCyq2k6KB63M6gMC0YIvy+iABzmXThCRI6xpCEyVObBdWSDQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@typescript-eslint/typescript-estree/5.52.0_typescript@4.9.5: + resolution: {integrity: sha512-WeWnjanyEwt6+fVrSR0MYgEpUAuROxuAH516WPjUblIrClzYJj0kBbjdnbQXLpgAN8qbEuGywiQsXUVDiAoEuQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + 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.8 + tsutils: 3.21.0_typescript@4.9.5 + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils/5.52.0_7kw3g6rralp5ps6mg3uyzz6azm: + resolution: {integrity: sha512-As3lChhrbwWQLNk2HC8Ree96hldKIqk98EYvypd3It8Q1f8d5zWyIoaZEp2va5667M4ZyE7X8UUR+azXrFl+NA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@types/json-schema': 7.0.11 + '@types/semver': 7.3.13 + '@typescript-eslint/scope-manager': 5.52.0 + '@typescript-eslint/types': 5.52.0 + '@typescript-eslint/typescript-estree': 5.52.0_typescript@4.9.5 + eslint: 8.34.0 + eslint-scope: 5.1.1 + eslint-utils: 3.0.0_eslint@8.34.0 + semver: 7.3.8 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys/5.52.0: + resolution: {integrity: sha512-qMwpw6SU5VHCPr99y274xhbm+PRViK/NATY6qzt+Et7+mThGuFSl/ompj2/hrBlRP/kq+BFdgagnOSgw9TB0eA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.52.0 + eslint-visitor-keys: 3.3.0 + dev: true + + /@vitejs/plugin-react/2.2.0_vite@3.2.5: + resolution: {integrity: sha512-FFpefhvExd1toVRlokZgxgy2JtnBOdp4ZDsq7ldCWaqGSGn9UhWMAVm/1lxPL14JfNS5yGz+s9yFrQY6shoStA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^3.0.0 dependencies: - '@babel/core': 7.19.0 - '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.19.0 - '@babel/plugin-transform-react-jsx-development': 7.18.6_@babel+core@7.19.0 - '@babel/plugin-transform-react-jsx-self': 7.18.6_@babel+core@7.19.0 - '@babel/plugin-transform-react-jsx-source': 7.18.6_@babel+core@7.19.0 - magic-string: 0.26.3 + '@babel/core': 7.20.12 + '@babel/plugin-transform-react-jsx': 7.20.13_@babel+core@7.20.12 + '@babel/plugin-transform-react-jsx-development': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-react-jsx-self': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-react-jsx-source': 7.19.6_@babel+core@7.20.12 + magic-string: 0.26.7 react-refresh: 0.14.0 - vite: 3.1.6 + vite: 3.2.5 transitivePeerDependencies: - supports-color dev: true - /@xmldom/xmldom/0.7.5: - resolution: {integrity: sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==} + /@xmldom/xmldom/0.7.9: + resolution: {integrity: sha512-yceMpm/xd4W2a85iqZyO09gTnHvXF6pyiWjD2jcOJs7hRoZtNNOO1eJlhHj1ixA+xip2hOyGn+LgcvLCMo5zXA==} engines: {node: '>=10.0.0'} dev: false - /@zag-js/element-size/0.1.0: - resolution: {integrity: sha512-QF8wp0+V8++z+FHXiIw93+zudtubYszOtYbNgK39fg3pi+nCZtuSm4L1jC5QZMatNZ83MfOzyNCfgUubapagJQ==} + /@zag-js/element-size/0.3.1: + resolution: {integrity: sha512-jR5j4G//bRzcxwAACWi9EfITnwjNmn10LxF4NmALrdZU7/PNWP3uUCdhCxd/0SCyeiJXUl0yvD57rWAbKPs1nw==} dev: false - /@zag-js/focus-visible/0.1.0: - resolution: {integrity: sha512-PeaBcTmdZWcFf7n1aM+oiOdZc+sy14qi0emPIeUuGMTjbP0xLGrZu43kdpHnWSXy7/r4Ubp/vlg50MCV8+9Isg==} + /@zag-js/focus-visible/0.2.1: + resolution: {integrity: sha512-19uTjoZGP4/Ax7kSNhhay9JA83BirKzpqLkeEAilrpdI1hE5xuq6q+tzJOsrMOOqJrm7LkmZp5lbsTQzvK2pYg==} dev: false /accepts/1.3.8: @@ -2203,6 +3564,22 @@ packages: negotiator: 0.6.3 dev: true + /acorn-jsx/5.3.2_acorn@7.4.1: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 7.4.1 + dev: true + + /acorn-jsx/5.3.2_acorn@8.8.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.8.2 + dev: true + /acorn-node/1.8.2: resolution: {integrity: sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==} dependencies: @@ -2222,6 +3599,12 @@ packages: hasBin: true dev: true + /acorn/8.8.2: + resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + /aggregate-error/3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -2238,10 +3621,19 @@ packages: indent-string: 5.0.0 dev: true - /ajv/6.10.0: - resolution: {integrity: sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==} + /ajv/6.10.0: + resolution: {integrity: sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==} + dependencies: + fast-deep-equal: 2.0.1 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ajv/6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: - fast-deep-equal: 2.0.1 + fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 @@ -2255,7 +3647,7 @@ packages: bluebird: 3.7.2 buffer-more-ints: 0.0.2 readable-stream: 1.1.14 - safe-buffer: 5.1.2 + safe-buffer: 5.2.1 dev: true /ansi-escapes/4.3.2: @@ -2265,6 +3657,16 @@ packages: type-fest: 0.21.3 dev: true + /ansi-regex/2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + dev: true + + /ansi-regex/3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + dev: true + /ansi-regex/5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2275,6 +3677,11 @@ packages: engines: {node: '>=12'} dev: true + /ansi-styles/2.2.1: + resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} + engines: {node: '>=0.10.0'} + dev: true + /ansi-styles/3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -2288,13 +3695,13 @@ packages: color-convert: 2.0.1 dev: true - /ansi-styles/6.1.1: - resolution: {integrity: sha512-qDOv24WjnYuL+wbwHdlsYZFy+cgPtrYw0Tn7GLORicQp9BkQLzrgI3Pm4VyR9ERZ41YTn7KlMPuL1n05WdZvmg==} + /ansi-styles/6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} dev: true - /anymatch/3.1.2: - resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} + /anymatch/3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} dependencies: normalize-path: 3.0.0 @@ -2310,8 +3717,12 @@ packages: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} dev: true - /aria-hidden/1.2.1_iapumuv4e6jcjznwuxpf4tt22e: - resolution: {integrity: sha512-PN344VAf9j1EAi+jyVHOJ8XidQdPVssGco39eNcsGdM4wcsILtxrKLkbuiMfLWYROK1FjRQasMWCBttrhjnr6A==} + /argparse/2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /aria-hidden/1.2.2_@types+react@18.0.28: + resolution: {integrity: sha512-6y/ogyDTk/7YAe91T3E2PR1ALVKyM2QbTio5HwM+N1Q6CMlCKhvClyIjkckBswa0f2xJhjsfzIGa1yVSe1UMVA==} engines: {node: '>=10'} peerDependencies: '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 @@ -2320,15 +3731,25 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.0.21 - react: 18.2.0 - tslib: 2.4.0 + '@types/react': 18.0.28 + tslib: 2.5.0 dev: false /array-flatten/1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} dev: true + /array-includes/3.1.6: + resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.1 + get-intrinsic: 1.2.0 + is-string: 1.0.7 + dev: true + /array-union/1.0.2: resolution: {integrity: sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==} engines: {node: '>=0.10.0'} @@ -2336,22 +3757,47 @@ packages: array-uniq: 1.0.3 dev: true + /array-union/2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + /array-uniq/1.0.3: resolution: {integrity: sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==} engines: {node: '>=0.10.0'} dev: true - /array.prototype.reduce/1.0.4: - resolution: {integrity: sha512-WnM+AjG/DvLRLo4DDl+r+SvCzYtD2Jd9oeBYMcEaI7t3fFrHY9M53/wdLcTvmZNQ70IU6Htj0emFkZ5TS+lrdw==} + /array.prototype.flatmap/1.3.1: + resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.1 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.reduce/1.0.5: + resolution: {integrity: sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.1 + define-properties: 1.2.0 + es-abstract: 1.21.1 es-array-method-boxes-properly: 1.0.0 is-string: 1.0.7 dev: true + /array.prototype.tosorted/1.1.1: + resolution: {integrity: sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.1 + es-shim-unscopables: 1.0.0 + get-intrinsic: 1.2.0 + dev: true + /arrify/1.0.1: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} @@ -2384,24 +3830,29 @@ packages: /asynckit/0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - /autoprefixer/10.4.12_postcss@8.4.17: - resolution: {integrity: sha512-WrCGV9/b97Pa+jtwf5UGaRjgQIg7OK3D06GnoYoZNcG1Xb8Gt3EfuKjlhh9i/VtT16g6PYjZ69jdJ2g8FxSC4Q==} + /autoprefixer/10.4.13_postcss@8.4.21: + resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: postcss: ^8.1.0 dependencies: - browserslist: 4.21.4 - caniuse-lite: 1.0.30001418 + browserslist: 4.21.5 + caniuse-lite: 1.0.30001456 fraction.js: 4.2.0 normalize-range: 0.1.2 picocolors: 1.0.0 - postcss: 8.4.17 + postcss: 8.4.21 postcss-value-parser: 4.2.0 dev: true - /axios/1.1.2: - resolution: {integrity: sha512-bznQyETwElsXl2RK7HLLwb5GPpOLlycxHCtrpDR/4RqqBzjARaOTo3jz4IgtntWUYee7Ne4S8UHd92VCuzPaWA==} + /available-typed-arrays/1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + + /axios/1.3.3: + resolution: {integrity: sha512-eYq77dYIFS77AQlhzEL937yUBSepBfPIe8FcgEDN35vMNZKMrs81pgnyrQpwfy4NF4b4XWX1Zgx7yX+25w8QJA==} dependencies: follow-redirects: 1.15.2 form-data: 4.0.0 @@ -2409,15 +3860,113 @@ packages: transitivePeerDependencies: - debug + /babel-plugin-conditional-invariant/2.0.2_@babel+core@7.20.12: + resolution: {integrity: sha512-/+Ep7cRKiHvV9Dder00MSDTZs5SQcltbIVDIC0wRgwZ5CSEmFBaWn5G19UTQerqHc/MirpqZXOF+7g9x/hTt3w==} + engines: {node: '>=14.15.0', npm: '>=6.14.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.12 + dev: true + + /babel-plugin-env-constants/2.0.2_@babel+core@7.20.12: + resolution: {integrity: sha512-9AwlhYuCaHQo45ga4C4NLPItVguOnNITbdDnZPTgWKnU88L1VDlHHXsX1a4zTwEYLYLBHEgmD6jbj5iDtaLNAw==} + engines: {node: '>=14.15.0', npm: '>=6.14.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.12 + dev: true + + /babel-plugin-jsx-dom-expressions/0.35.16_@babel+core@7.20.12: + resolution: {integrity: sha512-Z8vaeXRdtI4qyq3bmQiLjiZnbjn2Rr0mjpXMwN+QxHbWjtlAFOJSHlkcxbrwPz/DdcfSgkmZM0Atvt/zMLeLyA==} + peerDependencies: + '@babel/core': ^7.20.12 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-module-imports': 7.18.6 + '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.20.12 + '@babel/types': 7.20.7 + html-entities: 2.3.3 + validate-html-nesting: 1.2.1 + dev: true + /babel-plugin-macros/3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} dependencies: - '@babel/runtime': 7.19.0 - cosmiconfig: 7.0.1 + '@babel/runtime': 7.20.13 + cosmiconfig: 7.1.0 resolve: 1.22.1 dev: false + /babel-plugin-polyfill-corejs2/0.3.3_@babel+core@7.20.12: + resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.20.14 + '@babel/core': 7.20.12 + '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.20.12 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-corejs3/0.6.0_@babel+core@7.20.12: + resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.20.12 + core-js-compat: 3.28.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-regenerator/0.4.1_@babel+core@7.20.12: + resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.20.12 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-preset-moon/1.1.4_@babel+core@7.20.12: + resolution: {integrity: sha512-i+jeutSqqJQGXA93QkO3AEOk/xWKbvz8OXGidH9XGUDnS3cS44ptF6yPvFYpp3Bwg5jep8rD0KnzN9BnVpXM+g==} + engines: {node: '>=14.15.0', npm: '>=6.14.0'} + peerDependencies: + '@babel/core': '>=7.0.0' + dependencies: + '@babel/core': 7.20.12 + '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-proposal-decorators': 7.20.13_@babel+core@7.20.12 + '@babel/plugin-proposal-export-default-from': 7.18.10_@babel+core@7.20.12 + '@babel/plugin-proposal-export-namespace-from': 7.18.9_@babel+core@7.20.12 + '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.20.12 + '@babel/preset-env': 7.20.2_@babel+core@7.20.12 + '@babel/preset-react': 7.18.6_@babel+core@7.20.12 + '@babel/preset-typescript': 7.18.6_@babel+core@7.20.12 + babel-plugin-conditional-invariant: 2.0.2_@babel+core@7.20.12 + babel-plugin-env-constants: 2.0.2_@babel+core@7.20.12 + babel-preset-solid: 1.6.10_@babel+core@7.20.12 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-preset-solid/1.6.10_@babel+core@7.20.12: + resolution: {integrity: sha512-qBLjzeWmgY5jX11sJg/lriXABYdClfJrJJrIHaT6G5EuGhxhm6jn7XjqXjLBZHBgy5n/Z+iqJ5YfQj8KG2jKTA==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.12 + babel-plugin-jsx-dom-expressions: 0.35.16_@babel+core@7.20.12 + dev: true + /babel-runtime/6.26.0: resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==} dependencies: @@ -2484,26 +4033,15 @@ packages: fill-range: 7.0.1 dev: true - /browserslist/4.21.3: - resolution: {integrity: sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - dependencies: - caniuse-lite: 1.0.30001400 - electron-to-chromium: 1.4.213 - node-releases: 2.0.6 - update-browserslist-db: 1.0.5_browserslist@4.21.3 - dev: true - - /browserslist/4.21.4: - resolution: {integrity: sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==} + /browserslist/4.21.5: + resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001418 - electron-to-chromium: 1.4.276 - node-releases: 2.0.6 - update-browserslist-db: 1.0.10_browserslist@4.21.4 + caniuse-lite: 1.0.30001456 + electron-to-chromium: 1.4.302 + node-releases: 2.0.10 + update-browserslist-db: 1.0.10_browserslist@4.21.5 dev: true /buffer-equal-constant-time/1.0.1: @@ -2523,13 +4061,12 @@ packages: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: function-bind: 1.1.1 - get-intrinsic: 1.1.2 + get-intrinsic: 1.2.0 dev: true /callsites/3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - dev: false /camelcase-css/2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} @@ -2565,16 +4102,12 @@ packages: engines: {node: '>=10'} dev: true - /caniuse-lite/1.0.30001400: - resolution: {integrity: sha512-Mv659Hn65Z4LgZdJ7ge5JTVbE3rqbJaaXgW5LEI9/tOaXclfIZ8DW7D7FCWWWmWiiPS7AC48S8kf3DApSxQdgA==} - dev: true - - /caniuse-lite/1.0.30001418: - resolution: {integrity: sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==} + /caniuse-lite/1.0.30001456: + resolution: {integrity: sha512-XFHJY5dUgmpMV25UqaD4kVq2LsiaU5rS8fb0f17pCoXQiQslzmFgnfOxfvo1bTpTqf7dwG/N/05CnLCnOEKmzA==} dev: true - /chakra-react-select/4.2.5_7ibnqr7ipkokiucezzal4ajjcm: - resolution: {integrity: sha512-JZ8lExk0JFdAeGdj18ZnwA8oyJhd5030MwfGNw2ShtPkO/tsLvXtiOr4yAKY+m12H6f7PWCNWgJMX/LA4EQjLA==} + /chakra-react-select/4.4.3_guv77w3wtq2ixjrrapgt4j5nnm: + resolution: {integrity: sha512-anDgJyYUpIapTmUbgXB+Iw5hJ90hOPvgoUPUaYdO5q9zY2VBFhQ1L0gBMqWAQxiKUmuHpwQypf8sPoVtd0b3KA==} peerDependencies: '@chakra-ui/form-control': ^2.0.0 '@chakra-ui/icon': ^3.0.0 @@ -2586,15 +4119,24 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@emotion/react': 11.10.4_iapumuv4e6jcjznwuxpf4tt22e - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - react-select: 5.4.0_rj7ozvcq3uehdlnj3cbwzbi5ce + '@emotion/react': 11.10.6_@types+react@18.0.28 + react-dom: 18.2.0 + react-select: 5.7.0_bbiwgh4gz2syd5dgiw2t3uvqai transitivePeerDependencies: - - '@babel/core' - '@types/react' dev: false + /chalk/1.1.3: + resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} + engines: {node: '>=0.10.0'} + dependencies: + ansi-styles: 2.2.1 + escape-string-regexp: 1.0.5 + has-ansi: 2.0.0 + strip-ansi: 3.0.1 + supports-color: 2.0.0 + dev: true + /chalk/2.4.1: resolution: {integrity: sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==} engines: {node: '>=4'} @@ -2624,7 +4166,7 @@ packages: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} dependencies: - anymatch: 3.1.2 + anymatch: 3.1.3 braces: 3.0.2 glob-parent: 5.1.2 is-binary-path: 2.1.0 @@ -2635,6 +4177,17 @@ packages: fsevents: 2.3.2 dev: true + /class-variance-authority/0.2.4_typescript@4.9.5: + resolution: {integrity: sha512-JJKn9ESARiNEBBRdTSPB9/SwaPb+wi19DMX/r8BVSyp1dxHa3JyUxa2GQjEVag5rBi+O3pL64UZ0XhnQsMz+3w==} + peerDependencies: + typescript: '>= 4.5.5 < 5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + typescript: 4.9.5 + dev: false + /clean-stack/2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -2703,6 +4256,10 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /color2k/2.0.2: + resolution: {integrity: sha512-kJhwH5nAwb34tmyuqq/lgjEKzlFXn1U99NlnB6Ws4qVaERcRUYeYP1cBw6BJ4vxaWStAUEef4WMr7WjOCnBt8w==} + dev: false + /colorette/2.0.19: resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} dev: true @@ -2713,8 +4270,8 @@ packages: dependencies: delayed-stream: 1.0.0 - /commander/9.4.1: - resolution: {integrity: sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==} + /commander/9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} dev: true @@ -2726,6 +4283,11 @@ packages: uuidv4: 2.0.0 dev: true + /common-tags/1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + dev: true + /comparejs/1.0.0: resolution: {integrity: sha512-Ue/Zd9aOucHzHXwaCe4yeHR7jypp7TKrIBZ5yls35nPNiVXlW14npmNVKM1ZaLlQTKZ6/4ewA//gYKHHIwCpOw==} dev: true @@ -2752,28 +4314,28 @@ packages: - supports-color dev: true - /compute-scroll-into-view/1.0.14: - resolution: {integrity: sha512-mKDjINe3tc6hGelUMNDzuhorIUZ7kS7BwyY0r2wQd2HOH2tRuJykiC06iSEX8y1TuhNzvz4GcJnK16mM2J1NMQ==} + /compute-scroll-into-view/1.0.20: + resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} dev: false /concat-map/0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true - /concurrently/7.4.0: - resolution: {integrity: sha512-M6AfrueDt/GEna/Vg9BqQ+93yuvzkSKmoTixnwEJkH0LlcGrRC2eCmjeG1tLLHIYfpYJABokqSGyMcXjm96AFA==} + /concurrently/7.6.0: + resolution: {integrity: sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==} engines: {node: ^12.20.0 || ^14.13.0 || >=16.0.0} hasBin: true dependencies: chalk: 4.1.2 date-fns: 2.29.3 lodash: 4.17.21 - rxjs: 7.5.7 - shell-quote: 1.7.3 + rxjs: 7.8.0 + shell-quote: 1.8.0 spawn-command: 0.0.2-1 supports-color: 8.1.1 tree-kill: 1.2.2 - yargs: 17.6.0 + yargs: 17.7.0 dev: true /content-disposition/0.5.2: @@ -2786,10 +4348,8 @@ packages: engines: {node: '>= 0.6'} dev: true - /convert-source-map/1.8.0: - resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==} - dependencies: - safe-buffer: 5.1.2 + /convert-source-map/1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} /cookie-signature/1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} @@ -2800,28 +4360,33 @@ packages: engines: {node: '>= 0.6'} dev: true - /copy-anything/3.0.2: - resolution: {integrity: sha512-CzATjGXzUQ0EvuvgOCI6A4BGOo2bcVx8B+eC2nF862iv9fopnPQwlrbACakNCHRIJbCSBj+J/9JeDf60k64MkA==} + /copy-anything/3.0.3: + resolution: {integrity: sha512-fpW2W/BqEzqPp29QS+MwwfisHCQZtiduTe/m8idFo0xbti9fIZ2WVhAsCv4ggFVH3AgCkVdpoOCtQC6gBrdhjw==} engines: {node: '>=12.13'} dependencies: - is-what: 4.1.7 + is-what: 4.1.8 dev: false - /copy-to-clipboard/3.3.1: - resolution: {integrity: sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==} + /copy-to-clipboard/3.3.3: + resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} dependencies: toggle-selection: 1.0.6 dev: false + /core-js-compat/3.28.0: + resolution: {integrity: sha512-myzPgE7QodMg4nnd3K1TDoES/nADRStM8Gpz0D6nhkwbmwEnE0ZGJgoWsvQ722FR8D7xS0n0LV556RcEicjTyg==} + dependencies: + browserslist: 4.21.5 + dev: true + /core-js/2.6.12: resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. requiresBuild: true dev: true - /core-js/3.22.8: - resolution: {integrity: sha512-UoGQ/cfzGYIuiq6Z7vWL1HfkE9U9IZ4Ub+0XSiJTCzvbZzgPA69oDF2f+lgJ6dFFLEdjW5O6svvoKzXX23xFkA==} - deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + /core-js/3.28.0: + resolution: {integrity: sha512-GiZn9D4Z/rSYvTeg1ljAIsEqFm0LaN9gVtwDCrKL80zHtS31p9BAjmTxVqTQDMpwlMolJZOFntUG2uwyj7DAqw==} requiresBuild: true dev: false @@ -2836,8 +4401,8 @@ packages: vary: 1.1.2 dev: true - /cosmiconfig/7.0.1: - resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==} + /cosmiconfig/7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} dependencies: '@types/parse-json': 4.0.0 @@ -2863,7 +4428,7 @@ packages: hasBin: true dependencies: cpy: 9.0.1 - meow: 10.1.2 + meow: 10.1.5 dev: true /cpy/9.0.1: @@ -2872,7 +4437,7 @@ packages: dependencies: arrify: 3.0.0 cp-file: 9.1.0 - globby: 13.1.2 + globby: 13.1.3 junk: 4.0.0 micromatch: 4.0.5 nested-error-stacks: 2.1.1 @@ -2909,17 +4474,13 @@ packages: hasBin: true dev: true - /csstype/3.1.0: - resolution: {integrity: sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==} - dev: false - /csstype/3.1.1: resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} /d/1.0.1: resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==} dependencies: - es5-ext: 0.10.61 + es5-ext: 0.10.62 type: 1.2.0 dev: false @@ -2936,6 +4497,10 @@ packages: engines: {node: '>=0.11'} dev: true + /dayjs/1.11.7: + resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==} + dev: false + /debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -2959,8 +4524,8 @@ packages: ms: 2.1.2 dev: true - /decamelize-keys/1.1.0: - resolution: {integrity: sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg==} + /decamelize-keys/1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} dependencies: decamelize: 1.2.0 @@ -2977,16 +4542,20 @@ packages: engines: {node: '>=10'} dev: true - /define-properties/1.1.4: - resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==} + /deep-is/0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /define-properties/1.2.0: + resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} engines: {node: '>= 0.4'} dependencies: has-property-descriptors: 1.0.0 object-keys: 1.1.1 dev: true - /defined/1.0.0: - resolution: {integrity: sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==} + /defined/1.0.1: + resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==} dev: true /delayed-stream/1.0.0: @@ -3002,6 +4571,11 @@ packages: resolution: {integrity: sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==} dev: true + /detect-libc/2.0.1: + resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==} + engines: {node: '>=8'} + dev: true + /detect-node-es/1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} dev: false @@ -3012,8 +4586,8 @@ packages: hasBin: true dependencies: acorn-node: 1.8.2 - defined: 1.0.0 - minimist: 1.2.6 + defined: 1.0.1 + minimist: 1.2.8 dev: true /didyoumean/1.2.2: @@ -3038,10 +4612,24 @@ packages: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} dev: true + /doctrine/2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctrine/3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + /dom-helpers/5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: - '@babel/runtime': 7.19.0 + '@babel/runtime': 7.20.13 csstype: 3.1.1 dev: false @@ -3058,19 +4646,15 @@ packages: /ecdsa-sig-formatter/1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} dependencies: - safe-buffer: 5.1.2 + safe-buffer: 5.2.1 dev: true /ee-first/1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: true - /electron-to-chromium/1.4.213: - resolution: {integrity: sha512-+3DbGHGOCHTVB/Ms63bGqbyC1b8y7Fk86+7ltssB8NQrZtSCvZG6eooSl9U2Q0yw++fL2DpHKOdTU0NVEkFObg==} - dev: true - - /electron-to-chromium/1.4.276: - resolution: {integrity: sha512-EpuHPqu8YhonqLBXHoU6hDJCD98FCe6KDoet3/gY1qsQ6usjJoHqBH2YIVs8FXaAtHwVL8Uqa/fsYao/vq9VWQ==} + /electron-to-chromium/1.4.302: + resolution: {integrity: sha512-Uk7C+7aPBryUR1Fwvk9VmipBcN9fVsqBO57jV2ZjTm+IZ6BMNqu7EDVEg2HxCNufk6QcWlFsBkhQyQroB2VWKw==} dev: true /emoji-regex/8.0.0: @@ -3096,10 +4680,10 @@ packages: resolution: {integrity: sha512-c06pNSdBxcXv3dZSbXAVLE1/pmleRhOT6mXNZo6INKmvuKpYB65MwU/lO7830czCtjIiK9i+KR+3S+p0wtljrw==} dependencies: '@types/localforage': 0.0.34 - '@xmldom/xmldom': 0.7.5 - core-js: 3.22.8 + '@xmldom/xmldom': 0.7.9 + core-js: 3.28.0 event-emitter: 0.3.5 - jszip: 3.10.0 + jszip: 3.10.1 localforage: 1.10.0 lodash: 4.17.21 marks-pane: 1.0.9 @@ -3111,50 +4695,75 @@ packages: dependencies: is-arrayish: 0.2.1 - /es-abstract/1.20.1: - resolution: {integrity: sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==} + /es-abstract/1.21.1: + resolution: {integrity: sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==} engines: {node: '>= 0.4'} dependencies: + available-typed-arrays: 1.0.5 call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 es-to-primitive: 1.2.1 function-bind: 1.1.1 function.prototype.name: 1.1.5 - get-intrinsic: 1.1.2 + get-intrinsic: 1.2.0 get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 has: 1.0.3 has-property-descriptors: 1.0.0 + has-proto: 1.0.1 has-symbols: 1.0.3 - internal-slot: 1.0.3 - is-callable: 1.2.4 + internal-slot: 1.0.5 + is-array-buffer: 3.0.1 + is-callable: 1.2.7 is-negative-zero: 2.0.2 is-regex: 1.1.4 is-shared-array-buffer: 1.0.2 is-string: 1.0.7 + is-typed-array: 1.1.10 is-weakref: 1.0.2 - object-inspect: 1.12.2 + object-inspect: 1.12.3 object-keys: 1.1.1 - object.assign: 4.1.2 + object.assign: 4.1.4 regexp.prototype.flags: 1.4.3 - string.prototype.trimend: 1.0.5 - string.prototype.trimstart: 1.0.5 + 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 dev: true /es-array-method-boxes-properly/1.0.0: resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} dev: true + /es-set-tostringtag/2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.0 + has: 1.0.3 + has-tostringtag: 1.0.0 + dev: true + + /es-shim-unscopables/1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.3 + dev: true + /es-to-primitive/1.2.1: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} dependencies: - is-callable: 1.2.4 + is-callable: 1.2.7 is-date-object: 1.0.5 is-symbol: 1.0.4 dev: true - /es5-ext/0.10.61: - resolution: {integrity: sha512-yFhIqQAzu2Ca2I4SE2Au3rxVfmohU9Y7wqGR+s7+H7krk26NXhIRAZDgqd6xqjCEFUomDEA3/Bo/7fKmIkW1kA==} + /es5-ext/0.10.62: + resolution: {integrity: sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==} engines: {node: '>=0.10'} requiresBuild: true dependencies: @@ -3167,7 +4776,7 @@ packages: resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} dependencies: d: 1.0.1 - es5-ext: 0.10.61 + es5-ext: 0.10.62 es6-symbol: 3.1.3 dev: false @@ -3175,11 +4784,11 @@ packages: resolution: {integrity: sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==} dependencies: d: 1.0.1 - ext: 1.6.0 + ext: 1.7.0 dev: false - /esbuild-android-64/0.15.10: - resolution: {integrity: sha512-UI7krF8OYO1N7JYTgLT9ML5j4+45ra3amLZKx7LO3lmLt1Ibn8t3aZbX5Pu4BjWiqDuJ3m/hsvhPhK/5Y/YpnA==} + /esbuild-android-64/0.15.18: + resolution: {integrity: sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==} engines: {node: '>=12'} cpu: [x64] os: [android] @@ -3187,8 +4796,8 @@ packages: dev: true optional: true - /esbuild-android-arm64/0.15.10: - resolution: {integrity: sha512-EOt55D6xBk5O05AK8brXUbZmoFj4chM8u3riGflLa6ziEoVvNjRdD7Cnp82NHQGfSHgYR06XsPI8/sMuA/cUwg==} + /esbuild-android-arm64/0.15.18: + resolution: {integrity: sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==} engines: {node: '>=12'} cpu: [arm64] os: [android] @@ -3196,8 +4805,8 @@ packages: dev: true optional: true - /esbuild-darwin-64/0.15.10: - resolution: {integrity: sha512-hbDJugTicqIm+WKZgp208d7FcXcaK8j2c0l+fqSJ3d2AzQAfjEYDRM3Z2oMeqSJ9uFxyj/muSACLdix7oTstRA==} + /esbuild-darwin-64/0.15.18: + resolution: {integrity: sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==} engines: {node: '>=12'} cpu: [x64] os: [darwin] @@ -3205,8 +4814,8 @@ packages: dev: true optional: true - /esbuild-darwin-arm64/0.15.10: - resolution: {integrity: sha512-M1t5+Kj4IgSbYmunf2BB6EKLkWUq+XlqaFRiGOk8bmBapu9bCDrxjf4kUnWn59Dka3I27EiuHBKd1rSO4osLFQ==} + /esbuild-darwin-arm64/0.15.18: + resolution: {integrity: sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] @@ -3214,8 +4823,8 @@ packages: dev: true optional: true - /esbuild-freebsd-64/0.15.10: - resolution: {integrity: sha512-KMBFMa7C8oc97nqDdoZwtDBX7gfpolkk6Bcmj6YFMrtCMVgoU/x2DI1p74DmYl7CSS6Ppa3xgemrLrr5IjIn0w==} + /esbuild-freebsd-64/0.15.18: + resolution: {integrity: sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] @@ -3223,8 +4832,8 @@ packages: dev: true optional: true - /esbuild-freebsd-arm64/0.15.10: - resolution: {integrity: sha512-m2KNbuCX13yQqLlbSojFMHpewbn8wW5uDS6DxRpmaZKzyq8Dbsku6hHvh2U+BcLwWY4mpgXzFUoENEf7IcioGg==} + /esbuild-freebsd-arm64/0.15.18: + resolution: {integrity: sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] @@ -3232,8 +4841,8 @@ packages: dev: true optional: true - /esbuild-linux-32/0.15.10: - resolution: {integrity: sha512-guXrwSYFAvNkuQ39FNeV4sNkNms1bLlA5vF1H0cazZBOLdLFIny6BhT+TUbK/hdByMQhtWQ5jI9VAmPKbVPu1w==} + /esbuild-linux-32/0.15.18: + resolution: {integrity: sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] @@ -3241,8 +4850,8 @@ packages: dev: true optional: true - /esbuild-linux-64/0.15.10: - resolution: {integrity: sha512-jd8XfaSJeucMpD63YNMO1JCrdJhckHWcMv6O233bL4l6ogQKQOxBYSRP/XLWP+6kVTu0obXovuckJDcA0DKtQA==} + /esbuild-linux-64/0.15.18: + resolution: {integrity: sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==} engines: {node: '>=12'} cpu: [x64] os: [linux] @@ -3250,8 +4859,8 @@ packages: dev: true optional: true - /esbuild-linux-arm/0.15.10: - resolution: {integrity: sha512-6N8vThLL/Lysy9y4Ex8XoLQAlbZKUyExCWyayGi2KgTBelKpPgj6RZnUaKri0dHNPGgReJriKVU6+KDGQwn10A==} + /esbuild-linux-arm/0.15.18: + resolution: {integrity: sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==} engines: {node: '>=12'} cpu: [arm] os: [linux] @@ -3259,8 +4868,8 @@ packages: dev: true optional: true - /esbuild-linux-arm64/0.15.10: - resolution: {integrity: sha512-GByBi4fgkvZFTHFDYNftu1DQ1GzR23jws0oWyCfhnI7eMOe+wgwWrc78dbNk709Ivdr/evefm2PJiUBMiusS1A==} + /esbuild-linux-arm64/0.15.18: + resolution: {integrity: sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==} engines: {node: '>=12'} cpu: [arm64] os: [linux] @@ -3268,8 +4877,8 @@ packages: dev: true optional: true - /esbuild-linux-mips64le/0.15.10: - resolution: {integrity: sha512-BxP+LbaGVGIdQNJUNF7qpYjEGWb0YyHVSKqYKrn+pTwH/SiHUxFyJYSP3pqkku61olQiSBnSmWZ+YUpj78Tw7Q==} + /esbuild-linux-mips64le/0.15.18: + resolution: {integrity: sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] @@ -3277,8 +4886,8 @@ packages: dev: true optional: true - /esbuild-linux-ppc64le/0.15.10: - resolution: {integrity: sha512-LoSQCd6498PmninNgqd/BR7z3Bsk/mabImBWuQ4wQgmQEeanzWd5BQU2aNi9mBURCLgyheuZS6Xhrw5luw3OkQ==} + /esbuild-linux-ppc64le/0.15.18: + resolution: {integrity: sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] @@ -3286,129 +4895,341 @@ packages: dev: true optional: true - /esbuild-linux-riscv64/0.15.10: - resolution: {integrity: sha512-Lrl9Cr2YROvPV4wmZ1/g48httE8z/5SCiXIyebiB5N8VT7pX3t6meI7TQVHw/wQpqP/AF4SksDuFImPTM7Z32Q==} + /esbuild-linux-riscv64/0.15.18: + resolution: {integrity: sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] requiresBuild: true dev: true - optional: true + optional: true + + /esbuild-linux-s390x/0.15.18: + resolution: {integrity: sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-netbsd-64/0.15.18: + resolution: {integrity: sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-openbsd-64/0.15.18: + resolution: {integrity: sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-sunos-64/0.15.18: + resolution: {integrity: sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-32/0.15.18: + resolution: {integrity: sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-64/0.15.18: + resolution: {integrity: sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-arm64/0.15.18: + resolution: {integrity: sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild/0.15.18: + resolution: {integrity: sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.15.18 + '@esbuild/linux-loong64': 0.15.18 + esbuild-android-64: 0.15.18 + esbuild-android-arm64: 0.15.18 + esbuild-darwin-64: 0.15.18 + esbuild-darwin-arm64: 0.15.18 + esbuild-freebsd-64: 0.15.18 + esbuild-freebsd-arm64: 0.15.18 + esbuild-linux-32: 0.15.18 + esbuild-linux-64: 0.15.18 + esbuild-linux-arm: 0.15.18 + esbuild-linux-arm64: 0.15.18 + esbuild-linux-mips64le: 0.15.18 + esbuild-linux-ppc64le: 0.15.18 + esbuild-linux-riscv64: 0.15.18 + esbuild-linux-s390x: 0.15.18 + esbuild-netbsd-64: 0.15.18 + esbuild-openbsd-64: 0.15.18 + esbuild-sunos-64: 0.15.18 + esbuild-windows-32: 0.15.18 + esbuild-windows-64: 0.15.18 + esbuild-windows-arm64: 0.15.18 + dev: true + + /escalade/3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-html/1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: true + + /escape-string-regexp/1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + /escape-string-regexp/4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + /escape-string-regexp/5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + dev: true + + /eslint-config-prettier/8.6.0_eslint@8.34.0: + resolution: {integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.34.0 + dev: true + + /eslint-plugin-prettier/4.2.1_u5wnrdwibbfomslmnramz52buy: + resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + eslint: '>=7.28.0' + eslint-config-prettier: '*' + prettier: '>=2.0.0' + peerDependenciesMeta: + eslint-config-prettier: + optional: true + dependencies: + eslint: 8.34.0 + eslint-config-prettier: 8.6.0_eslint@8.34.0 + prettier: 2.8.4 + prettier-linter-helpers: 1.0.0 + dev: true + + /eslint-plugin-react-hooks/4.6.0_eslint@8.34.0: + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.34.0 + dev: true + + /eslint-plugin-react/7.32.2_eslint@8.34.0: + resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + array-includes: 3.1.6 + array.prototype.flatmap: 1.3.1 + array.prototype.tosorted: 1.1.1 + doctrine: 2.1.0 + eslint: 8.34.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.3 + 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 + dev: true + + /eslint-plugin-simple-import-sort/8.0.0_eslint@8.34.0: + resolution: {integrity: sha512-bXgJQ+lqhtQBCuWY/FUWdB27j4+lqcvXv5rUARkzbeWLwea+S5eBZEQrhnO+WgX3ZoJHVj0cn943iyXwByHHQw==} + peerDependencies: + eslint: '>=5.0.0' + dependencies: + eslint: 8.34.0 + dev: true + + /eslint-plugin-sort-keys-fix/1.1.2: + resolution: {integrity: sha512-DNPHFGCA0/hZIsfODbeLZqaGY/+q3vgtshF85r+YWDNCQ2apd9PNs/zL6ttKm0nD1IFwvxyg3YOTI7FHl4unrw==} + engines: {node: '>=0.10.0'} + dependencies: + espree: 6.2.1 + esutils: 2.0.3 + natural-compare: 1.4.0 + requireindex: 1.2.0 + dev: true + + /eslint-scope/5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true - /esbuild-linux-s390x/0.15.10: - resolution: {integrity: sha512-ReP+6q3eLVVP2lpRrvl5EodKX7EZ1bS1/z5j6hsluAlZP5aHhk6ghT6Cq3IANvvDdscMMCB4QEbI+AjtvoOFpA==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true + /eslint-scope/7.1.1: + resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 dev: true - optional: true - /esbuild-netbsd-64/0.15.10: - resolution: {integrity: sha512-iGDYtJCMCqldMskQ4eIV+QSS/CuT7xyy9i2/FjpKvxAuCzrESZXiA1L64YNj6/afuzfBe9i8m/uDkFHy257hTw==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true + /eslint-utils/3.0.0_eslint@8.34.0: + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.34.0 + eslint-visitor-keys: 2.1.0 dev: true - optional: true - /esbuild-openbsd-64/0.15.10: - resolution: {integrity: sha512-ftMMIwHWrnrYnvuJQRJs/Smlcb28F9ICGde/P3FUTCgDDM0N7WA0o9uOR38f5Xe2/OhNCgkjNeb7QeaE3cyWkQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true + /eslint-visitor-keys/1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} dev: true - optional: true - /esbuild-sunos-64/0.15.10: - resolution: {integrity: sha512-mf7hBL9Uo2gcy2r3rUFMjVpTaGpFJJE5QTDDqUFf1632FxteYANffDZmKbqX0PfeQ2XjUDE604IcE7OJeoHiyg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true + /eslint-visitor-keys/2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} dev: true - optional: true - /esbuild-windows-32/0.15.10: - resolution: {integrity: sha512-ttFVo+Cg8b5+qHmZHbEc8Vl17kCleHhLzgT8X04y8zudEApo0PxPg9Mz8Z2cKH1bCYlve1XL8LkyXGFjtUYeGg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true + /eslint-visitor-keys/3.3.0: + resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - optional: true - /esbuild-windows-64/0.15.10: - resolution: {integrity: sha512-2H0gdsyHi5x+8lbng3hLbxDWR7mKHWh5BXZGKVG830KUmXOOWFE2YKJ4tHRkejRduOGDrBvHBriYsGtmTv3ntA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true + /eslint/8.34.0: + resolution: {integrity: sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint/eslintrc': 1.4.1 + '@humanwhocodes/config-array': 0.11.8 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.1.1 + eslint-utils: 3.0.0_eslint@8.34.0 + eslint-visitor-keys: 3.3.0 + espree: 9.4.1 + esquery: 1.4.2 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.20.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 + import-fresh: 3.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-sdsl: 4.3.0 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.1 + regexpp: 3.2.0 + strip-ansi: 6.0.1 + strip-json-comments: 3.1.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color dev: true - optional: true - /esbuild-windows-arm64/0.15.10: - resolution: {integrity: sha512-S+th4F+F8VLsHLR0zrUcG+Et4hx0RKgK1eyHc08kztmLOES8BWwMiaGdoW9hiXuzznXQ0I/Fg904MNbr11Nktw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true + /espree/6.2.1: + resolution: {integrity: sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==} + engines: {node: '>=6.0.0'} + dependencies: + acorn: 7.4.1 + acorn-jsx: 5.3.2_acorn@7.4.1 + eslint-visitor-keys: 1.3.0 dev: true - optional: true - /esbuild/0.15.10: - resolution: {integrity: sha512-N7wBhfJ/E5fzn/SpNgX+oW2RLRjwaL8Y0ezqNqhjD6w0H2p0rDuEz2FKZqpqLnO8DCaWumKe8dsC/ljvVSSxng==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/android-arm': 0.15.10 - '@esbuild/linux-loong64': 0.15.10 - esbuild-android-64: 0.15.10 - esbuild-android-arm64: 0.15.10 - esbuild-darwin-64: 0.15.10 - esbuild-darwin-arm64: 0.15.10 - esbuild-freebsd-64: 0.15.10 - esbuild-freebsd-arm64: 0.15.10 - esbuild-linux-32: 0.15.10 - esbuild-linux-64: 0.15.10 - esbuild-linux-arm: 0.15.10 - esbuild-linux-arm64: 0.15.10 - esbuild-linux-mips64le: 0.15.10 - esbuild-linux-ppc64le: 0.15.10 - esbuild-linux-riscv64: 0.15.10 - esbuild-linux-s390x: 0.15.10 - esbuild-netbsd-64: 0.15.10 - esbuild-openbsd-64: 0.15.10 - esbuild-sunos-64: 0.15.10 - esbuild-windows-32: 0.15.10 - esbuild-windows-64: 0.15.10 - esbuild-windows-arm64: 0.15.10 + /espree/9.4.1: + resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.8.2 + acorn-jsx: 5.3.2_acorn@8.8.2 + eslint-visitor-keys: 3.3.0 dev: true - /escalade/3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} + /esquery/1.4.2: + resolution: {integrity: sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 dev: true - /escape-html/1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + /esrecurse/4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 dev: true - /escape-string-regexp/1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} + /estraverse/4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true - /escape-string-regexp/4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - dev: false + /estraverse/5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true - /escape-string-regexp/5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} + /esutils/2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} dev: true /etag/1.8.1: @@ -3420,7 +5241,7 @@ packages: resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} dependencies: d: 1.0.1 - es5-ext: 0.10.61 + es5-ext: 0.10.62 dev: false /eventemitter2/5.0.1: @@ -3495,18 +5316,26 @@ packages: - supports-color dev: true - /ext/1.6.0: - resolution: {integrity: sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==} + /ext/1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} dependencies: - type: 2.6.0 + type: 2.7.2 dev: false /fast-deep-equal/2.0.1: resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} dev: true - /fast-glob/3.2.11: - resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==} + /fast-deep-equal/3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-diff/1.2.0: + resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} + dev: true + + /fast-glob/3.2.12: + resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} engines: {node: '>=8.6.0'} dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3520,12 +5349,23 @@ packages: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} dev: true - /fastq/1.13.0: - resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} + /fast-levenshtein/2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq/1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: reusify: 1.0.4 dev: true + /file-entry-cache/6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.0.4 + dev: true + /fill-range/7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -3587,11 +5427,23 @@ packages: varname: 2.0.3 dev: true - /focus-lock/0.11.3: - resolution: {integrity: sha512-4n0pYcPTa/uI7Q66BZna61nRT7lDhnuJ9PJr6wiDjx4uStg491ks41y7uOG+s0umaaa+hulNKSldU9aTg9/yVg==} + /flat-cache/3.0.4: + resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.7 + rimraf: 3.0.2 + dev: true + + /flatted/3.2.7: + resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + dev: true + + /focus-lock/0.11.6: + resolution: {integrity: sha512-KSuV3ur4gf2KqMNoZx3nXNVhqCkn42GuTYCX4tXPEwf0MjpFQmNMiN6m7dXaUXgIoivL6/65agoUMg4RLS0Vbg==} engines: {node: '>=10'} dependencies: - tslib: 2.4.0 + tslib: 2.5.0 dev: false /follow-redirects/1.15.2: @@ -3603,6 +5455,12 @@ packages: debug: optional: true + /for-each/0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + /form-data/4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -3624,30 +5482,20 @@ packages: resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} dev: true - /framer-motion/7.5.3_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-VvANga9Z7bYtKMAsM/je81FwJDHfThOYywN04xVQ4OGdMVY09Bowx/q7nZd6XtytLuv6byc6GT1mYwag+SQ/nw==} + /framer-motion/7.10.3_react-dom@18.2.0: + resolution: {integrity: sha512-k2ccYeZNSpPg//HTaqrU+4pRq9f9ZpaaN7rr0+Rx5zA4wZLbk547wtDzge2db1sB+1mnJ6r59P4xb+aEIi/W+w==} peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@motionone/dom': 10.13.1 - framesync: 6.1.2 + '@motionone/dom': 10.15.5 hey-listen: 1.0.8 - popmotion: 11.0.5 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - style-value-types: 5.1.2 + react-dom: 18.2.0 tslib: 2.4.0 optionalDependencies: '@emotion/is-prop-valid': 0.8.8 dev: false - /framesync/5.3.0: - resolution: {integrity: sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==} - dependencies: - tslib: 2.4.0 - dev: false - /framesync/6.1.2: resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==} dependencies: @@ -3679,8 +5527,8 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.1 + define-properties: 1.2.0 + es-abstract: 1.21.1 functions-have-names: 1.2.3 dev: true @@ -3698,8 +5546,8 @@ packages: engines: {node: 6.* || 8.* || >= 10.*} dev: true - /get-intrinsic/1.1.2: - resolution: {integrity: sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==} + /get-intrinsic/1.2.0: + resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} dependencies: function-bind: 1.1.1 has: 1.0.3 @@ -3732,7 +5580,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - get-intrinsic: 1.1.2 + get-intrinsic: 1.2.0 dev: true /glob-parent/5.1.2: @@ -3775,13 +5623,39 @@ packages: engines: {node: '>=4'} dev: true - /globby/13.1.2: - resolution: {integrity: sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==} + /globals/13.20.0: + resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis/1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.0 + dev: true + + /globby/11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.2.12 + ignore: 5.2.4 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /globby/13.1.3: + resolution: {integrity: sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: dir-glob: 3.0.1 - fast-glob: 3.2.11 - ignore: 5.2.0 + fast-glob: 3.2.12 + ignore: 5.2.4 merge2: 1.4.1 slash: 4.0.0 dev: true @@ -3798,21 +5672,38 @@ packages: slash: 1.0.0 dev: true - /goober/2.1.10: - resolution: {integrity: sha512-7PpuQMH10jaTWm33sQgBQvz45pHR8N4l3Cu3WMGEWmHShAcTuuP7I+5/DwKo39fwti5A80WAjvqgz6SSlgWmGA==} + /goober/2.1.12: + resolution: {integrity: sha512-yXHAvO08FU1JgTXX6Zn6sYCUFfB/OJSX8HHjDSgerZHZmFKAb08cykp5LBw5QnmyMcZyPRMqkdyHUSSzge788Q==} peerDependencies: csstype: ^3.0.10 dev: false + /gopd/1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.0 + dev: true + /graceful-fs/4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} dev: true + /grapheme-splitter/1.0.4: + resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + dev: true + /hard-rejection/2.1.0: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} engines: {node: '>=6'} dev: true + /has-ansi/2.0.0: + resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} + engines: {node: '>=0.10.0'} + dependencies: + ansi-regex: 2.1.1 + dev: true + /has-bigints/1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} dev: true @@ -3829,7 +5720,12 @@ packages: /has-property-descriptors/1.0.0: resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} dependencies: - get-intrinsic: 1.1.2 + get-intrinsic: 1.2.0 + dev: true + + /has-proto/1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} dev: true /has-symbols/1.0.3: @@ -3882,6 +5778,10 @@ packages: resolution: {integrity: sha512-2zuLt85Ta+gIyvs4N88pCYskNrxf1TFv3LR9t5mdAZIX8BcgQQ48F2opUptvHa6m8zsy5v/a0i9mWzTrlNWU0Q==} dev: false + /html-entities/2.3.3: + resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} + dev: true + /html-parse-stringify/3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} dependencies: @@ -3903,8 +5803,8 @@ packages: engines: {node: '>=12.20.0'} dev: true - /husky/8.0.1: - resolution: {integrity: sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==} + /husky/8.0.3: + resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} engines: {node: '>=14'} hasBin: true dev: true @@ -3912,7 +5812,7 @@ packages: /i18next/21.10.0: resolution: {integrity: sha512-YeuIBmFsGjUfO3qBmMOc0rQaun4mIpGKET5WDwvu8lU7gvwpcariZLNtL0Fzj+zazcHUrlXHiptcFhBMFaxzfg==} dependencies: - '@babel/runtime': 7.19.0 + '@babel/runtime': 7.20.13 dev: false /iconv-lite/0.4.23: @@ -3926,8 +5826,8 @@ packages: resolution: {integrity: sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==} dev: true - /ignore/5.2.0: - resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} + /ignore/5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} dev: true @@ -3935,8 +5835,8 @@ packages: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} dev: false - /immer/9.0.15: - resolution: {integrity: sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==} + /immer/9.0.19: + resolution: {integrity: sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==} dev: false /import-fresh/3.3.0: @@ -3945,7 +5845,11 @@ packages: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - dev: false + + /imurmurhash/0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true /indent-string/4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} @@ -3971,11 +5875,11 @@ packages: /inherits/2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - /internal-slot/1.0.3: - resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} + /internal-slot/1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.1.2 + get-intrinsic: 1.2.0 has: 1.0.3 side-channel: 1.0.4 dev: true @@ -3991,6 +5895,14 @@ packages: engines: {node: '>= 0.10'} dev: true + /is-array-buffer/3.0.1: + resolution: {integrity: sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + is-typed-array: 1.1.10 + dev: true + /is-arrayish/0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -4015,21 +5927,15 @@ packages: has-tostringtag: 1.0.0 dev: true - /is-callable/1.2.4: - resolution: {integrity: sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==} + /is-callable/1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} dev: true - /is-core-module/2.10.0: - resolution: {integrity: sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==} - dependencies: - has: 1.0.3 - - /is-core-module/2.9.0: - resolution: {integrity: sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==} + /is-core-module/2.11.0: + resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} dependencies: has: 1.0.3 - dev: true /is-date-object/1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} @@ -4082,6 +5988,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-path-inside/3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + /is-path-inside/4.0.0: resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} engines: {node: '>=12'} @@ -4135,14 +6046,25 @@ packages: has-symbols: 1.0.3 dev: true + /is-typed-array/1.1.10: + resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} + engines: {node: '>= 0.4'} + 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 + dev: true + /is-weakref/1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: call-bind: 1.0.2 dev: true - /is-what/4.1.7: - resolution: {integrity: sha512-DBVOQNiPKnGMxRMLIYSwERAS5MVY1B7xYiGnpgctsOFvVDz9f9PFXXxMcTOHuoqYp4NK9qFYQaIC1NRRxLMpBQ==} + /is-what/4.1.8: + resolution: {integrity: sha512-yq8gMao5upkPoGEU9LsB2P+K3Kt8Q3fQFCGyNCWOAnJAMzEXVV9drYb0TXr42TTliLLhKIBvulgAXgtLLnwzGA==} engines: {node: '>=12.13'} dev: false @@ -4158,9 +6080,25 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /js-sdsl/4.3.0: + resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==} + dev: true + /js-tokens/4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + /js-yaml/4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsesc/0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + dev: true + /jsesc/2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -4180,8 +6118,12 @@ packages: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true - /json5/2.2.1: - resolution: {integrity: sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==} + /json-stable-stringify-without-jsonify/1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5/2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true dev: true @@ -4202,8 +6144,16 @@ packages: semver: 5.7.1 dev: true - /jszip/3.10.0: - resolution: {integrity: sha512-LDfVtOLtOxb9RXkYOwPyNBTQDL4eUbqahtoY6x07GiDJHwSYvn8sHHIw8wINImV3MqbMNve2gSuM1DDqEKk09Q==} + /jsx-ast-utils/3.3.3: + resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==} + engines: {node: '>=4.0'} + dependencies: + array-includes: 3.1.6 + object.assign: 4.1.4 + dev: true + + /jszip/3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} dependencies: lie: 3.3.0 pako: 1.0.11 @@ -4221,14 +6171,14 @@ packages: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.1.2 + safe-buffer: 5.2.1 dev: true /jws/3.2.2: resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} dependencies: jwa: 1.4.1 - safe-buffer: 5.1.2 + safe-buffer: 5.2.1 dev: true /kind-of/6.0.3: @@ -4236,6 +6186,14 @@ packages: engines: {node: '>=0.10.0'} dev: true + /levn/0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + /lie/3.1.1: resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} dependencies: @@ -4248,11 +6206,6 @@ packages: immediate: 3.0.6 dev: false - /lilconfig/2.0.5: - resolution: {integrity: sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==} - engines: {node: '>=10'} - dev: true - /lilconfig/2.0.6: resolution: {integrity: sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==} engines: {node: '>=10'} @@ -4268,32 +6221,32 @@ packages: /lines-and-columns/1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - /lint-staged/13.0.3: - resolution: {integrity: sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug==} + /lint-staged/13.1.2: + resolution: {integrity: sha512-K9b4FPbWkpnupvK3WXZLbgu9pchUJ6N7TtVZjbaPsoizkqFUDkUReUL25xdrCljJs7uLUF3tZ7nVPeo/6lp+6w==} engines: {node: ^14.13.1 || >=16.0.0} hasBin: true dependencies: cli-truncate: 3.1.0 colorette: 2.0.19 - commander: 9.4.1 + commander: 9.5.0 debug: 4.3.4 execa: 6.1.0 - lilconfig: 2.0.5 - listr2: 4.0.5 + lilconfig: 2.0.6 + listr2: 5.0.7 micromatch: 4.0.5 normalize-path: 3.0.0 - object-inspect: 1.12.2 + object-inspect: 1.12.3 pidtree: 0.6.0 string-argv: 0.3.1 - yaml: 2.1.3 + yaml: 2.2.1 transitivePeerDependencies: - enquirer - supports-color dev: true - /listr2/4.0.5: - resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} - engines: {node: '>=12'} + /listr2/5.0.7: + resolution: {integrity: sha512-MD+qXHPmtivrHIDRwPYdfNkrzqDiuaKU/rfBcec3WMyMF3xylQj3jMq344OtvQxz7zaCFViRAeqlr2AFhPvXHw==} + engines: {node: ^14.13.1 || >=16.0.0} peerDependencies: enquirer: '>= 2.3.0 < 3' peerDependenciesMeta: @@ -4305,7 +6258,7 @@ packages: log-update: 4.0.0 p-map: 4.0.0 rfdc: 1.3.0 - rxjs: 7.5.7 + rxjs: 7.8.0 through: 2.3.8 wrap-ansi: 7.0.0 dev: true @@ -4336,7 +6289,6 @@ packages: /lodash.debounce/4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - dev: false /lodash.includes/4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -4395,12 +6347,29 @@ packages: wrap-ansi: 6.2.0 dev: true + /loglevel-colored-level-prefix/1.0.0: + resolution: {integrity: sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==} + dependencies: + chalk: 1.1.3 + loglevel: 1.8.1 + dev: true + + /loglevel/1.8.1: + resolution: {integrity: sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==} + engines: {node: '>= 0.6.0'} + dev: true + /loose-envify/1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true dependencies: js-tokens: 4.0.0 - dev: false + + /lru-cache/5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true /lru-cache/6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} @@ -4416,8 +6385,8 @@ packages: tsscmp: 1.0.6 dev: true - /magic-string/0.26.3: - resolution: {integrity: sha512-u1Po0NDyFcwdg2nzHT88wSK0+Rih0N1M+Ph1Sp08k8yvFFU3KR72wryS7e1qMPJypt99WB7fIFVCA92mQrMjrg==} + /magic-string/0.26.7: + resolution: {integrity: sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==} engines: {node: '>=12'} dependencies: sourcemap-codec: 1.4.8 @@ -4449,18 +6418,18 @@ packages: engines: {node: '>= 0.6'} dev: true - /memoize-one/5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + /memoize-one/6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} dev: false - /meow/10.1.2: - resolution: {integrity: sha512-zbuAlN+V/sXlbGchNS9WTWjUzeamwMt/BApKCJi7B0QyZstZaMx0n4Unll/fg0njGtMdC9UP5SAscvOCLYdM+Q==} + /meow/10.1.5: + resolution: {integrity: sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: '@types/minimist': 1.2.2 camelcase-keys: 7.0.2 decamelize: 5.0.1 - decamelize-keys: 1.1.0 + decamelize-keys: 1.1.1 hard-rejection: 2.1.0 minimist-options: 4.1.0 normalize-package-data: 3.0.3 @@ -4477,7 +6446,7 @@ packages: dependencies: '@types/minimist': 1.2.2 camelcase-keys: 6.2.2 - decamelize-keys: 1.1.0 + decamelize-keys: 1.1.1 hard-rejection: 2.1.0 minimist-options: 4.1.0 normalize-package-data: 2.5.0 @@ -4559,15 +6528,15 @@ packages: kind-of: 6.0.3 dev: true - /minimist/1.2.6: - resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} + /minimist/1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: true /mkdirp/0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true dependencies: - minimist: 1.2.6 + minimist: 1.2.8 dev: true /moment/2.22.2: @@ -4639,6 +6608,14 @@ packages: hasBin: true dev: true + /natural-compare-lite/1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + dev: true + + /natural-compare/1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + /ncp/2.0.0: resolution: {integrity: sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==} hasBin: true @@ -4661,8 +6638,8 @@ packages: resolution: {integrity: sha512-YdKcy2x0dDwOh+8BEuHvA+mnOKAhmMQDgKBOCUGaLpewdmsRYguYZSom3yA+/OrE61O/q+NMQANnun65xpI1Hw==} dev: true - /node-releases/2.0.6: - resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==} + /node-releases/2.0.10: + resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} dev: true /node-rsa/0.4.2: @@ -4690,8 +6667,8 @@ packages: engines: {node: '>=10'} dependencies: hosted-git-info: 4.1.0 - is-core-module: 2.9.0 - semver: 7.3.7 + is-core-module: 2.11.0 + semver: 7.3.8 validate-npm-package-license: 3.0.4 dev: true @@ -4732,8 +6709,8 @@ packages: engines: {node: '>= 6'} dev: true - /object-inspect/1.12.2: - resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} + /object-inspect/1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} dev: true /object-keys/1.1.1: @@ -4741,24 +6718,58 @@ packages: engines: {node: '>= 0.4'} dev: true - /object.assign/4.1.2: - resolution: {integrity: sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==} + /object.assign/4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 has-symbols: 1.0.3 object-keys: 1.1.1 dev: true - /object.getownpropertydescriptors/2.1.4: - resolution: {integrity: sha512-sccv3L/pMModT6dJAYF3fzGMVcb38ysQ0tEE6ixv2yXJDtEIPph268OlAdJj5/qZMZDq2g/jqvwppt36uS/uQQ==} + /object.entries/1.1.6: + resolution: {integrity: sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.1 + dev: true + + /object.fromentries/2.0.6: + resolution: {integrity: sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.1 + dev: true + + /object.getownpropertydescriptors/2.1.5: + resolution: {integrity: sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==} engines: {node: '>= 0.8'} dependencies: - array.prototype.reduce: 1.0.4 + array.prototype.reduce: 1.0.5 + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.1 + dev: true + + /object.hasown/1.1.2: + resolution: {integrity: sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==} + dependencies: + define-properties: 1.2.0 + es-abstract: 1.21.1 + dev: true + + /object.values/1.1.6: + resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} + engines: {node: '>= 0.4'} + dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.1 + define-properties: 1.2.0 + es-abstract: 1.21.1 dev: true /on-finished/2.3.0: @@ -4793,6 +6804,18 @@ packages: mimic-fn: 4.0.0 dev: true + /optionator/0.9.1: + resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.3 + dev: true + /os-homedir/1.0.2: resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} engines: {node: '>=0.10.0'} @@ -4885,7 +6908,6 @@ packages: engines: {node: '>=6'} dependencies: callsites: 3.1.0 - dev: false /parse-json/5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} @@ -4956,13 +6978,11 @@ packages: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} dev: false - /phosphor-react/1.4.1_react@18.2.0: + /phosphor-react/1.4.1: resolution: {integrity: sha512-gO5j7U0xZrdglTAYDYPACU4xDOFBTJmptrrB/GeR+tHhCZF3nUMyGmV/0hnloKjuTrOmpSFlbfOY78H39rgjUQ==} engines: {node: '>=10'} peerDependencies: react: '>=16' - dependencies: - react: 18.2.0 dev: false /picocolors/1.0.0: @@ -5007,38 +7027,29 @@ packages: engines: {node: '>=4'} dev: false - /popmotion/11.0.5: - resolution: {integrity: sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==} - dependencies: - framesync: 6.1.2 - hey-listen: 1.0.8 - style-value-types: 5.1.2 - tslib: 2.4.0 - dev: false - - /postcss-import/14.1.0_postcss@8.4.17: + /postcss-import/14.1.0_postcss@8.4.21: resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} engines: {node: '>=10.0.0'} peerDependencies: postcss: ^8.0.0 dependencies: - postcss: 8.4.17 + postcss: 8.4.21 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.1 dev: true - /postcss-js/4.0.0_postcss@8.4.17: - resolution: {integrity: sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==} + /postcss-js/4.0.1_postcss@8.4.21: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} engines: {node: ^12 || ^14 || >= 16} peerDependencies: - postcss: ^8.3.3 + postcss: ^8.4.21 dependencies: camelcase-css: 2.0.1 - postcss: 8.4.17 + postcss: 8.4.21 dev: true - /postcss-load-config/3.1.4_postcss@8.4.17: + /postcss-load-config/3.1.4_postcss@8.4.21: resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} engines: {node: '>= 10'} peerDependencies: @@ -5051,18 +7062,18 @@ packages: optional: true dependencies: lilconfig: 2.0.6 - postcss: 8.4.17 + postcss: 8.4.21 yaml: 1.10.2 dev: true - /postcss-nested/5.0.6_postcss@8.4.17: - resolution: {integrity: sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==} + /postcss-nested/6.0.0_postcss@8.4.21: + resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==} engines: {node: '>=12.0'} peerDependencies: postcss: ^8.2.14 dependencies: - postcss: 8.4.17 - postcss-selector-parser: 6.0.10 + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 dev: true /postcss-selector-parser/6.0.10: @@ -5073,25 +7084,74 @@ packages: util-deprecate: 1.0.2 dev: true - /postcss-value-parser/4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + /postcss-selector-parser/6.0.11: + resolution: {integrity: sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser/4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss/8.4.21: + resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prelude-ls/1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier-eslint/15.0.1: + resolution: {integrity: sha512-mGOWVHixSvpZWARqSDXbdtTL54mMBxc5oQYQ6RAqy8jecuNJBgN3t9E5a81G66F8x8fsKNiR1HWaBV66MJDOpg==} + engines: {node: '>=10.0.0'} + dependencies: + '@types/eslint': 8.21.1 + '@types/prettier': 2.7.2 + '@typescript-eslint/parser': 5.52.0_7kw3g6rralp5ps6mg3uyzz6azm + common-tags: 1.8.2 + dlv: 1.1.3 + eslint: 8.34.0 + indent-string: 4.0.0 + lodash.merge: 4.6.2 + loglevel-colored-level-prefix: 1.0.0 + prettier: 2.8.4 + pretty-format: 23.6.0 + require-relative: 0.8.7 + typescript: 4.9.5 + vue-eslint-parser: 8.3.0_eslint@8.34.0 + transitivePeerDependencies: + - supports-color dev: true - /postcss/8.4.17: - resolution: {integrity: sha512-UNxNOLQydcOFi41yHNMcKRZ39NeXlr8AxGuZJsdub8vIb12fHzcq37DTU/QtbI6WLxNg2gF9Z+8qtRwTj1UI1Q==} - engines: {node: ^10 || ^12 || >=14} + /prettier-linter-helpers/1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} dependencies: - nanoid: 3.3.4 - picocolors: 1.0.0 - source-map-js: 1.0.2 + fast-diff: 1.2.0 dev: true - /prettier/2.7.1: - resolution: {integrity: sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==} + /prettier/2.8.4: + resolution: {integrity: sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==} engines: {node: '>=10.13.0'} hasBin: true dev: true + /pretty-format/23.6.0: + resolution: {integrity: sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==} + dependencies: + ansi-regex: 3.0.1 + ansi-styles: 3.2.1 + dev: true + /process-nextick-args/2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: false @@ -5108,7 +7168,6 @@ packages: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 - dev: false /proxy-addr/2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} @@ -5128,8 +7187,8 @@ packages: once: 1.4.0 dev: true - /punycode/2.1.1: - resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} + /punycode/2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} dev: true @@ -5173,13 +7232,21 @@ packages: unpipe: 1.0.0 dev: true - /react-clientside-effect/1.2.6_react@18.2.0: + /react-clientside-effect/1.2.6: resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==} peerDependencies: react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.19.0 - react: 18.2.0 + '@babel/runtime': 7.20.13 + dev: false + + /react-dom/18.2.0: + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + scheduler: 0.23.0 dev: false /react-dom/18.2.0_react@18.2.0: @@ -5192,22 +7259,21 @@ packages: scheduler: 0.23.0 dev: false - /react-error-boundary/3.1.4_react@18.2.0: + /react-error-boundary/3.1.4: resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} engines: {node: '>=10', npm: '>=6'} peerDependencies: react: '>=16.13.1' dependencies: - '@babel/runtime': 7.19.0 - react: 18.2.0 + '@babel/runtime': 7.20.13 dev: false /react-fast-compare/3.2.0: resolution: {integrity: sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==} dev: false - /react-focus-lock/2.9.1_iapumuv4e6jcjznwuxpf4tt22e: - resolution: {integrity: sha512-pSWOQrUmiKLkffPO6BpMXN7SNKXMsuOakl652IBuALAu1esk+IcpJyM+ALcYzPTTFz1rD0R54aB9A4HuP5t1Wg==} + /react-focus-lock/2.9.4_@types+react@18.0.28: + resolution: {integrity: sha512-7pEdXyMseqm3kVjhdVH18sovparAzLg5h6WvIx7/Ck3ekjhrrDMEegHSa3swwC8wgfdd7DIdUVRGeiHT9/7Sgg==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5215,63 +7281,57 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.19.0 - '@types/react': 18.0.21 - focus-lock: 0.11.3 + '@babel/runtime': 7.20.13 + '@types/react': 18.0.28 + focus-lock: 0.11.6 prop-types: 15.8.1 - react: 18.2.0 - react-clientside-effect: 1.2.6_react@18.2.0 - use-callback-ref: 1.3.0_iapumuv4e6jcjznwuxpf4tt22e - use-sidecar: 1.1.2_iapumuv4e6jcjznwuxpf4tt22e + react-clientside-effect: 1.2.6 + use-callback-ref: 1.3.0_@types+react@18.0.28 + use-sidecar: 1.1.2_@types+react@18.0.28 dev: false - /react-helmet/6.1.0_react@18.2.0: + /react-helmet/6.1.0: resolution: {integrity: sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==} peerDependencies: react: '>=16.3.0' dependencies: object-assign: 4.1.1 prop-types: 15.8.1 - react: 18.2.0 react-fast-compare: 3.2.0 - react-side-effect: 2.1.1_react@18.2.0 + react-side-effect: 2.1.2 dev: false - /react-hook-form/7.37.0_react@18.2.0: - resolution: {integrity: sha512-6NFTxsnw+EXSpNNvLr5nFMjPdYKRryQcelTHg7zwBB6vAzfPIcZq4AExP4heVlwdzntepQgwiOQW4z7Mr99Lsg==} + /react-hook-form/7.43.1: + resolution: {integrity: sha512-+s3+s8LLytRMriwwuSqeLStVjRXFGxgjjx2jED7Z+wz1J/88vpxieRQGvJVvzrzVxshZ0BRuocFERb779m2kNg==} engines: {node: '>=12.22.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 - dependencies: - react: 18.2.0 dev: false - /react-hot-toast/2.4.0_biqbaboplfbrettd7655fr4n2y: + /react-hot-toast/2.4.0_react-dom@18.2.0: resolution: {integrity: sha512-qnnVbXropKuwUpriVVosgo8QrB+IaPJCpL8oBI6Ov84uvHZ5QQcTp2qg6ku2wNfgJl6rlQXJIQU5q+5lmPOutA==} engines: {node: '>=10'} peerDependencies: react: '>=16' react-dom: '>=16' dependencies: - goober: 2.1.10 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 + goober: 2.1.12 + react-dom: 18.2.0 transitivePeerDependencies: - csstype dev: false - /react-hotkeys-hook/3.4.7_biqbaboplfbrettd7655fr4n2y: + /react-hotkeys-hook/3.4.7_react-dom@18.2.0: resolution: {integrity: sha512-+bbPmhPAl6ns9VkXkNNyxlmCAIyDAcWbB76O4I0ntr3uWCRuIQf/aRLartUahe9chVMPj+OEzzfk3CQSjclUEQ==} peerDependencies: react: '>=16.8.1' react-dom: '>=16.8.1' dependencies: hotkeys-js: 3.9.4 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 + react-dom: 18.2.0 dev: false - /react-i18next/11.18.6_vfm63zmruocgezzfl2v26zlzpy: + /react-i18next/11.18.6_dh3esvl7t2cobrubf5ry42ti3i: resolution: {integrity: sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==} peerDependencies: i18next: '>= 19.0.0' @@ -5284,24 +7344,22 @@ packages: react-native: optional: true dependencies: - '@babel/runtime': 7.19.0 + '@babel/runtime': 7.20.13 html-parse-stringify: 3.0.1 i18next: 21.10.0 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 + react-dom: 18.2.0 dev: false /react-is/16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: false /react-refresh/0.14.0: resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} engines: {node: '>=0.10.0'} dev: true - /react-remove-scroll-bar/2.3.3_iapumuv4e6jcjznwuxpf4tt22e: - resolution: {integrity: sha512-i9GMNWwpz8XpUpQ6QlevUtFjHGqnPG4Hxs+wlIJntu/xcsZVEpJcIV71K3ZkqNy2q3GfgvkD7y6t/Sv8ofYSbw==} + /react-remove-scroll-bar/2.3.4_@types+react@18.0.28: + resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==} engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5310,13 +7368,12 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.0.21 - react: 18.2.0 - react-style-singleton: 2.2.1_iapumuv4e6jcjznwuxpf4tt22e - tslib: 2.4.0 + '@types/react': 18.0.28 + react-style-singleton: 2.2.1_@types+react@18.0.28 + tslib: 2.5.0 dev: false - /react-remove-scroll/2.5.5_iapumuv4e6jcjznwuxpf4tt22e: + /react-remove-scroll/2.5.5_@types+react@18.0.28: resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} engines: {node: '>=10'} peerDependencies: @@ -5326,67 +7383,85 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.0.21 - react: 18.2.0 - react-remove-scroll-bar: 2.3.3_iapumuv4e6jcjznwuxpf4tt22e - react-style-singleton: 2.2.1_iapumuv4e6jcjznwuxpf4tt22e - tslib: 2.4.0 - use-callback-ref: 1.3.0_iapumuv4e6jcjznwuxpf4tt22e - use-sidecar: 1.1.2_iapumuv4e6jcjznwuxpf4tt22e + '@types/react': 18.0.28 + react-remove-scroll-bar: 2.3.4_@types+react@18.0.28 + react-style-singleton: 2.2.1_@types+react@18.0.28 + tslib: 2.5.0 + use-callback-ref: 1.3.0_@types+react@18.0.28 + use-sidecar: 1.1.2_@types+react@18.0.28 dev: false - /react-router-dom/6.4.2_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-yM1kjoTkpfjgczPrcyWrp+OuQMyB1WleICiiGfstnQYo/S8hPEEnVjr/RdmlH6yKK4Tnj1UGXFSa7uwAtmDoLQ==} + /react-router-dom/6.8.1_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==} engines: {node: '>=14'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' dependencies: - '@remix-run/router': 1.0.2 + '@remix-run/router': 1.3.2 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 - react-router: 6.4.2_react@18.2.0 + react-router: 6.8.1_react@18.2.0 + dev: false + + /react-router-dom/6.8.1_react-dom@18.2.0: + resolution: {integrity: sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==} + engines: {node: '>=14'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + dependencies: + '@remix-run/router': 1.3.2 + react-dom: 18.2.0 + react-router: 6.8.1 + dev: false + + /react-router/6.8.1: + resolution: {integrity: sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==} + engines: {node: '>=14'} + peerDependencies: + react: '>=16.8' + dependencies: + '@remix-run/router': 1.3.2 dev: false - /react-router/6.4.2_react@18.2.0: - resolution: {integrity: sha512-Rb0BAX9KHhVzT1OKhMvCDMw776aTYM0DtkxqUBP8dNBom3mPXlfNs76JNGK8wKJ1IZEY1+WGj+cvZxHVk/GiKw==} + /react-router/6.8.1_react@18.2.0: + resolution: {integrity: sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==} engines: {node: '>=14'} peerDependencies: react: '>=16.8' dependencies: - '@remix-run/router': 1.0.2 + '@remix-run/router': 1.3.2 react: 18.2.0 dev: false - /react-select/5.4.0_rj7ozvcq3uehdlnj3cbwzbi5ce: - resolution: {integrity: sha512-CjE9RFLUvChd5SdlfG4vqxZd55AZJRrLrHzkQyTYeHlpOztqcgnyftYAolJ0SGsBev6zAs6qFrjm6KU3eo2hzg==} + /react-select/5.7.0_bbiwgh4gz2syd5dgiw2t3uvqai: + resolution: {integrity: sha512-lJGiMxCa3cqnUr2Jjtg9YHsaytiZqeNOKeibv6WF5zbK/fPegZ1hg3y/9P1RZVLhqBTs0PfqQLKuAACednYGhQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.19.0 - '@emotion/cache': 11.10.3 - '@emotion/react': 11.10.4_iapumuv4e6jcjznwuxpf4tt22e + '@babel/runtime': 7.20.13 + '@emotion/cache': 11.10.5 + '@emotion/react': 11.10.6_@types+react@18.0.28 + '@floating-ui/dom': 1.2.1 '@types/react-transition-group': 4.4.5 - memoize-one: 5.2.1 + memoize-one: 6.0.0 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - react-transition-group: 4.4.5_biqbaboplfbrettd7655fr4n2y + react-dom: 18.2.0 + react-transition-group: 4.4.5_react-dom@18.2.0 + use-isomorphic-layout-effect: 1.1.2_@types+react@18.0.28 transitivePeerDependencies: - - '@babel/core' - '@types/react' dev: false - /react-side-effect/2.1.1_react@18.2.0: - resolution: {integrity: sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ==} + /react-side-effect/2.1.2: + resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==} peerDependencies: - react: ^16.3.0 || ^17.0.0 - dependencies: - react: 18.2.0 + react: ^16.3.0 || ^17.0.0 || ^18.0.0 dev: false - /react-style-singleton/2.2.1_iapumuv4e6jcjznwuxpf4tt22e: + /react-style-singleton/2.2.1_@types+react@18.0.28: resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} peerDependencies: @@ -5396,40 +7471,38 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.0.21 + '@types/react': 18.0.28 get-nonce: 1.0.1 invariant: 2.2.4 - react: 18.2.0 - tslib: 2.4.0 + tslib: 2.5.0 dev: false - /react-swipeable/7.0.0_react@18.2.0: + /react-swipeable/7.0.0: resolution: {integrity: sha512-NI7KGfQ6gwNFN0Hor3vytYW3iRfMMaivGEuxcADOOfBCx/kqwXE8IfHFxEcxSUkxCYf38COLKYd9EMYZghqaUA==} peerDependencies: react: ^16.8.3 || ^17 || ^18 - dependencies: - react: 18.2.0 dev: false - /react-transition-group/4.4.5_biqbaboplfbrettd7655fr4n2y: + /react-transition-group/4.4.5_react-dom@18.2.0: resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: react: '>=16.6.0' react-dom: '>=16.6.0' dependencies: - '@babel/runtime': 7.19.0 + '@babel/runtime': 7.20.13 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 + react-dom: 18.2.0 dev: false - /react-use-websocket/4.2.0: - resolution: {integrity: sha512-ZovaTlc/tWX6a590fi3kMWImhyoWj46BWJWvO5oucZJzRnVVhYtes2D9g+5MKXjSdR7Es3456hB89v4/1pcBKg==} + /react-use-websocket/4.3.1_react@18.2.0: + resolution: {integrity: sha512-zHPLWrgcqydJaak2O5V9hiz4q2dwkwqNQqpgFVmSuPxLZdsZlnDs8DVHy3WtHH+A6ms/8aHIyX7+7ulOcrnR0Q==} peerDependencies: react: '>= 18.0.0' react-dom: '>= 18.0.0' + dependencies: + react: 18.2.0 dev: false /react/18.2.0: @@ -5437,7 +7510,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - dev: false /read-cache/1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} @@ -5509,7 +7581,7 @@ packages: engines: {node: '>= 6'} dependencies: inherits: 2.0.4 - string_decoder: 1.1.1 + string_decoder: 1.3.0 util-deprecate: 1.0.2 dev: true @@ -5536,6 +7608,17 @@ packages: strip-indent: 4.0.0 dev: true + /regenerate-unicode-properties/10.1.0: + resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + dev: true + + /regenerate/1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + dev: true + /regenerator-runtime/0.11.1: resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} dev: true @@ -5544,19 +7627,48 @@ packages: resolution: {integrity: sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==} dev: true - /regenerator-runtime/0.13.9: - resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} - dev: false + /regenerator-runtime/0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + /regenerator-transform/0.15.1: + resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==} + dependencies: + '@babel/runtime': 7.20.13 + dev: true /regexp.prototype.flags/1.4.3: resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 functions-have-names: 1.2.3 dev: true + /regexpp/3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: true + + /regexpu-core/5.3.1: + resolution: {integrity: sha512-nCOzW2V/X15XpLsK2rlgdwrysrBq+AauCn+omItIz4R1pIcmeot5zvjdmOBRLzEH/CkC6IxMJVmxDe3QcMuNVQ==} + engines: {node: '>=4'} + dependencies: + '@babel/regjsgen': 0.8.0 + regenerate: 1.4.2 + regenerate-unicode-properties: 10.1.0 + regjsparser: 0.9.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.1.0 + dev: true + + /regjsparser/0.9.1: + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} + hasBin: true + dependencies: + jsesc: 0.5.0 + dev: true + /remove-accents/0.4.2: resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==} dev: false @@ -5566,18 +7678,35 @@ packages: engines: {node: '>=0.10.0'} dev: true + /require-relative/0.8.7: + resolution: {integrity: sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==} + dev: true + + /requireindex/1.2.0: + resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} + engines: {node: '>=0.10.5'} + dev: true + /resolve-from/4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - dev: false /resolve/1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} hasBin: true dependencies: - is-core-module: 2.10.0 + is-core-module: 2.11.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + /resolve/2.0.0-next.4: + resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} + hasBin: true + dependencies: + is-core-module: 2.11.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + dev: true /restore-cursor/3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} @@ -5608,16 +7737,23 @@ packages: glob: 6.0.4 dev: true - /rollup/2.78.1: - resolution: {integrity: sha512-VeeCgtGi4P+o9hIg+xz4qQpRl6R401LWEXBmxYKOV4zlF82lyhgh2hTZnheFUbANE8l2A41F458iwj2vEYaXJg==} + /rimraf/3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rollup/2.79.1: + resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} engines: {node: '>=10.0.0'} hasBin: true optionalDependencies: fsevents: 2.3.2 dev: true - /rooks/7.4.0_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-k9gOJid4/BRcznOSkce+sPlVulLj7ZuM9iEXKYk9Js4TplFULs8K/I8x1N9c0z/YkF9MHjmpe1JYVKpUJrNqVg==} + /rooks/7.4.3_react-dom@18.2.0: + resolution: {integrity: sha512-SdXdPbw8bSqpjke4mkqLocn7e46jUNRjQs6bOKUHlZmf7b5zoMOn7Xkr+FjT5ZH/0px4Zj2TJRRPzoPJTUt2lQ==} engines: {node: '>=v10.24.1'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5625,8 +7761,7 @@ packages: dependencies: lodash.debounce: 4.0.8 raf: 3.4.1 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 + react-dom: 18.2.0 dev: false /run-parallel/1.2.0: @@ -5635,15 +7770,27 @@ packages: queue-microtask: 1.2.3 dev: true - /rxjs/7.5.7: - resolution: {integrity: sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==} + /rxjs/7.8.0: + resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} dependencies: - tslib: 2.4.0 + tslib: 2.5.0 dev: true /safe-buffer/5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + /safe-buffer/5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /safe-regex-test/1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + is-regex: 1.1.4 + dev: true + /safer-buffer/2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: true @@ -5664,8 +7811,8 @@ packages: hasBin: true dev: true - /semver/7.3.7: - resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==} + /semver/7.3.8: + resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} engines: {node: '>=10'} hasBin: true dependencies: @@ -5729,16 +7876,16 @@ packages: engines: {node: '>=8'} dev: true - /shell-quote/1.7.3: - resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==} + /shell-quote/1.8.0: + resolution: {integrity: sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==} dev: true /side-channel/1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: call-bind: 1.0.2 - get-intrinsic: 1.1.2 - object-inspect: 1.12.2 + get-intrinsic: 1.2.0 + object-inspect: 1.12.3 dev: true /signal-exit/3.0.7: @@ -5750,6 +7897,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /slash/3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + /slash/4.0.0: resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} engines: {node: '>=12'} @@ -5777,7 +7929,7 @@ packages: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} dependencies: - ansi-styles: 6.1.1 + ansi-styles: 6.2.1 is-fullwidth-code-point: 4.0.0 dev: true @@ -5793,6 +7945,7 @@ packages: /sourcemap-codec/1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead dev: true /spawn-command/0.0.2-1: @@ -5803,7 +7956,7 @@ packages: resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==} dependencies: spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.11 + spdx-license-ids: 3.0.12 dev: true /spdx-exceptions/2.3.0: @@ -5814,11 +7967,11 @@ packages: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} dependencies: spdx-exceptions: 2.3.0 - spdx-license-ids: 3.0.11 + spdx-license-ids: 3.0.12 dev: true - /spdx-license-ids/3.0.11: - resolution: {integrity: sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==} + /spdx-license-ids/3.0.12: + resolution: {integrity: sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==} dev: true /split2/3.0.0: @@ -5870,20 +8023,33 @@ packages: strip-ansi: 7.0.1 dev: true - /string.prototype.trimend/1.0.5: - resolution: {integrity: sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==} + /string.prototype.matchall/4.0.8: + resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.1 + get-intrinsic: 1.2.0 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + regexp.prototype.flags: 1.4.3 + side-channel: 1.0.4 + dev: true + + /string.prototype.trimend/1.0.6: + resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.1 + define-properties: 1.2.0 + es-abstract: 1.21.1 dev: true - /string.prototype.trimstart/1.0.5: - resolution: {integrity: sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==} + /string.prototype.trimstart/1.0.6: + resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.1 + define-properties: 1.2.0 + es-abstract: 1.21.1 dev: true /string_decoder/0.10.31: @@ -5894,6 +8060,13 @@ packages: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} dependencies: safe-buffer: 5.1.2 + dev: false + + /string_decoder/1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true /stringify-object/3.3.0: resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} @@ -5904,6 +8077,13 @@ packages: is-regexp: 1.0.0 dev: true + /strip-ansi/3.0.1: + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} + dependencies: + ansi-regex: 2.1.1 + dev: true + /strip-ansi/6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -5952,24 +8132,27 @@ packages: engines: {node: '>=0.10.0'} dev: true - /style-value-types/5.1.2: - resolution: {integrity: sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==} - dependencies: - hey-listen: 1.0.8 - tslib: 2.4.0 - dev: false + /strip-json-comments/3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true - /stylis/4.0.13: - resolution: {integrity: sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==} + /stylis/4.1.3: + resolution: {integrity: sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==} dev: false - /superjson/1.10.0: - resolution: {integrity: sha512-ks6I5fm5KXUbDqt4Epe1VwkKDaC9+kIj5HF7yhiHjChFne0EkFqsnTv1mdHE2IT6fq2CzLC3zeA/fw0BRIoNwA==} + /superjson/1.12.2: + resolution: {integrity: sha512-ugvUo9/WmvWOjstornQhsN/sR9mnGtWGYeTxFuqLb4AiT4QdUavjGFRALCPKWWnAiUJ4HTpytj5e0t5HoMRkXg==} engines: {node: '>=10'} dependencies: - copy-anything: 3.0.2 + copy-anything: 3.0.3 dev: false + /supports-color/2.0.0: + resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} + engines: {node: '>=0.8.0'} + dev: true + /supports-color/5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -6034,10 +8217,12 @@ packages: - utf-8-validate dev: true - /tailwindcss/3.1.8: - resolution: {integrity: sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g==} + /tailwindcss/3.2.7_postcss@8.4.21: + resolution: {integrity: sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==} engines: {node: '>=12.13.0'} hasBin: true + peerDependencies: + postcss: ^8.0.9 dependencies: arg: 5.0.2 chokidar: 3.5.3 @@ -6045,19 +8230,20 @@ packages: detective: 5.2.1 didyoumean: 1.2.2 dlv: 1.1.3 - fast-glob: 3.2.11 + fast-glob: 3.2.12 glob-parent: 6.0.2 is-glob: 4.0.3 lilconfig: 2.0.6 + micromatch: 4.0.5 normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.0 - postcss: 8.4.17 - postcss-import: 14.1.0_postcss@8.4.17 - postcss-js: 4.0.0_postcss@8.4.17 - postcss-load-config: 3.1.4_postcss@8.4.17 - postcss-nested: 5.0.6_postcss@8.4.17 - postcss-selector-parser: 6.0.10 + postcss: 8.4.21 + postcss-import: 14.1.0_postcss@8.4.21 + postcss-js: 4.0.1_postcss@8.4.21 + postcss-load-config: 3.1.4_postcss@8.4.21 + postcss-nested: 6.0.0_postcss@8.4.21 + postcss-selector-parser: 6.0.11 postcss-value-parser: 4.2.0 quick-lru: 5.1.1 resolve: 1.22.1 @@ -6065,6 +8251,10 @@ packages: - ts-node dev: true + /text-table/0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + /through/2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: true @@ -6097,12 +8287,12 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true dependencies: - meow: 10.1.2 - trash: 8.1.0 + meow: 10.1.5 + trash: 8.1.1 dev: true - /trash/8.1.0: - resolution: {integrity: sha512-gp+zp7IDcyeLCPzsSqF/zmEykOVaga9lsdxzCmlS/bgbjdA1/SdFRYmHI2KCXrqg01Wmyl8fO6tgcb4kDijZSA==} + /trash/8.1.1: + resolution: {integrity: sha512-r15NUF+BJpDBKLTyOXaB+PhF8qh53TAOTpu/wCt6bqpu488jamsiOV7VdC//yPwAnyGIv1EJgetEnjLNer5XVw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: '@sindresorhus/chunkify': 0.2.0 @@ -6130,6 +8320,10 @@ packages: engines: {node: '>=12'} dev: true + /tsconfig-moon/1.2.1: + resolution: {integrity: sha512-nAUeWsZ6FlUvNWWRVWIrr2J8sJn8dnPdY/q+sry3+mIkwaBnnC5eTHvZ+d1Bqsd0v6pcUaa+r9U+FrNpW8czSA==} + dev: true + /tsconfig/7.0.0: resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} dependencies: @@ -6139,19 +8333,49 @@ packages: strip-json-comments: 2.0.1 dev: true + /tslib/1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true + /tslib/2.4.0: resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + dev: false + + /tslib/2.5.0: + resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} /tsscmp/1.0.6: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} engines: {node: '>=0.6.x'} dev: true + /tsutils/3.21.0_typescript@4.9.5: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 4.9.5 + dev: true + + /type-check/0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + /type-fest/0.13.1: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} dev: true + /type-fest/0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + /type-fest/0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -6184,23 +8408,30 @@ packages: resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==} dev: false - /type/2.6.0: - resolution: {integrity: sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ==} + /type/2.7.2: + resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==} dev: false - /typescript-paths/1.4.0_typescript@4.8.4: + /typed-array-length/1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.10 + dev: true + + /typescript-paths/1.4.0_typescript@4.9.5: resolution: {integrity: sha512-olt3yKj5d4ON5MpQgxvqpiN24ApsrdlRnIQFwqlbzuTjRZFQNoTOzzTJ13vyDRt7VW0cuO+g8HA+eoYfBLWWMA==} peerDependencies: typescript: ^4.7.2 dependencies: - typescript: 4.8.4 + typescript: 4.9.5 dev: true - /typescript/4.8.4: - resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==} + /typescript/4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} engines: {node: '>=4.2.0'} hasBin: true - dev: true /unbox-primitive/1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} @@ -6211,6 +8442,29 @@ packages: which-boxed-primitive: 1.0.2 dev: true + /unicode-canonical-property-names-ecmascript/2.0.0: + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + engines: {node: '>=4'} + dev: true + + /unicode-match-property-ecmascript/2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-property-aliases-ecmascript: 2.1.0 + dev: true + + /unicode-match-property-value-ecmascript/2.1.0: + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + engines: {node: '>=4'} + dev: true + + /unicode-property-aliases-ecmascript/2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + dev: true + /unpipe/1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -6221,24 +8475,13 @@ packages: engines: {node: '>=4'} dev: true - /update-browserslist-db/1.0.10_browserslist@4.21.4: + /update-browserslist-db/1.0.10_browserslist@4.21.5: resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.21.4 - escalade: 3.1.1 - picocolors: 1.0.0 - dev: true - - /update-browserslist-db/1.0.5_browserslist@4.21.3: - resolution: {integrity: sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - dependencies: - browserslist: 4.21.3 + browserslist: 4.21.5 escalade: 3.1.1 picocolors: 1.0.0 dev: true @@ -6246,10 +8489,10 @@ packages: /uri-js/4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: - punycode: 2.1.1 + punycode: 2.3.0 dev: true - /use-callback-ref/1.3.0_iapumuv4e6jcjznwuxpf4tt22e: + /use-callback-ref/1.3.0_@types+react@18.0.28: resolution: {integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==} engines: {node: '>=10'} peerDependencies: @@ -6259,29 +8502,37 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.0.21 - react: 18.2.0 - tslib: 2.4.0 + '@types/react': 18.0.28 + tslib: 2.5.0 dev: false - /use-count-up/3.0.1_react@18.2.0: + /use-count-up/3.0.1: resolution: {integrity: sha512-jlVsXJYje6jh+xwQaCEYrwHoB+nRyillNEmr21bhe9kw7tpRzyrSq9jQs9UOlo+8hCFkuOmjUihL3IjEK/piVg==} peerDependencies: react: '>=16.8.0' dependencies: - react: 18.2.0 - use-elapsed-time: 3.0.2_react@18.2.0 + use-elapsed-time: 3.0.2 dev: false - /use-elapsed-time/3.0.2_react@18.2.0: + /use-elapsed-time/3.0.2: resolution: {integrity: sha512-2EY9lJ5DWbAvT8wWiEp6Ztnl46DjXz2j78uhWbXaz/bg3OfpbgVucCAlcN8Bih6hTJfFTdVYX9L6ySMn5py/wQ==} peerDependencies: react: '>=16.8.0' + dev: false + + /use-isomorphic-layout-effect/1.1.2_@types+react@18.0.28: + resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true dependencies: - react: 18.2.0 + '@types/react': 18.0.28 dev: false - /use-sidecar/1.1.2_iapumuv4e6jcjznwuxpf4tt22e: + /use-sidecar/1.1.2_@types+react@18.0.28: resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} peerDependencies: @@ -6291,10 +8542,9 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.0.21 + '@types/react': 18.0.28 detect-node-es: 1.1.0 - react: 18.2.0 - tslib: 2.4.0 + tslib: 2.5.0 dev: false /use-sync-external-store/1.2.0: @@ -6324,8 +8574,8 @@ packages: /util.promisify/1.0.0: resolution: {integrity: sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==} dependencies: - define-properties: 1.1.4 - object.getownpropertydescriptors: 2.1.4 + define-properties: 1.2.0 + object.getownpropertydescriptors: 2.1.5 dev: true /utils-merge/1.0.1: @@ -6357,6 +8607,10 @@ packages: uuid: 3.3.2 dev: true + /validate-html-nesting/1.2.1: + resolution: {integrity: sha512-T1ab131NkP3BfXB7KUSgV7Rhu81R2id+L6NaJ7NypAAG5iV6gXnPpQE5RK1fvb+3JYsPTL+ihWna5sr5RN9gaQ==} + dev: true + /validate-npm-package-license/3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -6374,40 +8628,80 @@ packages: engines: {node: '>= 0.8'} dev: true - /vite-plugin-tsconfig-paths/1.2.0_2qgi2qwv6eydpccu35h24komdm: + /vite-plugin-tsconfig-paths/1.2.0_mmfldfnusamjexuwtlvii3fpxu: resolution: {integrity: sha512-DpYiubYbrT3MzuJJ/DEavnbACiApoNM5RsH9HQHG5vDlTG4aRmKkwlw9gUFEeymCjvLTwmlScwtj0m02t3sgRQ==} peerDependencies: vite: '*' dependencies: - typescript-paths: 1.4.0_typescript@4.8.4 - vite: 3.1.6 + typescript-paths: 1.4.0_typescript@4.9.5 + vite: 3.2.5 transitivePeerDependencies: - typescript dev: true - /vite/3.1.6: - resolution: {integrity: sha512-qMXIwnehvvcK5XfJiXQUiTxoYAEMKhM+jqCY6ZSTKFBKu1hJnAKEzP3AOcnTerI0cMZYAaJ4wpW1wiXLMDt4mA==} + /vite/3.2.5: + resolution: {integrity: sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.15.18 + postcss: 8.4.21 + resolve: 1.22.1 + rollup: 2.79.1 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /vite/3.2.5_@types+node@18.14.0: + resolution: {integrity: sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: + '@types/node': '>= 14' less: '*' sass: '*' stylus: '*' + sugarss: '*' terser: ^5.4.0 peerDependenciesMeta: + '@types/node': + optional: true less: optional: true sass: optional: true stylus: optional: true + sugarss: + optional: true terser: optional: true dependencies: - esbuild: 0.15.10 - postcss: 8.4.17 + '@types/node': 18.14.0 + esbuild: 0.15.18 + postcss: 8.4.21 resolve: 1.22.1 - rollup: 2.78.1 + rollup: 2.79.1 optionalDependencies: fsevents: 2.3.2 dev: true @@ -6417,6 +8711,24 @@ packages: engines: {node: '>=0.10.0'} dev: false + /vue-eslint-parser/8.3.0_eslint@8.34.0: + resolution: {integrity: sha512-dzHGG3+sYwSf6zFBa0Gi9ZDshD7+ad14DGOdTLjruRVgZXe2J+DcZ9iUhyR48z5g1PqRa20yt3Njna/veLJL/g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + dependencies: + debug: 4.3.4 + eslint: 8.34.0 + eslint-scope: 7.1.1 + eslint-visitor-keys: 3.3.0 + espree: 9.4.1 + esquery: 1.4.2 + lodash: 4.17.21 + semver: 7.3.8 + transitivePeerDependencies: + - supports-color + dev: true + /which-boxed-primitive/1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: @@ -6427,6 +8739,18 @@ packages: is-symbol: 1.0.4 dev: true + /which-typed-array/1.1.9: + resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} + engines: {node: '>= 0.4'} + 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 + dev: true + /which/2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -6435,6 +8759,11 @@ packages: isexe: 2.0.0 dev: true + /word-wrap/1.2.3: + resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} + engines: {node: '>=0.10.0'} + dev: true + /wrap-ansi/6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -6496,6 +8825,10 @@ packages: engines: {node: '>=10'} dev: true + /yallist/3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + /yallist/4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true @@ -6504,8 +8837,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - /yaml/2.1.3: - resolution: {integrity: sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==} + /yaml/2.2.1: + resolution: {integrity: sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==} engines: {node: '>= 14'} dev: true @@ -6527,8 +8860,8 @@ packages: engines: {node: '>=12'} dev: true - /yargs/17.6.0: - resolution: {integrity: sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==} + /yargs/17.7.0: + resolution: {integrity: sha512-dwqOPg5trmrre9+v8SUo2q/hAwyKoVfu8OC1xPHKJGNdxAvPl4sKxL4vBnh3bQz/ZvvGAFeA5H3ou2kcOY8sQQ==} engines: {node: '>=12'} dependencies: cliui: 8.0.1 @@ -6545,12 +8878,12 @@ packages: engines: {node: '>=10'} dev: true - /zod/3.19.1: - resolution: {integrity: sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==} + /zod/3.20.6: + resolution: {integrity: sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA==} dev: false - /zustand/4.1.1_immer@9.0.15: - resolution: {integrity: sha512-h4F3WMqsZgvvaE0n3lThx4MM81Ls9xebjvrABNzf5+jb3/03YjNTSgZXeyrvXDArMeV9untvWXRw1tY+ntPYbA==} + /zustand/4.3.3_immer@9.0.19: + resolution: {integrity: sha512-x2jXq8S0kfLGNwGh87nhRfEc2eZy37tSatpSoSIN+O6HIaBhgQHSONV/F9VNrNcBcKQu/E80K1DeHDYQC/zCrQ==} engines: {node: '>=12.7.0'} peerDependencies: immer: '>=9.0' @@ -6561,12 +8894,12 @@ packages: react: optional: true dependencies: - immer: 9.0.15 + immer: 9.0.19 use-sync-external-store: 1.2.0 dev: false - /zustand/4.1.1_immer@9.0.15+react@18.2.0: - resolution: {integrity: sha512-h4F3WMqsZgvvaE0n3lThx4MM81Ls9xebjvrABNzf5+jb3/03YjNTSgZXeyrvXDArMeV9untvWXRw1tY+ntPYbA==} + /zustand/4.3.3_immer@9.0.19+react@18.2.0: + resolution: {integrity: sha512-x2jXq8S0kfLGNwGh87nhRfEc2eZy37tSatpSoSIN+O6HIaBhgQHSONV/F9VNrNcBcKQu/E80K1DeHDYQC/zCrQ==} engines: {node: '>=12.7.0'} peerDependencies: immer: '>=9.0' @@ -6577,7 +8910,7 @@ packages: react: optional: true dependencies: - immer: 9.0.15 + immer: 9.0.19 react: 18.2.0 use-sync-external-store: 1.2.0_react@18.2.0 dev: false diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b81133cf7..480131343 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,4 @@ packages: - 'core' - 'apps/*' - - 'common/*' + - 'packages/*' diff --git a/scripts/lib b/scripts/lib new file mode 100644 index 000000000..203e9b69e --- /dev/null +++ b/scripts/lib @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +log_error() { + echo $1 1>&2 + exit 1 +} + +create_dummy_rust_file() { + local path=$1 + echo "Creating dummy Rust file in $path" + mkdir -p $path/src + if [[ $path == "core" ]]; then + echo 'fn foo() { println!("Wow, such empty!"); }' > $path/src/lib.rs + else + echo 'fn main() { println!("Wow, such empty!"); }' > $path/src/main.rs + fi + +} + +# prisma uses some `include_str!` macros that are mapped to locations on the host machine. so +# when we build in docker, we need to correct these paths according to the docker workdir. +# it's a bit of a hack, but it works lol +prisma_sed_correction() { + set -ex; \ + sed -i 's|\/.*\/core\/prisma\/schema.prisma|\/app\/core\/prisma\/schema.prisma|g' core/src/prisma.rs; \ + sed -i 's|\/.*\/core\/prisma\/migrations|\/app\/core\/prisma\/migrations|g' core/src/prisma.rs +} + +workspaces_sed_correction() { + set -ex; \ + sed -i '/core\/integration-tests/d' Cargo.toml; \ + sed -i '/apps\/desktop\/src-tauri/d' Cargo.toml; \ + sed -i '/apps\/tui/d' Cargo.toml; \ + sed -i '/packages\/prisma-cli/d' Cargo.toml +} \ No newline at end of file diff --git a/scripts/release/Dockerfile b/scripts/release/Dockerfile new file mode 100644 index 000000000..9635a3758 --- /dev/null +++ b/scripts/release/Dockerfile @@ -0,0 +1,214 @@ +# ------------------------------------------------------------------------------ +# Frontend Build Stage +# ------------------------------------------------------------------------------ + +FROM node:16-alpine3.14 as frontend +ARG TARGETARCH + +WORKDIR /app + +# Note: I don't like copying ~everything~ but since I now use types exported from +# the core, and use pnpm specific means of accessing it via the workspace, I kind +# of need to maintain the structure of the workspace and use pnpm +COPY . . + +RUN npm install -g pnpm + +RUN pnpm i +RUN pnpm web build + +RUN mv ./apps/web/dist build + +# ------------------------------------------------------------------------------ +# Cargo Build Stage +# ------------------------------------------------------------------------------ + +###################### +### aarch64 / arm64 ## +###################### + +FROM messense/rust-musl-cross:aarch64-musl AS arm64-backend + +ARG GIT_REV +ENV GIT_REV=${GIT_REV} + +WORKDIR /app + +ENV CARGO_NET_GIT_FETCH_WITH_CLI=true +RUN rustup target add aarch64-unknown-linux-musl + +# TODO: make one-liner +COPY .cargo .cargo +COPY scripts scripts +COPY Cargo.toml Cargo.lock ./ + +# Run the build utils script to create the dummy rust files for core and server +RUN set -ex; \ + ./scripts/release/build-utils.sh -w; \ + ./scripts/release/build-utils.sh -d './core'; \ + ./scripts/release/build-utils.sh -d './apps/server' + +# Copy the core and server Cargo.{toml,lock} files +COPY ./apps/server/Cargo.toml ./apps/server/ +COPY ./core/Cargo.toml ./core/ + +# This is where the ~magic~ happens. We build the server (which pulls the core as a dependency) with +# the dummy files we created above. This ~should~ allow caching until the dependencies themselves change. +RUN set -ex; \ + cargo build --release --target aarch64-unknown-linux-musl; \ + rm -rf apps/server/src; \ + rm -rf core/src + +# Now we can copy the real source files and build the server +COPY . . +COPY ./core/src/prisma.rs ./core/src/prisma.rs + +RUN set -ex; \ + ./scripts/release/build-utils.sh -p; \ + cargo build --package stump_server --bin stump_server --release --target aarch64-unknown-linux-musl; \ + cp target/aarch64-unknown-linux-musl/release/stump_server ./stump + +# FIXME: armv7 is currently broken. I have a gut it needs a similar workaround as the arm64 +###################### +### armv7 / arm/v7 ### +###################### + +# Note: the name here isn't entirely accurate to my understanding. But I can't figure +# out how to have the name be v7 inclusive so +FROM messense/rust-musl-cross:armv7-musleabihf@sha256:3e133558686fd5059ce25749cece40a81d87dad2c7a68727c36a1bcacba6752c AS arm-backend + +ARG GIT_REV +ENV GIT_REV=${GIT_REV} + +WORKDIR /app + +ENV CARGO_NET_GIT_FETCH_WITH_CLI=true + +RUN rustup update && rustup target add armv7-unknown-linux-musleabihf + +# TODO: make one-liner +COPY .cargo .cargo +COPY scripts scripts +COPY Cargo.toml Cargo.lock ./ + +# Run the build utils script to create the dummy rust files for core and server +RUN set -ex; \ + ./scripts/release/build-utils.sh -w; \ + ./scripts/release/build-utils.sh -d './core'; \ + ./scripts/release/build-utils.sh -d './apps/server' + +# Copy the core and server Cargo.{toml,lock} files +COPY ./apps/server/Cargo.toml ./apps/server/ +COPY ./core/Cargo.toml ./core/ + +# This is where the ~magic~ happens. We build the server (which pulls the core as a dependency) with +# the dummy files we created above. This ~should~ allow caching until the dependencies themselves change. +RUN set -ex; \ + cargo build --release --target armv7-unknown-linux-musleabihf; \ + rm -rf apps/server/src; \ + rm -rf core/src + +# Now we can copy the real source files and build the server +COPY . . +COPY ./core/src/prisma.rs ./core/src/prisma.rs + +RUN set -ex; \ + ./scripts/release/build-utils.sh -p; \ + cargo build --package stump_server --bin stump_server --release --target armv7-unknown-linux-musleabihf; \ + cp target/armv7-unknown-linux-musleabihf/release/stump_server ./stump + +###################### +### x86_64 / amd64 ### +###################### + +FROM messense/rust-musl-cross:x86_64-musl AS amd64-backend + +ARG GIT_REV +ENV GIT_REV=${GIT_REV} + + +WORKDIR /app + +ENV CARGO_NET_GIT_FETCH_WITH_CLI=true + +RUN rustup update && rustup target add x86_64-unknown-linux-musl + +# TODO: make one-liner +COPY .cargo .cargo +COPY scripts scripts +COPY Cargo.toml Cargo.lock ./ + +# Run the build utils script to create the dummy rust files for core and server +RUN set -ex; \ + ./scripts/release/build-utils.sh -w; \ + ./scripts/release/build-utils.sh -d './core'; \ + ./scripts/release/build-utils.sh -d './apps/server' + +# Copy the core and server Cargo.{toml,lock} files +COPY ./apps/server/Cargo.toml ./apps/server/ +COPY ./core/Cargo.toml ./core/ + +# This is where the ~magic~ happens. We build the server (which pulls the core as a dependency) with +# the dummy files we created above. This ~should~ allow caching until the dependencies themselves change. +RUN set -ex; \ + cargo build --release --target x86_64-unknown-linux-musl; \ + rm -rf apps/server/src; \ + rm -rf core/src + +# Now we can copy the real source files and build the server +COPY . . +COPY ./core/src/prisma.rs ./core/src/prisma.rs + +RUN set -ex; \ + ./scripts/release/build-utils.sh -p; \ + cargo build --package stump_server --bin stump_server --release --target x86_64-unknown-linux-musl; \ + cp target/x86_64-unknown-linux-musl/release/stump_server ./stump + +###################### +## Conditional step ## +###################### + +# Conditional to skip non-targetarch build stages +FROM ${TARGETARCH}-backend AS core-builder + +# ------------------------------------------------------------------------------ +# Final Stage +# ------------------------------------------------------------------------------ +FROM alpine:latest + +# libc6-compat +RUN apk add --no-cache libstdc++ binutils + +WORKDIR / + +# create the config, data and app directories +RUN mkdir -p config && \ + mkdir -p data && \ + mkdir -p app + +# copy the binary +COPY --from=core-builder /app/stump ./app/stump + +# copy the react build +COPY --from=frontend /app/build ./app/client + +# Copy docker entrypoint +# This will take care of starting the service daemon as a regular user, if desired +COPY scripts/release/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# TODO: replace this with something more elegant lol maybe a bash case statement +RUN ln -s /lib/ld-musl-aarch64.so.1 /lib/ld-linux-aarch64.so.1; exit 0 + +# Default Stump environment variables +ENV STUMP_CONFIG_DIR=/config +ENV STUMP_CLIENT_DIR=/app/client +ENV STUMP_PROFILE=release +ENV STUMP_PORT=10801 +ENV STUMP_IN_DOCKER=true + +ENV API_VERSION=v1 + +WORKDIR /app + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/scripts/release/build-docker.sh b/scripts/release/build-docker.sh new file mode 100755 index 000000000..4ab3651f6 --- /dev/null +++ b/scripts/release/build-docker.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +FORMAT=${1:-auto} +PLATFORMS=${2:-linux/amd64} +TAG=${3:-nightly} +GIT_REV=$(git rev-parse --short HEAD) + +docker buildx build -f ./scripts/release/Dockerfile --load --progress=$FORMAT --platform=$PLATFORMS -t aaronleopold/stump:$TAG --build-arg GIT_REV=$GIT_REV . \ No newline at end of file diff --git a/scripts/release/build-utils.sh b/scripts/release/build-utils.sh new file mode 100755 index 000000000..14a931b26 --- /dev/null +++ b/scripts/release/build-utils.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +SCRIPTS_DIR="${BASH_SOURCE%/*}/.." +source "${SCRIPTS_DIR}/lib" + +while getopts "pwd:" opt; do + case $opt in + p) + prisma_sed_correction + ;; + w) + workspaces_sed_correction + ;; + d) + path="$OPTARG" + echo "The path provided is $OPTARG" + create_dummy_rust_file $path + ;; + ?) + echo "Invalid option -$OPTARG" >&2 + exit 1 + ;; + esac +done +shift "$(($OPTIND -1))" \ No newline at end of file diff --git a/scripts/release/compile-server.sh b/scripts/release/compile-server.sh new file mode 100755 index 000000000..628346972 --- /dev/null +++ b/scripts/release/compile-server.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +SCRIPTS_DIR="${BASH_SOURCE%/*}/.." +source "${SCRIPTS_DIR}/lib" + +echo "Ensuring targets for apple(x86_64,darwin),linux(x86_64,arm64), and windows(x86_64) are installed..." + +set -ex; \ + rustup target add x86_64-apple-darwin; \ + rustup target add aarch64-apple-darwin; \ + rustup target add x86_64-unknown-linux-gnu; \ + rustup target add aarch64-unknown-linux-gnu; \ + rustup target add x86_64-pc-windows-gnu; \ + set +x + +# https://www.shogan.co.uk/development/rust-cross-compile-linux-to-macos-using-github-actions/ +# https://stackoverflow.com/questions/66849112/how-do-i-cross-compile-a-rust-application-from-macos-x86-to-macos-silicon +# https://gist.github.com/shqld/256e2c4f4b97957fb0ec250cdc6dc463 +# lol, at this point, I think it might just be easier to use GH hosted runners +# for the executable builds... Which would mean instead of this scripting doing it all, +# it would just run a subset of these operations on a per-os basis. E.g. the linux +# build would just run the linux-specific commands, and the macos build would just +# run the macos-specific commands. This would also mean that the build process +# would be a lot more straightforward, and would be a lot easier to maintain? Maybe to start, +# I trim it even further. Don't support both arm+x86, just do vanilla builds for each + +echo "Targets installed." + +CALL_TO_ACTION_LOL="Please consider helping to expand support for your system: https://github.com/aaronleopold/stump/issues" + +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + UNSUPPORTED_DISTRO="Your distro '$(lsb_release -s -d)' is not supported by this script. $CALL_TO_ACTION_LOL" + + if which apt-get &> /dev/null; then + # TODO: add support for other distros + log_error "$UNSUPPORTED_DISTRO" + elif which pacman &> /dev/null; then + set -ex; \ + sudo pacman -S --needed mingw-w64-gcc + elif which dnf &> /dev/null; then + # TODO: add support for other distros + log_error "$UNSUPPORTED_DISTRO" + else + log_error "$UNSUPPORTED_DISTRO" + fi +elif [[ "$OSTYPE" == "darwin"* ]]; then + set -ex; \ + set HOMEBREW_NO_AUTO_UPDATE=1; \ + brew tap messense/macos-cross-toolchains; \ + brew install filosottile/musl-cross/musl-cross mingw-w64 x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; \ + set +x + + if which musl-gcc &> /dev/null; then + echo "musl-gcc installed successfully." + else + # https://github.com/FiloSottile/homebrew-musl-cross + echo "musl-gcc is not detected, attempting symlink workaround..." + DIR="/usr/local/opt/musl-cross/bin/x86_64-linux-musl-gcc" + TARG="/usr/local/bin/musl-gcc" + if ln -s "$DIR" "$TARG"; then + echo "Symlink created successfully." + else + log_error "Symlink creation failed." + fi + fi +else + log_error "Your OS '$OSTYPE' is not supported by this script. $CALL_TO_ACTION_LOL" +fi + +export CC_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-gcc +export CXX_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-g++ +export AR_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-ar +export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-unknown-linux-gnu-gcc + +cargo build --package stump_server --release \ + --target x86_64-apple-darwin \ + --target aarch64-apple-darwin \ + --target x86_64-unknown-linux-gnu \ + --target x86_64-unknown-linux-musl \ + --target x86_64-pc-windows-gnu diff --git a/scripts/release/entrypoint.sh b/scripts/release/entrypoint.sh new file mode 100644 index 000000000..53e4b569c --- /dev/null +++ b/scripts/release/entrypoint.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# Depending on the values passed for PUID/PGID via environment variables, +# either starts the stump server daemon as root or as a regular user +# +# Also takes care of assigning proper attributes to the folders /data, /config and /app +PUID=${PUID:-0} +PGID=${PGID:-0} + +USER=stump +GROUP=stump + +## Add stump group if it doesn't already exist +if [[ -z "$(getent group "$PGID" | cut -d':' -f1)" ]]; then + addgroup -g "$PGID" $GROUP +fi + +## Add stump user if it doesn't already exist +if [[ -z "$(getent passwd "$PUID" | cut -d':' -f1)" ]]; then + adduser -D -s /bin/sh -u "$PUID" -G "$GROUP" $USER +fi + +# Change current working directory +cd /app + +if [[ "$PUID" -eq 0 ]]; then + # Run as root + ./stump +else + # Set ownership on config, app and data dir + chown -R "$PUID":"$PGID" /app + chown -R "$PUID":"$PGID" /config + # NOTE: Only change the directory itself, not recursively + # We dont want to accidentally overwrite with incorrect + # permissions if users provide wrong values for PUID/PGID + chown "$PUID":"$PGID" /data + + # Run as non-root user + # NOTE: Omit "-l" switch to keep env vars + su $USER -c ./stump +fi diff --git a/scripts/system-setup.sh b/scripts/system-setup.sh new file mode 100755 index 000000000..81af7195e --- /dev/null +++ b/scripts/system-setup.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +SCRIPTS_DIR="${BASH_SOURCE%/*}" +source "${SCRIPTS_DIR}/lib" + +_DEV_SETUP=${DEV_SETUP:=1} +_CHECK_CARGO=${CHECK_CARGO:=1} +_CHECK_NODE=${CHECK_NODE:=1} +_FORCE_INSTALL_PNPM=${INSTALL_PNPM:=0} + +dev_setup() { + set -ex; \ + cargo install cargo-watch; \ + pnpm run setup; \ + set +x +} + +if [ "$name" == "nix-shell" ]; then + echo "Running nix-shell" + exit 0 +fi + +if [ ${_CHECK_CARGO} == 1 ]; then + which cargo &> /dev/null + if [ $? -ne 0 ]; then + log_error "Rust could not be found on your system. Visit https://www.rust-lang.org/tools/install" + else + echo "Rust requirement met!" + fi +fi + +if [ ${_CHECK_NODE} == 1 ]; then + which node &> /dev/null + if [ $? -eq 1 ]; then + log_error "Node could not be found on your system. Visit https://nodejs.org/en/download/" + else + echo "Node requirement met!" + fi + + which pnpm &> /dev/null + if [ $? -eq 1 ]; then + if [ ${_FORCE_INSTALL_PNPM} == 1 ]; then + echo "Installing pnpm..." + npm install -g pnpm + else + echo "pnpm could not be found on your system. Would you like for this script to attempt to install 'pnpm'? (y/n)" + + can_continue=false + until [ $can_continue = true ]; do + read -p "Choice: " choice + + case $choice in + y) + echo "Attempting to install 'pnpm'..." + npm install -g pnpm + if [ $? -eq 0 ]; then + echo "pnpm installed successfully." + can_continue=true + else + can_continue=false + log_error "pnpm could not be installed. Please ensure you have node and npm installed." + fi + ;; + n) + echo "Skipping 'pnpm' installation. Exiting." + can_continue=false + exit 1 + ;; + *) + echo "Invalid choice. Please enter 'y' or 'n'." + can_continue=false + ;; + esac + + echo + echo "Would you like for this script to attempt to install 'pnpm'? (y/n)" + done + fi + else + echo "pnpm requirement met!" + fi +fi + +CALL_TO_ACTION_LOL="Please consider helping to expand support for your system: https://github.com/aaronleopold/stump/issues" + +# TODO: group these? so lines aren't so long... +# https://tauri.app/v1/guides/getting-started/prerequisites/#1-system-dependencies +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + UNSUPPORTED_DISTRO="Your distro '$(lsb_release -s -d)' is not supported by this script. $CALL_TO_ACTION_LOL" + + if which apt-get &> /dev/null; then + sudo apt-get -y update + sudo apt-get -y install pkg-config libssl-dev libdbus-1-dev libsoup2.4-dev libwebkit2gtk-4.0-dev curl wget libgtk-3-dev libappindicator3-dev librsvg2-dev build-essential libayatana-appindicator3-dev + elif which pacman &> /dev/null; then + sudo pacman -Syu + sudo pacman -S --needed webkit2gtk base-devel curl wget openssl appmenu-gtk-module gtk3 libappindicator-gtk3 librsvg libvips + elif which dnf &> /dev/null; then + sudo dnf check-update + sudo dnf install openssl-devel webkit2gtk4.0-devel curl wget libappindicator-gtk3 librsvg2-devel + sudo dnf group install "C Development Tools and Libraries" + else + log_error $UNSUPPORTED_DISTRO + fi + + if [ {$_DEV_SETUP} == 1 ]; then + dev_setup + fi + + echo "Setup completed! Run 'pnpm dev:web' or 'pnpm start:web' to get started." +elif [[ "$OSTYPE" == "darwin"* ]]; then + if [ {$_DEV_SETUP} == 1 ]; then + dev_setup + fi + + echo "Setup completed! Run 'pnpm dev:web' or 'pnpm start:web' to get started." +else + log_error "Your OS '$OSTYPE' is not supported by the pre-setup script. $CALL_TO_ACTION_LOL" +fi diff --git a/scripts/version/.placeholder b/scripts/version/.placeholder new file mode 100644 index 000000000..390199fd5 --- /dev/null +++ b/scripts/version/.placeholder @@ -0,0 +1 @@ +TODO: put versioning related, helper scripts here \ No newline at end of file diff --git a/stump.service b/stump.service deleted file mode 100644 index e6aaf5209..000000000 --- a/stump.service +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=Stump Book Server -After=network.target -StartLimitIntervalSec=0 -[Service] -Type=simple -Restart=always -RestartSec=1 -User=stump -WorkingDirectory=/opt/stump -ExecStart=pnpm start core - -[Install] -WantedBy=multi-user.target diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 000000000..e5866c61f --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.options.json", + "include": [ + "apps/**/*", + "core", + "crates/**/*", + "packages/**/*", + "**/.eslintrc.js", + "**/*.config.js" + ], + "exclude": [ + "**/.cache/**/*", + "**/.next/**/*", + "**/build/**/*", + "**/cjs/**/*", + "**/dist/**/*", + "**/dts/**/*", + "**/esm/**/*", + "**/lib/**/*", + "**/mjs/**/*", + "**/node_modules/**/*" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..dd738a850 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "./tsconfig.options.json", + "files": [], + "references": [ + { + "path": "apps/desktop" + }, + { + "path": "apps/mobile" + }, + { + "path": "apps/web" + }, + { + "path": "packages/api" + }, + { + "path": "packages/client" + }, + { + "path": "packages/components" + }, + { + "path": "packages/interface" + }, + { + "path": "packages/types" + } + ] +} diff --git a/tsconfig.options.json b/tsconfig.options.json new file mode 100644 index 000000000..4b69d32ab --- /dev/null +++ b/tsconfig.options.json @@ -0,0 +1,27 @@ +{ + "extends": "tsconfig-moon/tsconfig.json", + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "strict": true, + "allowJs": true, + "moduleResolution": "nodenext", + "target": "es2022", + "composite": true, + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "incremental": true, + "noEmit": false, + "noEmitOnError": true, + "noUncheckedIndexedAccess": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "skipLibCheck": true, + "paths": { + "@stump/client": ["./packages/client"], + "@stump/api": ["./packages/api"], + "@stump/types": ["./packages/types"], + "@stump/interface": ["./packages/interface"] + } + } +}