commit ccbb9ea8912ef3b7b6d8fc73b725786a232d087b Author: 还不如一只猪威武 Date: Wed Apr 17 11:45:33 2024 +0800 init diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8e94654 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +insert_final_newline = true + +[*.rs] +charset = utf-8 +end_of_line = lf +indent_size = 4 +insert_final_newline = true diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..6749373 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Information** + - OS: [e.g. macOS] + - Clash Verge Version: [e.g. 1.3.4] + - Clash Core: [e.g. Clash or Clash Meta] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..6269982 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature]" +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/alpha.yml b/.github/workflows/alpha.yml new file mode 100644 index 0000000..b69aa9d --- /dev/null +++ b/.github/workflows/alpha.yml @@ -0,0 +1,93 @@ +name: Alpha CI + +on: + workflow_dispatch: + inputs: + debug: + type: boolean + default: false + +env: + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: short + +jobs: + release: + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-20.04, macos-latest] + runs-on: ${{ matrix.os }} + if: startsWith(github.repository, 'zzzgydi') + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: "16" + cache: "yarn" + + - name: Delete current release assets + if: startsWith(matrix.os, 'ubuntu-') + uses: mknejp/delete-release-assets@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + tag: alpha + fail-if-no-assets: false + fail-if-no-release: false + assets: | + *.zip + *.gz + *.AppImage + *.deb + *.dmg + *.msi + *.sig + *.exe + + - name: Install Dependencies (ubuntu only) + if: startsWith(matrix.os, 'ubuntu-') + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl + + - name: Yarn install and check + run: | + yarn install --network-timeout 1000000 --frozen-lockfile + yarn run check --force + + - name: Tauri build + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + with: + tagName: alpha + releaseName: "Clash Verge Alpha" + releaseBody: "Alpha Version (include debug)" + releaseDraft: false + prerelease: true + includeDebug: ${{ github.event.inputs.debug }} + + - name: Portable Bundle + if: startsWith(matrix.os, 'windows-') + run: | + yarn build + yarn run portable + env: + TAG_NAME: alpha + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + VITE_WIN_PORTABLE: 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1fce34f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,98 @@ +name: Release CI + +on: + workflow_dispatch: + push: + tags: + - v** + +env: + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: short + +jobs: + release: + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + if: startsWith(github.repository, 'zzzgydi') + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: "16" + cache: "yarn" + + - name: Install Dependencies (ubuntu only) + if: startsWith(matrix.os, 'ubuntu-') + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl + + - name: Yarn install and check + run: | + yarn install --network-timeout 1000000 --frozen-lockfile + yarn run check + + - name: Tauri build + uses: tauri-apps/tauri-action@v0 + # enable cache even though failed + # continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + with: + tagName: v__VERSION__ + releaseName: "Clash Verge v__VERSION__" + releaseBody: "More new features are now supported." + releaseDraft: false + prerelease: true + + - name: Portable Bundle + if: startsWith(matrix.os, 'windows-') + # rebuild with env settings + run: | + yarn build + yarn run portable + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + VITE_WIN_PORTABLE: 1 + + release-update: + needs: release + runs-on: ubuntu-latest + if: | + startsWith(github.repository, 'zzzgydi') && + startsWith(github.ref, 'refs/tags/v') + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: "16" + cache: "yarn" + + - name: Yarn install + run: yarn install --network-timeout 1000000 --frozen-lockfile + + - name: Release updater file + run: yarn run updater + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/compatible.yml b/.github/workflows/compatible.yml new file mode 100644 index 0000000..abbbbb6 --- /dev/null +++ b/.github/workflows/compatible.yml @@ -0,0 +1,102 @@ +name: Compatible CI + +on: + workflow_dispatch: + # push: + # tags: + # - v** + +env: + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: short + +jobs: + build: + strategy: + fail-fast: false + matrix: + targets: + - tag: macOS-10.15 + os: macos-10.15 + - tag: Ubuntu18 + os: ubuntu-18.04 + - tag: Ubuntu22 + os: ubuntu-22.04 + + runs-on: ${{ matrix.targets.os }} + if: startsWith(github.repository, 'zzzgydi') + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + + - name: Install Node + uses: actions/setup-node@v1 + with: + node-version: 16 + + # - name: Install Dependencies (ubuntu18 only) + # if: matrix.targets.os == 'ubuntu-18.04' + # run: | + # sudo apt-get update + # sudo apt-get install -y libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libappindicator3-dev librsvg2-dev libayatana-appindicator3-dev + + - name: Install Dependencies (ubuntu22 only) + if: startsWith(matrix.targets.os, 'ubuntu-') + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf + + - name: Get yarn cache dir path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: Yarn Cache + uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Yarn install and check + run: | + yarn install --network-timeout 1000000 + yarn run check + + - name: Tauri build + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + with: + tagName: ${{ matrix.targets.tag }} + releaseName: "Compatible For ${{ matrix.targets.tag }}" + releaseBody: "More new features are now supported." + releaseDraft: false + prerelease: false + + # - name: Portable Bundle + # if: matrix.os == 'windows-latest' + # # rebuild with env settings + # run: | + # yarn build + # yarn run portable + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + # TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + # VITE_WIN_PORTABLE: 1 diff --git a/.github/workflows/meta.yml b/.github/workflows/meta.yml new file mode 100644 index 0000000..a26d9f0 --- /dev/null +++ b/.github/workflows/meta.yml @@ -0,0 +1,107 @@ +name: Meta CI + +on: + workflow_dispatch: + push: + tags: + - v** + +env: + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: short + +jobs: + release: + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + if: startsWith(github.repository, 'zzzgydi') + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + + - name: Install Node + uses: actions/setup-node@v1 + with: + node-version: 16 + + - name: Delete current release assets + if: matrix.os == 'ubuntu-latest' + uses: mknejp/delete-release-assets@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + tag: meta + fail-if-no-assets: false + fail-if-no-release: false + assets: | + *.zip + *.gz + *.AppImage + *.deb + *.dmg + *.msi + *.sig + + - name: Install Dependencies (ubuntu only) + if: startsWith(matrix.os, 'ubuntu-') + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl + + - name: Get yarn cache dir path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: Yarn Cache + uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Yarn install and check + run: | + yarn install --network-timeout 1000000 + yarn run check + + - name: Tauri build + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + with: + tagName: meta + releaseName: "Clash Verge Meta" + releaseBody: "" + releaseDraft: false + prerelease: true + args: -f default-meta + + - name: Portable Bundle + if: matrix.os == 'windows-latest' + run: | + yarn build -f default-meta + yarn run portable + env: + TAG_NAME: meta + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + VITE_WIN_PORTABLE: 1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3576f5c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,76 @@ +name: Test CI + +on: + workflow_dispatch: + inputs: + os: + description: "Runs on OS" + required: true + default: windows-latest + type: choice + options: + - windows-latest + - ubuntu-latest + - macos-latest + - ubuntu-18.04 + - ubuntu-20.04 + - ubuntu-22.04 + - macos-10.15 + - macos-11 + - macos-12 + - windows-2019 + - windows-2022 + +env: + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: short + +jobs: + release: + runs-on: ${{ github.event.inputs.os }} + if: startsWith(github.repository, 'zzzgydi') + steps: + - name: System Version + run: | + echo ${{ github.event.inputs.os }} + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: "16" + cache: "yarn" + + - name: Install Dependencies (ubuntu only) + if: startsWith(github.event.inputs.os, 'ubuntu-') + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf + + - name: Yarn install and check + run: | + yarn install --network-timeout 1000000 --frozen-lockfile + yarn run check + + - name: Tauri build + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + with: + tagName: alpha + releaseName: "Clash Verge Alpha" + releaseBody: "Alpha Version (include debug)" + releaseDraft: false + includeUpdaterJson: false diff --git a/.github/workflows/updater.yml b/.github/workflows/updater.yml new file mode 100644 index 0000000..7c21078 --- /dev/null +++ b/.github/workflows/updater.yml @@ -0,0 +1,25 @@ +name: Updater CI + +on: workflow_dispatch + +jobs: + release-update: + runs-on: ubuntu-latest + if: startsWith(github.repository, 'zzzgydi') + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: "16" + cache: "yarn" + + - name: Yarn install + run: yarn install --network-timeout 1000000 --frozen-lockfile + + - name: Release updater file + run: yarn run updater + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9e2fb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +update.json +scripts/_env.sh +.vscode diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..f3a6796 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn pretty-quick --staged diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d78bc47 --- /dev/null +++ b/README.md @@ -0,0 +1,158 @@ +

+ Clash +
+ Clash Verge +
+

+ +

+A Clash GUI based on tauri. +

+ +## Features + +- Full `clash` config supported, Partial `clash premium` config supported. +- Profiles management and enhancement (by yaml and Javascript). [Doc](https://github.com/zzzgydi/clash-verge/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97) +- Simple UI and supports custom theme color. +- Built-in support [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta) core. +- System proxy setting and guard. + +## Promotion + +[狗狗加速 —— 技术流机场 Doggygo VPN](https://dg1.top) + +- High-performance overseas VPN, free trial, discounted packages, unlock streaming media, the world's first to support Hysteria protocol. +- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。 +- 使用 Clash Verge 专属邀请链接注册送 15 天,每天 1G 流量免费试用:https://panel.dg1.top/#/register?code=sFCDayZf + +
+Promotion Detail + +- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份) +- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折 +- 海外团队,无跑路风险,高达 50% 返佣 +- 集群负载均衡设计,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开 +- 全球首家 Hysteria 协议机场,将在今年 10 月上线更快的 `tuic` 协议(Clash Verge 客户端最佳搭配) +- 解锁流媒体及 ChatGPT +- 官网:https://dg1.top + +
+ +
+ +[EEVPN —— 海外运营机场 ※ 支持 ChatGPT](https://www.eejsq.net/#/register?code=yRr6qBO3) + +- 年付低至 9.99 元,价格低,速度不减 + +
+Promotion Detail + +- 中国大陆 BGP 网络接入 +- IEPL 专线网络 +- 最高 2500Mbps 速率可用 +- 不限制在线客户端 +- 解锁流媒体及 ChatGPT +- 海外运营 数据安全 + +
+ +## Install + +Download from [release](https://github.com/zzzgydi/clash-verge/releases). Supports Windows x64, Linux x86_64 and macOS 11+ + +- [Windows x64](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_x64_en-US.msi) +- [macOS intel](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_x64.dmg) +- [macOS arm](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_aarch64.dmg) +- [Linux AppImage](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/clash-verge_1.3.8_amd64.AppImage) +- [Linux deb](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/clash-verge_1.3.8_amd64.deb) +- [Fedora Linux](https://github.com/zzzgydi/clash-verge/issues/352) + +Or you can build it yourself. Supports Windows, Linux and macOS 10.15+ + +Notes: If you could not start the app on Windows, please check that you have [Webview2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section) installed. + +### FAQ + +#### 1. **macOS** "Clash Verge" is damaged and can't be opened + +open the terminal and run `sudo xattr -r -d com.apple.quarantine /Applications/Clash\ Verge.app` + +## Development + +You should install Rust and Nodejs, see [here](https://tauri.app/v1/guides/getting-started/prerequisites) for more details. Then install Nodejs packages. + +```shell +yarn install +``` + +Then download the clash binary... Or you can download it from [clash premium release](https://github.com/Dreamacro/clash/releases/tag/premium) and rename it according to [tauri config](https://tauri.studio/docs/api/config/#tauri.bundle.externalBin). + +```shell +# force update to latest version +# yarn run check --force + +yarn run check +``` + +Then run + +```shell +yarn dev + +# run it in another way if app instance exists +yarn dev:diff +``` + +Or you can build it + +```shell +yarn build +``` + +## Todos + +> This keng is a little big... + +## Screenshots + +
+ demo1 + demo2 + demo3 + demo4 + demo5 + demo6 +
+ +### Custom Theme + +
+ demo1 + demo2 + demo3 + demo4 + demo5 + demo6 +
+ +## Disclaimer + +This is a learning project for Rust practice. + +## Contributions + +Issue and PR welcome! + +## Acknowledgement + +Clash Verge was based on or inspired by these projects and so on: + +- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, and more secure desktop applications with a web frontend. +- [Dreamacro/clash](https://github.com/Dreamacro/clash): A rule-based tunnel in Go. +- [MetaCubeX/Clash.Meta](https://github.com/MetaCubeX/Clash.Meta): A rule-based tunnel in Go. +- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): A Windows/macOS GUI based on Clash. +- [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast! + +## License + +GPL-3.0 License. See [License here](./LICENSE) for details. diff --git a/UPDATELOG.md b/UPDATELOG.md new file mode 100644 index 0000000..a08a86e --- /dev/null +++ b/UPDATELOG.md @@ -0,0 +1,472 @@ +## v1.3.8 + +### Features + +- update clash meta core +- add default valid keys +- adjust the delay display interval and color + +### Bug Fixes + +- fix connections page undefined exception + +--- + +## v1.3.7 + +### Features + +- update clash and clash meta core +- profiles page add paste button +- subscriptions url textfield use multi lines +- set min window size +- add check for updates buttons +- add open dashboard to the hotkey list + +### Bug Fixes + +- fix profiles page undefined exception + +--- + +## v1.3.6 + +### Features + +- add russian translation +- support to show connection detail +- support clash meta memory usage display +- support proxy provider update ui +- update geo data file from meta repo +- adjust setting page + +### Bug Fixes + +- center the window when it is out of screen +- use `sudo` when `pkexec` not found (Linux) +- reconnect websocket when window focus + +### Notes + +- The current version of the Linux installation package is built by Ubuntu 20.04 (Github Action). + +--- + +## v1.3.5 + +### Features + +- update clash core + +### Bug Fixes + +- fix blurry system tray icon (Windows) +- fix v1.3.4 wintun.dll not found (Windows) +- fix v1.3.4 clash core not found (macOS, Linux) + +--- + +## v1.3.4 + +### Features + +- update clash and clash meta core +- optimize traffic graph high CPU usage when window hidden +- use polkit to elevate permission (Linux) +- support app log level setting +- support copy environment variable +- overwrite resource file according to file modified +- save window size and position + +### Bug Fixes + +- remove fallback group select status +- enable context menu on editable element (Windows) + +--- + +## v1.3.3 + +### Features + +- update clash and clash meta core +- show tray icon variants in different system proxy status (Windows) +- close all connections when mode changed + +### Bug Fixes + +- encode controller secret into uri +- error boundary for each page + +--- + +## v1.3.2 + +### Features + +- update clash and clash meta core + +### Bug Fixes + +- fix import url issue +- fix profile undefined issue + +--- + +## v1.3.1 + +### Features + +- update clash and clash meta core + +### Bug Fixes + +- fix open url issue +- fix appimage path panic +- fix grant root permission in macOS +- fix linux system proxy default bypass + +--- + +## v1.3.0 + +### Features + +- update clash and clash meta +- support opening dir on tray +- support updating all profiles with one click +- support granting root permission to clash core(Linux, macOS) +- support enable/disable clash fields filter, feel free to experience the latest features of Clash Meta + +### Bug Fixes + +- deb add openssl depend(Linux) +- fix the AppImage auto launch path(Linux) +- fix get the default network service(macOS) +- remove the esc key listener in macOS, cmd+w instead(macOS) +- fix infinite retry when websocket error + +--- + +## v1.2.3 + +### Features + +- update clash +- adjust macOS window style +- profile supports UTF8 with BOM + +### Bug Fixes + +- fix selected proxy +- fix error log + +--- + +## v1.2.2 + +### Features + +- update clash meta +- recover clash core after panic +- use system window decorations(Linux) + +### Bug Fixes + +- flush system proxy settings(Windows) +- fix parse log panic +- fix ui bug + +--- + +## v1.2.1 + +### Features + +- update clash version +- proxy groups support multi columns +- optimize ui + +### Bug Fixes + +- fix ui websocket connection +- adjust delay check concurrency +- avoid setting login item repeatedly(macOS) + +--- + +## v1.2.0 + +### Features + +- update clash meta version +- support to change external-controller +- support to change default latency test URL +- close all connections when proxy changed or profile changed +- check the config by using the core +- increase the robustness of the program +- optimize windows service mode (need to reinstall) +- optimize ui + +### Bug Fixes + +- invalid hotkey cause panic +- invalid theme setting cause panic +- fix some other glitches + +--- + +## v1.1.2 + +### Features + +- the system tray follows i18n +- change the proxy group ui of global mode +- support to update profile with the system proxy/clash proxy +- check the remote profile more strictly + +### Bug Fixes + +- use app version as default user agent +- the clash not exit in service mode +- reset the system proxy when quit the app +- fix some other glitches + +--- + +## v1.1.1 + +### Features + +- optimize clash config feedback +- hide macOS dock icon +- use clash meta compatible version (Linux) + +### Bug Fixes + +- fix some other glitches + +--- + +## v1.1.0 + +### Features + +- add rule page +- supports proxy providers delay check +- add proxy delay check loading status +- supports hotkey/shortcut management +- supports displaying connections data in table layout(refer to yacd) + +### Bug Fixes + +- supports yaml merge key in clash config +- detect the network interface and set the system proxy(macOS) +- fix some other glitches + +--- + +## v1.0.6 + +### Features + +- update clash and clash.meta + +### Bug Fixes + +- only script profile display console +- automatic configuration update on demand at launch + +--- + +## v1.0.5 + +### Features + +- reimplement profile enhanced mode with quick-js +- optimize the runtime config generation process +- support web ui management +- support clash field management +- support viewing the runtime config +- adjust some pages style + +### Bug Fixes + +- fix silent start +- fix incorrectly reset system proxy on exit + +--- + +## v1.0.4 + +### Features + +- update clash core and clash meta version +- support switch clash mode on system tray +- theme mode support follows system + +### Bug Fixes + +- config load error on first use + +--- + +## v1.0.3 + +### Features + +- save some states such as URL test, filter, etc +- update clash core and clash-meta core +- new icon for macOS + +--- + +## v1.0.2 + +### Features + +- supports for switching clash core +- supports release UI processes +- supports script mode setting + +### Bug Fixes + +- fix service mode bug (Windows) + +--- + +## v1.0.1 + +### Features + +- adjust default theme settings +- reduce gpu usage of traffic graph when hidden +- supports more remote profile response header setting +- check remote profile data format when imported + +### Bug Fixes + +- service mode install and start issue (Windows) +- fix launch panic (Some Windows) + +--- + +## v1.0.0 + +### Features + +- update clash core +- optimize traffic graph animation +- supports interval update profiles +- supports service mode (Windows) + +### Bug Fixes + +- reset system proxy when exit from dock (macOS) +- adjust clash dns config process strategy + +--- + +## v0.0.29 + +### Features + +- sort proxy node +- custom proxy test url +- logs page filter +- connections page filter +- default user agent for subscription +- system tray add tun mode toggle +- enable to change the config dir (Windows only) + +--- + +## v0.0.28 + +### Features + +- enable to use clash config fields (UI) + +### Bug Fixes + +- remove the character +- fix some icon color + +--- + +## v0.0.27 + +### Features + +- supports custom theme color +- tun mode setting control the final config + +### Bug Fixes + +- fix transition flickers (macOS) +- reduce proxy page render + +--- + +## v0.0.26 + +### Features + +- silent start +- profile editor +- profile enhance mode supports more fields +- optimize profile enhance mode strategy + +### Bug Fixes + +- fix csp restriction on macOS +- window controllers on Linux + +--- + +## v0.0.25 + +### Features + +- update clash core version + +### Bug Fixes + +- app updater error +- display window controllers on Linux + +### Notes + +If you can't update the app properly, please consider downloading the latest version from github release. + +--- + +## v0.0.24 + +### Features + +- Connections page +- add wintun.dll (Windows) +- supports create local profile with selected file (Windows) +- system tray enable set system proxy + +### Bug Fixes + +- open dir error +- auto launch path (Windows) +- fix some clash config error +- reduce the impact of the enhanced mode + +--- + +## v0.0.23 + +### Features + +- i18n supports +- Remote profile User Agent supports + +### Bug Fixes + +- clash config file case ignore +- clash `external-controller` only port diff --git a/docs/color1.png b/docs/color1.png new file mode 100644 index 0000000..9c24fb8 Binary files /dev/null and b/docs/color1.png differ diff --git a/docs/color2.png b/docs/color2.png new file mode 100644 index 0000000..8d6d935 Binary files /dev/null and b/docs/color2.png differ diff --git a/docs/color3.png b/docs/color3.png new file mode 100644 index 0000000..247b841 Binary files /dev/null and b/docs/color3.png differ diff --git a/docs/color4.png b/docs/color4.png new file mode 100644 index 0000000..3ecb750 Binary files /dev/null and b/docs/color4.png differ diff --git a/docs/color5.png b/docs/color5.png new file mode 100644 index 0000000..65e916a Binary files /dev/null and b/docs/color5.png differ diff --git a/docs/color6.png b/docs/color6.png new file mode 100644 index 0000000..5067320 Binary files /dev/null and b/docs/color6.png differ diff --git a/docs/demo1.png b/docs/demo1.png new file mode 100644 index 0000000..b18450f Binary files /dev/null and b/docs/demo1.png differ diff --git a/docs/demo2.png b/docs/demo2.png new file mode 100644 index 0000000..a2919b6 Binary files /dev/null and b/docs/demo2.png differ diff --git a/docs/demo3.png b/docs/demo3.png new file mode 100644 index 0000000..55267b5 Binary files /dev/null and b/docs/demo3.png differ diff --git a/docs/demo4.png b/docs/demo4.png new file mode 100644 index 0000000..ef9b90a Binary files /dev/null and b/docs/demo4.png differ diff --git a/docs/demo5.png b/docs/demo5.png new file mode 100644 index 0000000..a7eac54 Binary files /dev/null and b/docs/demo5.png differ diff --git a/docs/demo6.png b/docs/demo6.png new file mode 100644 index 0000000..fe53e2b Binary files /dev/null and b/docs/demo6.png differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..8249427 --- /dev/null +++ b/package.json @@ -0,0 +1,74 @@ +{ + "name": "clash-verge", + "version": "1.3.8", + "license": "GPL-3.0", + "scripts": { + "dev": "tauri dev", + "dev:diff": "tauri dev -f verge-dev", + "build": "tauri build", + "tauri": "tauri", + "web:dev": "vite", + "web:build": "tsc && vite build", + "web:serve": "vite preview", + "aarch": "node scripts/aarch.mjs", + "check": "node scripts/check.mjs", + "updater": "node scripts/updater.mjs", + "publish": "node scripts/publish.mjs", + "portable": "node scripts/portable.mjs", + "prepare": "husky install" + }, + "dependencies": { + "@emotion/react": "^11.10.5", + "@emotion/styled": "^11.10.5", + "@juggle/resize-observer": "^3.4.0", + "@mui/icons-material": "^5.10.9", + "@mui/material": "^5.10.13", + "@mui/x-data-grid": "^5.17.11", + "@tauri-apps/api": "^1.3.0", + "ahooks": "^3.7.2", + "axios": "^1.1.3", + "dayjs": "1.11.5", + "i18next": "^22.0.4", + "lodash-es": "^4.17.21", + "monaco-editor": "^0.34.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-error-boundary": "^3.1.4", + "react-hook-form": "^7.39.5", + "react-i18next": "^12.0.0", + "react-router-dom": "^6.4.3", + "react-virtuoso": "^3.1.3", + "recoil": "^0.7.6", + "snarkdown": "^2.0.0", + "swr": "^1.3.0" + }, + "devDependencies": { + "@actions/github": "^5.0.3", + "@tauri-apps/cli": "^1.3.1", + "@types/fs-extra": "^9.0.13", + "@types/js-cookie": "^3.0.2", + "@types/lodash": "^4.14.180", + "@types/lodash-es": "^4.17.7", + "@types/react-dom": "^18.0.11", + "@vitejs/plugin-react": "^2.0.1", + "adm-zip": "^0.5.9", + "cross-env": "^7.0.3", + "fs-extra": "^10.0.0", + "https-proxy-agent": "^5.0.1", + "husky": "^7.0.0", + "node-fetch": "^3.2.6", + "prettier": "^2.7.1", + "pretty-quick": "^3.1.3", + "sass": "^1.54.0", + "typescript": "^4.7.4", + "vite": "^3.2.5", + "vite-plugin-monaco-editor": "^1.1.0", + "vite-plugin-svgr": "^2.2.1" + }, + "prettier": { + "tabWidth": 2, + "semi": true, + "singleQuote": false, + "endOfLine": "lf" + } +} diff --git a/scripts/aarch.mjs b/scripts/aarch.mjs new file mode 100644 index 0000000..989e774 --- /dev/null +++ b/scripts/aarch.mjs @@ -0,0 +1,119 @@ +/** + * Build and upload assets + * for macOS(aarch) + */ +import fs from "fs-extra"; +import path from "path"; +import { exit } from "process"; +import { execSync } from "child_process"; +import { createRequire } from "module"; +import { getOctokit, context } from "@actions/github"; + +// to `meta` tag +const META = process.argv.includes("--meta"); +// to `alpha` tag +const ALPHA = process.argv.includes("--alpha"); + +const require = createRequire(import.meta.url); + +async function resolve() { + if (!process.env.GITHUB_TOKEN) { + throw new Error("GITHUB_TOKEN is required"); + } + if (!process.env.GITHUB_REPOSITORY) { + throw new Error("GITHUB_REPOSITORY is required"); + } + if (!process.env.TAURI_PRIVATE_KEY) { + throw new Error("TAURI_PRIVATE_KEY is required"); + } + if (!process.env.TAURI_KEY_PASSWORD) { + throw new Error("TAURI_KEY_PASSWORD is required"); + } + + const { version } = require("../package.json"); + + const tag = META ? "meta" : ALPHA ? "alpha" : `v${version}`; + const buildCmd = META ? `yarn build -f default-meta` : `yarn build`; + + console.log(`[INFO]: Upload to tag "${tag}"`); + console.log(`[INFO]: Building app. "${buildCmd}"`); + + execSync(buildCmd); + + const cwd = process.cwd(); + const bundlePath = path.join(cwd, "src-tauri/target/release/bundle"); + const join = (p) => path.join(bundlePath, p); + + const appPathList = [ + join("macos/Clash Verge.aarch64.app.tar.gz"), + join("macos/Clash Verge.aarch64.app.tar.gz.sig"), + ]; + + for (const appPath of appPathList) { + if (fs.pathExistsSync(appPath)) { + fs.removeSync(appPath); + } + } + + fs.copyFileSync(join("macos/Clash Verge.app.tar.gz"), appPathList[0]); + fs.copyFileSync(join("macos/Clash Verge.app.tar.gz.sig"), appPathList[1]); + + const options = { owner: context.repo.owner, repo: context.repo.repo }; + const github = getOctokit(process.env.GITHUB_TOKEN); + + const { data: release } = await github.rest.repos.getReleaseByTag({ + ...options, + tag, + }); + + if (!release.id) throw new Error("failed to find the release"); + + await uploadAssets(release.id, [ + join(`dmg/Clash Verge_${version}_aarch64.dmg`), + ...appPathList, + ]); +} + +// From tauri-apps/tauri-action +// https://github.com/tauri-apps/tauri-action/blob/dev/packages/action/src/upload-release-assets.ts +async function uploadAssets(releaseId, assets) { + const github = getOctokit(process.env.GITHUB_TOKEN); + + // Determine content-length for header to upload asset + const contentLength = (filePath) => fs.statSync(filePath).size; + + for (const assetPath of assets) { + const headers = { + "content-type": "application/zip", + "content-length": contentLength(assetPath), + }; + + const ext = path.extname(assetPath); + const filename = path.basename(assetPath).replace(ext, ""); + const assetName = path.dirname(assetPath).includes(`target${path.sep}debug`) + ? `${filename}-debug${ext}` + : `${filename}${ext}`; + + console.log(`[INFO]: Uploading ${assetName}...`); + + try { + await github.rest.repos.uploadReleaseAsset({ + headers, + name: assetName, + data: fs.readFileSync(assetPath), + owner: context.repo.owner, + repo: context.repo.repo, + release_id: releaseId, + }); + } catch (error) { + console.log(error.message); + } + } +} + +if (process.platform === "darwin" && process.arch === "arm64") { + resolve(); +} else { + console.error("invalid"); + exit(1); +} diff --git a/scripts/check.mjs b/scripts/check.mjs new file mode 100644 index 0000000..50edc0e --- /dev/null +++ b/scripts/check.mjs @@ -0,0 +1,332 @@ +import fs from "fs-extra"; +import zlib from "zlib"; +import path from "path"; +import AdmZip from "adm-zip"; +import fetch from "node-fetch"; +import proxyAgent from "https-proxy-agent"; +import { execSync } from "child_process"; + +const cwd = process.cwd(); +const TEMP_DIR = path.join(cwd, "node_modules/.verge"); +const FORCE = process.argv.includes("--force"); + +const SIDECAR_HOST = execSync("rustc -vV") + .toString() + .match(/(?<=host: ).+(?=\s*)/g)[0]; + +/* ======= clash ======= */ +const CLASH_STORAGE_PREFIX = "https://release.dreamacro.workers.dev/"; +const CLASH_URL_PREFIX = + "https://github.com/Dreamacro/clash/releases/download/premium/"; +const CLASH_LATEST_DATE = "latest"; + +const CLASH_MAP = { + "win32-x64": "clash-windows-amd64", + "darwin-x64": "clash-darwin-amd64", + "darwin-arm64": "clash-darwin-arm64", + "linux-x64": "clash-linux-amd64", + "linux-arm64": "clash-linux-arm64", +}; + +/* ======= clash meta ======= */ +const META_URL_PREFIX = `https://github.com/MetaCubeX/Clash.Meta/releases/download/`; +const META_VERSION = "v1.16.0"; + +const META_MAP = { + "win32-x64": "clash.meta-windows-amd64-compatible", + "darwin-x64": "clash.meta-darwin-amd64", + "darwin-arm64": "clash.meta-darwin-arm64", + "linux-x64": "clash.meta-linux-amd64-compatible", + "linux-arm64": "clash.meta-linux-arm64", +}; + +/** + * check available + */ + +const { platform, arch } = process; +if (!CLASH_MAP[`${platform}-${arch}`]) { + throw new Error(`clash unsupported platform "${platform}-${arch}"`); +} +if (!META_MAP[`${platform}-${arch}`]) { + throw new Error(`clash meta unsupported platform "${platform}-${arch}"`); +} + +function clash() { + const name = CLASH_MAP[`${platform}-${arch}`]; + + const isWin = platform === "win32"; + const urlExt = isWin ? "zip" : "gz"; + const downloadURL = `${CLASH_URL_PREFIX}${name}-${CLASH_LATEST_DATE}.${urlExt}`; + const exeFile = `${name}${isWin ? ".exe" : ""}`; + const zipFile = `${name}.${urlExt}`; + + return { + name: "clash", + targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`, + exeFile, + zipFile, + downloadURL, + }; +} + +function clashS3() { + const name = CLASH_MAP[`${platform}-${arch}`]; + + const isWin = platform === "win32"; + const urlExt = isWin ? "zip" : "gz"; + const downloadURL = `${CLASH_STORAGE_PREFIX}${CLASH_LATEST_DATE}/${name}-${CLASH_LATEST_DATE}.${urlExt}`; + const exeFile = `${name}${isWin ? ".exe" : ""}`; + const zipFile = `${name}.${urlExt}`; + + return { + name: "clash", + targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`, + exeFile, + zipFile, + downloadURL, + }; +} + +function clashMeta() { + const name = META_MAP[`${platform}-${arch}`]; + const isWin = platform === "win32"; + const urlExt = isWin ? "zip" : "gz"; + const downloadURL = `${META_URL_PREFIX}${META_VERSION}/${name}-${META_VERSION}.${urlExt}`; + const exeFile = `${name}${isWin ? ".exe" : ""}`; + const zipFile = `${name}-${META_VERSION}.${urlExt}`; + + return { + name: "clash-meta", + targetFile: `clash-meta-${SIDECAR_HOST}${isWin ? ".exe" : ""}`, + exeFile, + zipFile, + downloadURL, + }; +} + +/** + * download sidecar and rename + */ +async function resolveSidecar(binInfo) { + const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo; + + const sidecarDir = path.join(cwd, "src-tauri", "sidecar"); + const sidecarPath = path.join(sidecarDir, targetFile); + + await fs.mkdirp(sidecarDir); + if (!FORCE && (await fs.pathExists(sidecarPath))) return; + + const tempDir = path.join(TEMP_DIR, name); + const tempZip = path.join(tempDir, zipFile); + const tempExe = path.join(tempDir, exeFile); + + await fs.mkdirp(tempDir); + try { + if (!(await fs.pathExists(tempZip))) { + await downloadFile(downloadURL, tempZip); + } + + if (zipFile.endsWith(".zip")) { + const zip = new AdmZip(tempZip); + zip.getEntries().forEach((entry) => { + console.log(`[DEBUG]: "${name}" entry name`, entry.entryName); + }); + zip.extractAllTo(tempDir, true); + await fs.rename(tempExe, sidecarPath); + console.log(`[INFO]: "${name}" unzip finished`); + } else { + // gz + const readStream = fs.createReadStream(tempZip); + const writeStream = fs.createWriteStream(sidecarPath); + await new Promise((resolve, reject) => { + const onError = (error) => { + console.error(`[ERROR]: "${name}" gz failed:`, error.message); + reject(error); + }; + readStream + .pipe(zlib.createGunzip().on("error", onError)) + .pipe(writeStream) + .on("finish", () => { + console.log(`[INFO]: "${name}" gunzip finished`); + execSync(`chmod 755 ${sidecarPath}`); + console.log(`[INFO]: "${name}" chmod binary finished`); + resolve(); + }) + .on("error", onError); + }); + } + } catch (err) { + // 需要删除文件 + await fs.remove(sidecarPath); + throw err; + } finally { + // delete temp dir + await fs.remove(tempDir); + } +} + +/** + * prepare clash core + * if the core version is not updated in time, use S3 storage as a backup. + */ +async function resolveClash() { + try { + return await resolveSidecar(clash()); + } catch { + console.log(`[WARN]: clash core needs to be updated`); + return await resolveSidecar(clashS3()); + } +} + +/** + * only Windows + * get the wintun.dll (not required) + */ +async function resolveWintun() { + const { platform } = process; + + if (platform !== "win32") return; + + const url = "https://www.wintun.net/builds/wintun-0.14.1.zip"; + + const tempDir = path.join(TEMP_DIR, "wintun"); + const tempZip = path.join(tempDir, "wintun.zip"); + + const wintunPath = path.join(tempDir, "wintun/bin/amd64/wintun.dll"); + const targetPath = path.join(cwd, "src-tauri/resources", "wintun.dll"); + + if (!FORCE && (await fs.pathExists(targetPath))) return; + + await fs.mkdirp(tempDir); + + if (!(await fs.pathExists(tempZip))) { + await downloadFile(url, tempZip); + } + + // unzip + const zip = new AdmZip(tempZip); + zip.extractAllTo(tempDir, true); + + if (!(await fs.pathExists(wintunPath))) { + throw new Error(`path not found "${wintunPath}"`); + } + + await fs.rename(wintunPath, targetPath); + await fs.remove(tempDir); + + console.log(`[INFO]: resolve wintun.dll finished`); +} + +/** + * download the file to the resources dir + */ +async function resolveResource(binInfo) { + const { file, downloadURL } = binInfo; + + const resDir = path.join(cwd, "src-tauri/resources"); + const targetPath = path.join(resDir, file); + + if (!FORCE && (await fs.pathExists(targetPath))) return; + + await fs.mkdirp(resDir); + await downloadFile(downloadURL, targetPath); + + console.log(`[INFO]: ${file} finished`); +} + +/** + * download file and save to `path` + */ +async function downloadFile(url, path) { + const options = {}; + + const httpProxy = + process.env.HTTP_PROXY || + process.env.http_proxy || + process.env.HTTPS_PROXY || + process.env.https_proxy; + + if (httpProxy) { + options.agent = proxyAgent(httpProxy); + } + + const response = await fetch(url, { + ...options, + method: "GET", + headers: { "Content-Type": "application/octet-stream" }, + }); + const buffer = await response.arrayBuffer(); + await fs.writeFile(path, new Uint8Array(buffer)); + + console.log(`[INFO]: download finished "${url}"`); +} + +/** + * main + */ +const SERVICE_URL = + "https://github.com/zzzgydi/clash-verge-service/releases/download/latest"; + +const resolveService = () => + resolveResource({ + file: "clash-verge-service.exe", + downloadURL: `${SERVICE_URL}/clash-verge-service.exe`, + }); +const resolveInstall = () => + resolveResource({ + file: "install-service.exe", + downloadURL: `${SERVICE_URL}/install-service.exe`, + }); +const resolveUninstall = () => + resolveResource({ + file: "uninstall-service.exe", + downloadURL: `${SERVICE_URL}/uninstall-service.exe`, + }); +const resolveMmdb = () => + resolveResource({ + file: "Country.mmdb", + downloadURL: `https://github.com/Dreamacro/maxmind-geoip/releases/download/20230812/Country.mmdb`, + }); +const resolveGeosite = () => + resolveResource({ + file: "geosite.dat", + downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat`, + }); +const resolveGeoIP = () => + resolveResource({ + file: "geoip.dat", + downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat`, + }); + +const tasks = [ + { name: "clash", func: () => resolveSidecar(clashS3()), retry: 5 }, + { name: "clash-meta", func: () => resolveSidecar(clashMeta()), retry: 5 }, + { name: "wintun", func: resolveWintun, retry: 5, winOnly: true }, + { name: "service", func: resolveService, retry: 5, winOnly: true }, + { name: "install", func: resolveInstall, retry: 5, winOnly: true }, + { name: "uninstall", func: resolveUninstall, retry: 5, winOnly: true }, + { name: "mmdb", func: resolveMmdb, retry: 5 }, + { name: "geosite", func: resolveGeosite, retry: 5 }, + { name: "geoip", func: resolveGeoIP, retry: 5 }, +]; + +async function runTask() { + const task = tasks.shift(); + if (!task) return; + if (task.winOnly && process.platform !== "win32") return runTask(); + + for (let i = 0; i < task.retry; i++) { + try { + await task.func(); + break; + } catch (err) { + console.error(`[ERROR]: task::${task.name} try ${i} ==`, err.message); + if (i === task.retry - 1) throw err; + } + } + return runTask(); +} + +runTask(); +runTask(); diff --git a/scripts/portable.mjs b/scripts/portable.mjs new file mode 100644 index 0000000..b523a50 --- /dev/null +++ b/scripts/portable.mjs @@ -0,0 +1,59 @@ +import fs from "fs-extra"; +import path from "path"; +import AdmZip from "adm-zip"; +import { createRequire } from "module"; +import { getOctokit, context } from "@actions/github"; + +/// Script for ci +/// 打包绿色版/便携版 (only Windows) +async function resolvePortable() { + if (process.platform !== "win32") return; + + const releaseDir = "./src-tauri/target/release"; + + if (!(await fs.pathExists(releaseDir))) { + throw new Error("could not found the release dir"); + } + + const zip = new AdmZip(); + + zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe")); + zip.addLocalFile(path.join(releaseDir, "clash.exe")); + zip.addLocalFile(path.join(releaseDir, "clash-meta.exe")); + zip.addLocalFolder(path.join(releaseDir, "resources"), "resources"); + + const require = createRequire(import.meta.url); + const packageJson = require("../package.json"); + const { version } = packageJson; + + const zipFile = `Clash.Verge_${version}_x64_portable.zip`; + zip.writeZip(zipFile); + + console.log("[INFO]: create portable zip successfully"); + + // push release assets + if (process.env.GITHUB_TOKEN === undefined) { + throw new Error("GITHUB_TOKEN is required"); + } + + const options = { owner: context.repo.owner, repo: context.repo.repo }; + const github = getOctokit(process.env.GITHUB_TOKEN); + + console.log("[INFO]: upload to ", process.env.TAG_NAME || `v${version}`); + + const { data: release } = await github.rest.repos.getReleaseByTag({ + ...options, + tag: process.env.TAG_NAME || `v${version}`, + }); + + console.log(release.name); + + await github.rest.repos.uploadReleaseAsset({ + ...options, + release_id: release.id, + name: zipFile, + data: zip.toBuffer(), + }); +} + +resolvePortable().catch(console.error); diff --git a/scripts/publish.mjs b/scripts/publish.mjs new file mode 100644 index 0000000..523ad18 --- /dev/null +++ b/scripts/publish.mjs @@ -0,0 +1,53 @@ +import fs from "fs-extra"; +import { createRequire } from "module"; +import { execSync } from "child_process"; +import { resolveUpdateLog } from "./updatelog.mjs"; + +const require = createRequire(import.meta.url); + +// publish +async function resolvePublish() { + const flag = process.argv[2] ?? "patch"; + const packageJson = require("../package.json"); + const tauriJson = require("../src-tauri/tauri.conf.json"); + + let [a, b, c] = packageJson.version.split(".").map(Number); + + if (flag === "major") { + a += 1; + b = 0; + c = 0; + } else if (flag === "minor") { + b += 1; + c = 0; + } else if (flag === "patch") { + c += 1; + } else throw new Error(`invalid flag "${flag}"`); + + const nextVersion = `${a}.${b}.${c}`; + packageJson.version = nextVersion; + tauriJson.package.version = nextVersion; + + // 发布更新前先写更新日志 + const nextTag = `v${nextVersion}`; + await resolveUpdateLog(nextTag); + + await fs.writeFile( + "./package.json", + JSON.stringify(packageJson, undefined, 2) + ); + await fs.writeFile( + "./src-tauri/tauri.conf.json", + JSON.stringify(tauriJson, undefined, 2) + ); + + execSync("git add ./package.json"); + execSync("git add ./src-tauri/tauri.conf.json"); + execSync(`git commit -m "v${nextVersion}"`); + execSync(`git tag -a v${nextVersion} -m "v${nextVersion}"`); + execSync(`git push`); + execSync(`git push origin v${nextVersion}`); + console.log(`Publish Successfully...`); +} + +resolvePublish(); diff --git a/scripts/updatelog.mjs b/scripts/updatelog.mjs new file mode 100644 index 0000000..fae7f62 --- /dev/null +++ b/scripts/updatelog.mjs @@ -0,0 +1,44 @@ +import fs from "fs-extra"; +import path from "path"; + +const UPDATE_LOG = "UPDATELOG.md"; + +// parse the UPDATELOG.md +export async function resolveUpdateLog(tag) { + const cwd = process.cwd(); + + const reTitle = /^## v[\d\.]+/; + const reEnd = /^---/; + + const file = path.join(cwd, UPDATE_LOG); + + if (!(await fs.pathExists(file))) { + throw new Error("could not found UPDATELOG.md"); + } + + const data = await fs.readFile(file).then((d) => d.toString("utf8")); + + const map = {}; + let p = ""; + + data.split("\n").forEach((line) => { + if (reTitle.test(line)) { + p = line.slice(3).trim(); + if (!map[p]) { + map[p] = []; + } else { + throw new Error(`Tag ${p} dup`); + } + } else if (reEnd.test(line)) { + p = ""; + } else if (p) { + map[p].push(line); + } + }); + + if (!map[tag]) { + throw new Error(`could not found "${tag}" in UPDATELOG.md`); + } + + return map[tag].join("\n").trim(); +} diff --git a/scripts/updater.mjs b/scripts/updater.mjs new file mode 100644 index 0000000..2a3a8ee --- /dev/null +++ b/scripts/updater.mjs @@ -0,0 +1,177 @@ +import fetch from "node-fetch"; +import { getOctokit, context } from "@actions/github"; +import { resolveUpdateLog } from "./updatelog.mjs"; + +const UPDATE_TAG_NAME = "updater"; +const UPDATE_JSON_FILE = "update.json"; +const UPDATE_JSON_PROXY = "update-proxy.json"; + +/// generate update.json +/// upload to update tag's release asset +async function resolveUpdater() { + if (process.env.GITHUB_TOKEN === undefined) { + throw new Error("GITHUB_TOKEN is required"); + } + + const options = { owner: context.repo.owner, repo: context.repo.repo }; + const github = getOctokit(process.env.GITHUB_TOKEN); + + const { data: tags } = await github.rest.repos.listTags({ + ...options, + per_page: 10, + page: 1, + }); + + // get the latest publish tag + const tag = tags.find((t) => t.name.startsWith("v")); + + console.log(tag); + console.log(); + + const { data: latestRelease } = await github.rest.repos.getReleaseByTag({ + ...options, + tag: tag.name, + }); + + const updateData = { + name: tag.name, + notes: await resolveUpdateLog(tag.name), // use updatelog.md + pub_date: new Date().toISOString(), + platforms: { + win64: { signature: "", url: "" }, // compatible with older formats + linux: { signature: "", url: "" }, // compatible with older formats + darwin: { signature: "", url: "" }, // compatible with older formats + "darwin-aarch64": { signature: "", url: "" }, + "darwin-intel": { signature: "", url: "" }, + "darwin-x86_64": { signature: "", url: "" }, + "linux-x86_64": { signature: "", url: "" }, + "windows-x86_64": { signature: "", url: "" }, + "windows-i686": { signature: "", url: "" }, // no supported + }, + }; + + const promises = latestRelease.assets.map(async (asset) => { + const { name, browser_download_url } = asset; + + // win64 url + if (name.endsWith(".msi.zip") && name.includes("en-US")) { + updateData.platforms.win64.url = browser_download_url; + updateData.platforms["windows-x86_64"].url = browser_download_url; + } + // win64 signature + if (name.endsWith(".msi.zip.sig") && name.includes("en-US")) { + const sig = await getSignature(browser_download_url); + updateData.platforms.win64.signature = sig; + updateData.platforms["windows-x86_64"].signature = sig; + } + + // darwin url (intel) + if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) { + updateData.platforms.darwin.url = browser_download_url; + updateData.platforms["darwin-intel"].url = browser_download_url; + updateData.platforms["darwin-x86_64"].url = browser_download_url; + } + // darwin signature (intel) + if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) { + const sig = await getSignature(browser_download_url); + updateData.platforms.darwin.signature = sig; + updateData.platforms["darwin-intel"].signature = sig; + updateData.platforms["darwin-x86_64"].signature = sig; + } + + // darwin url (aarch) + if (name.endsWith("aarch64.app.tar.gz")) { + updateData.platforms["darwin-aarch64"].url = browser_download_url; + } + // darwin signature (aarch) + if (name.endsWith("aarch64.app.tar.gz.sig")) { + const sig = await getSignature(browser_download_url); + updateData.platforms["darwin-aarch64"].signature = sig; + } + + // linux url + if (name.endsWith(".AppImage.tar.gz")) { + updateData.platforms.linux.url = browser_download_url; + updateData.platforms["linux-x86_64"].url = browser_download_url; + } + // linux signature + if (name.endsWith(".AppImage.tar.gz.sig")) { + const sig = await getSignature(browser_download_url); + updateData.platforms.linux.signature = sig; + updateData.platforms["linux-x86_64"].signature = sig; + } + }); + + await Promise.allSettled(promises); + console.log(updateData); + + // maybe should test the signature as well + // delete the null field + Object.entries(updateData.platforms).forEach(([key, value]) => { + if (!value.url) { + console.log(`[Error]: failed to parse release for "${key}"`); + delete updateData.platforms[key]; + } + }); + + // 生成一个代理github的更新文件 + // 使用 https://hub.fastgit.xyz/ 做github资源的加速 + const updateDataNew = JSON.parse(JSON.stringify(updateData)); + + Object.entries(updateDataNew.platforms).forEach(([key, value]) => { + if (value.url) { + updateDataNew.platforms[key].url = "https://ghproxy.com/" + value.url; + } else { + console.log(`[Error]: updateDataNew.platforms.${key} is null`); + } + }); + + // update the update.json + const { data: updateRelease } = await github.rest.repos.getReleaseByTag({ + ...options, + tag: UPDATE_TAG_NAME, + }); + + // delete the old assets + for (let asset of updateRelease.assets) { + if (asset.name === UPDATE_JSON_FILE) { + await github.rest.repos.deleteReleaseAsset({ + ...options, + asset_id: asset.id, + }); + } + + if (asset.name === UPDATE_JSON_PROXY) { + await github.rest.repos + .deleteReleaseAsset({ ...options, asset_id: asset.id }) + .catch(console.error); // do not break the pipeline + } + } + + // upload new assets + await github.rest.repos.uploadReleaseAsset({ + ...options, + release_id: updateRelease.id, + name: UPDATE_JSON_FILE, + data: JSON.stringify(updateData, null, 2), + }); + + await github.rest.repos.uploadReleaseAsset({ + ...options, + release_id: updateRelease.id, + name: UPDATE_JSON_PROXY, + data: JSON.stringify(updateDataNew, null, 2), + }); +} + +// get the signature file content +async function getSignature(url) { + const response = await fetch(url, { + method: "GET", + headers: { "Content-Type": "application/octet-stream" }, + }); + + return response.text(); +} + +resolveUpdater().catch(console.error); diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore new file mode 100644 index 0000000..7b128ff --- /dev/null +++ b/src-tauri/.gitignore @@ -0,0 +1,6 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +WixTools +resources +sidecar diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..9b1c0ec --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,5658 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.9", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" +dependencies = [ + "memchr", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "aho-corasick" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + +[[package]] +name = "async-channel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock", + "autocfg", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log 0.4.17", + "parking", + "polling", + "rustix", + "slab", + "socket2", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-net" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e67316bc7eff608fe723df5d32ed639946adcd69e07df41fd42a7b411f1f" +dependencies = [ + "async-io", + "autocfg", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +dependencies = [ + "async-io", + "async-lock", + "autocfg", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", + "signal-hook 0.3.15", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-task" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" + +[[package]] +name = "async-trait" +version = "0.1.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "atk" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +dependencies = [ + "atk-sys", + "bitflags", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.1.0", +] + +[[package]] +name = "atomic-waker" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" + +[[package]] +name = "attohttpc" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fcf00bc6d5abb29b5f97e3c61a90b6d3caa12f3faf897d4a3e3607c050a35a7" +dependencies = [ + "flate2", + "http", + "log 0.4.17", + "native-tls", + "serde", + "serde_json", + "serde_urlencoded", + "url", +] + +[[package]] +name = "auto-launch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" +dependencies = [ + "dirs 4.0.0", + "thiserror", + "winreg 0.10.1", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "log 0.4.17", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.1.0", +] + +[[package]] +name = "cargo_toml" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497049e9477329f8f6a559972ee42e117487d01d1e8c2cc9f836ea6fa23a9e1a" +dependencies = [ + "serde", + "toml 0.5.11", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3431df59f28accaf4cb4eed4a9acc66bea3f3c3753aa6cdc2f024174ef232af7" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-expr" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8790cf1286da485c72cf5fc7aeba308438800036ec67d89425924c4807268c9" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "serde", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "clash-verge" +version = "0.1.0" +dependencies = [ + "anyhow", + "auto-launch", + "chrono", + "ctrlc", + "deelevate", + "delay_timer", + "dirs 5.0.1", + "dunce", + "log 0.4.17", + "log4rs", + "nanoid", + "once_cell", + "open 4.1.0", + "parking_lot", + "port_scanner", + "reqwest", + "rquickjs", + "runas", + "serde", + "serde_json", + "serde_yaml 0.9.21", + "sysinfo", + "sysproxy", + "tauri", + "tauri-build", + "tokio", + "warp", + "which", + "window-shadows", + "window-vibrancy", + "windows-sys 0.48.0", + "winreg 0.50.0", + "wry", +] + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6" +dependencies = [ + "bitflags", + "block", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concat-idents" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe0e1d9f7de897d18e590a7496b5facbe87813f746cf4b8db596ba77e07e832" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" +dependencies = [ + "bitflags", + "core-foundation", + "foreign-types", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cron_clock" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8699d8ed16e3db689f8ae04d8dc3c6666a4ba7e724e5a157884b7cc385d16b" +dependencies = [ + "chrono", + "nom 7.1.3", + "once_cell", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.8.0", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ctrlc" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7394a21d012ce5c850497fb774b167d81b99f060025fbf06ee92b9848bd97eb2" +dependencies = [ + "nix 0.26.2", + "windows-sys 0.48.0", +] + +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + +[[package]] +name = "darling" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.18", +] + +[[package]] +name = "darling_macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "dashmap" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" +dependencies = [ + "cfg-if", + "num_cpus", +] + +[[package]] +name = "deelevate" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7397f8c48906dd9b5afc75001368c979418e5dff5575998a831eb2319b424e" +dependencies = [ + "lazy_static 1.4.0", + "pathsearch", + "rand 0.8.5", + "shared_library", + "termwiz", + "winapi", +] + +[[package]] +name = "delay_timer" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e3040b73d9397711697558109c983a2dc6fc63e98785ffbefd3ece57b46b67" +dependencies = [ + "anyhow", + "async-trait", + "autocfg", + "concat-idents", + "cron_clock", + "dashmap", + "event-listener", + "futures", + "log 0.4.17", + "lru", + "once_cell", + "rs-snowflake", + "rustc_version 0.2.3", + "smol", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.4.0", + "syn 1.0.109", +] + +[[package]] +name = "destructure_traitobject" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "dtoa-short" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03329ae10e79ede66c9ce4dc930aa8599043b0743008548680f25b91502d6" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cf3a800ff6e860c863ca6d4b16fd999db8b752819c1606884047b73e468535" +dependencies = [ + "memoffset 0.8.0", + "rustc_version 0.4.0", +] + +[[package]] +name = "filedescriptor" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" +dependencies = [ + "libc", + "thiserror", + "winapi", +] + +[[package]] +name = "filetime" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.2.16", + "windows-sys 0.48.0", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" +dependencies = [ + "bitflags", + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" +dependencies = [ + "bitflags", + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.1.0", +] + +[[package]] +name = "gdk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps 6.1.0", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca49a59ad8cfdf36ef7330fe7bdfbe1d34323220cc16a0de2679ee773aee2c2" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps 6.1.0", +] + +[[package]] +name = "gdkx11-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b7f8c7a84b407aa9b143877e267e848ff34106578b64d1e0a24bf550716178" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps 6.1.0", + "x11", +] + +[[package]] +name = "generator" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e123d9ae7c02966b4d892e550bdc32164f05853cd40ab570650ad600596a8a" +dependencies = [ + "cc", + "libc", + "log 0.4.17", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gio" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-io", + "gio-sys", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.1.0", + "winapi", +] + +[[package]] +name = "glib" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.15.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a" +dependencies = [ + "anyhow", + "heck 0.4.1", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glib-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" +dependencies = [ + "libc", + "system-deps 6.1.0", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" +dependencies = [ + "aho-corasick 0.7.20", + "bstr", + "fnv", + "log 0.4.17", + "regex 1.8.3", +] + +[[package]] +name = "gobject-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.1.0", +] + +[[package]] +name = "gtk" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +dependencies = [ + "atk", + "bitflags", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "once_cell", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps 6.1.0", +] + +[[package]] +name = "gtk3-macros" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "684c0456c086e8e7e9af73ec5b84e35938df394712054550e81558d21c44ab0d" +dependencies = [ + "anyhow", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "h2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "handlebars" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb04af2006ea09d985fef82b81e0eb25337e51b691c76403332378a53d521edc" +dependencies = [ + "lazy_static 0.2.11", + "log 0.3.9", + "pest 0.3.3", + "quick-error", + "regex 0.2.11", + "serde", + "serde_json", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "headers" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +dependencies = [ + "base64 0.13.1", + "bitflags", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148" +dependencies = [ + "log 0.4.17", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.6", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.6", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" +dependencies = [ + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows 0.48.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "ignore" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +dependencies = [ + "crossbeam-utils", + "globset", + "lazy_static 1.4.0", + "log 0.4.17", + "memchr", + "regex 1.8.3", + "same-file", + "thread_local 1.1.7", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-rational", + "num-traits", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", + "serde", +] + +[[package]] +name = "infer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f178e61cdbfe084aa75a2f4f7a25a5bb09701a47ae1753608f194b15783c937a" +dependencies = [ + "cfb", +] + +[[package]] +name = "infer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a898e4b7951673fce96614ce5751d13c40fc5674bc2d759288e46c3ab62598b3" +dependencies = [ + "cfb", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "interfaces" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec8f50a973916cac3da5057c986db05cd3346f38c78e9bc24f64cc9f6a3978f" +dependencies = [ + "bitflags", + "cc", + "handlebars", + "lazy_static 1.4.0", + "libc", + "nix 0.23.2", + "serde", + "serde_derive", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "javascriptcore-rs" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c" +dependencies = [ + "bitflags", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905fbb87419c5cde6e3269537e4ea7d46431f3008c5d057e915ef3f115e7793c" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log 0.4.17", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3fa5a61630976fc4c353c70297f2e93f1930e3ccee574d59d618ccbd5154ce" +dependencies = [ + "serde", + "serde_json", + "treediff 3.0.2", +] + +[[package]] +name = "json-patch" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f54898088ccb91df1b492cc80029a6fdf1c48ca0db7c6822a8babad69c94658" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "treediff 4.0.2", +] + +[[package]] +name = "kuchiki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358" +dependencies = [ + "cssparser", + "html5ever", + "matches", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libappindicator" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2d3cb96d092b4824cb306c9e544c856a4cb6210c1081945187f7f1924b47e8" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log 0.4.17", +] + +[[package]] +name = "libappindicator-sys" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b3b6681973cea8cc3bce7391e6d7d5502720b80a581c9a95c9cbaf592826aa" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" +dependencies = [ + "log 0.4.17", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", + "serde", +] + +[[package]] +name = "log-mdc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" + +[[package]] +name = "log4rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d36ca1786d9e79b8193a68d480a0907b612f109537115c6ff655a3a1967533fd" +dependencies = [ + "anyhow", + "arc-swap", + "chrono", + "derivative", + "fnv", + "humantime", + "libc", + "log 0.4.17", + "log-mdc", + "parking_lot", + "serde", + "serde-value", + "serde_json", + "serde_yaml 0.8.26", + "thiserror", + "thread-id", + "typemap-ors", + "winapi", +] + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" +dependencies = [ + "log 0.4.17", + "phf 0.8.0", + "phf_codegen 0.8.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "minisign-verify" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log 0.4.17", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.45.0", +] + +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log 0.4.17", + "memchr", + "mime", + "spin 0.9.8", + "version_check", +] + +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static 1.4.0", + "libc", + "log 0.4.17", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "static_assertions", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[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-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "open" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8" +dependencies = [ + "pathdiff", + "windows-sys 0.42.0", +] + +[[package]] +name = "open" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16814a067484415fda653868c9be0ac5f2abd2ef5d951082a5f2fe1b3662944" +dependencies = [ + "is-wsl", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "111.25.3+1.1.1t" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924757a6a226bf60da5f7dd0311a34d2b52283dd82ddeb103208ddc66362f80c" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" +dependencies = [ + "num-traits", +] + +[[package]] +name = "os_pipe" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae859aa07428ca9a929b936690f8b12dc5f11dd8c6992a18ca93919f28bc177" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pango" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +dependencies = [ + "bitflags", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.1.0", +] + +[[package]] +name = "parking" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "windows-sys 0.45.0", +] + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pathsearch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da983bc5e582ab17179c190b4b66c7d76c5943a69c6d34df2a2b6bf8a2977b05" +dependencies = [ + "anyhow", + "libc", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pest" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6dda33d67c26f0aac90d324ab2eb7239c819fc7b2552fe9faa4fe88441edc8" + +[[package]] +name = "pest" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +dependencies = [ + "phf_shared 0.11.1", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" +dependencies = [ + "phf_generator 0.11.1", + "phf_shared 0.11.1", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +dependencies = [ + "phf_shared 0.11.1", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "plist" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590" +dependencies = [ + "base64 0.21.2", + "indexmap", + "line-wrap", + "quick-xml", + "serde", + "time 0.3.15", +] + +[[package]] +name = "png" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaeebc51f9e7d2c150d3f3bfeb667f2aa985db5ef1e3d212847bdedb488beeaa" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "concurrent-queue", + "libc", + "log 0.4.17", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "port_scanner" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325a6d2ac5dee293c3b2612d4993b98aec1dff096b0a2dae70ed7d95784a05da" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-xml" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.9", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7e3d950b66e19e0c372f3fa3fbbcf85b1746b571f74e0c2af6042a5c93420a" +dependencies = [ + "cty", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom 0.2.9", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" +dependencies = [ + "aho-corasick 0.6.10", + "memchr", + "regex-syntax 0.5.6", + "thread_local 0.3.6", + "utf8-ranges", +] + +[[package]] +name = "regex" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" +dependencies = [ + "aho-corasick 1.0.1", + "memchr", + "regex-syntax 0.7.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-syntax" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" +dependencies = [ + "ucd-util", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "log 0.4.17", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", + "winreg 0.10.1", +] + +[[package]] +name = "rfd" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea" +dependencies = [ + "block", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "lazy_static 1.4.0", + "log 0.4.17", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.37.0", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rquickjs" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc022cc82b5de6f38b2f4ddb8ed9c49cdbd7ce112e650b181598e102157257de" +dependencies = [ + "rquickjs-core", +] + +[[package]] +name = "rquickjs-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fa1ecc1c84b31da87e5b26ce2b5218d36ffeb5c322141c78b79fa86a6ee3b9" +dependencies = [ + "rquickjs-sys", +] + +[[package]] +name = "rquickjs-sys" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24311952af42d8252e399cf48e7d470cb413b1a11a1a5b7fab648cd2edec76c5" +dependencies = [ + "cc", +] + +[[package]] +name = "rs-snowflake" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e60ef3b82994702bbe4e134d98aadca4b49ed04440148985678d415c68127666" + +[[package]] +name = "runas" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49535b7c73aec5596ae2c44a6d8a7a8f8592e5744564c327fd4846750413d921" +dependencies = [ + "libc", + "security-framework-sys", + "which", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver 1.0.17", +] + +[[package]] +name = "rustix" +version = "0.37.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e" +dependencies = [ + "log 0.4.17", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys 0.42.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags", + "cssparser", + "derive_more", + "fxhash", + "log 0.4.17", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser 0.7.0", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser 0.10.2", +] + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +dependencies = [ + "serde", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest 2.6.0", +] + +[[package]] +name = "serde" +version = "1.0.163" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.163" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa 1.0.6", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "serde_spanned" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.6", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" +dependencies = [ + "base64 0.13.1", + "chrono", + "hex", + "indexmap", + "serde", + "serde_json", + "serde_with_macros", + "time 0.3.15", +] + +[[package]] +name = "serde_with_macros" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "serde_yaml" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +dependencies = [ + "indexmap", + "ryu", + "serde", + "yaml-rust", +] + +[[package]] +name = "serde_yaml" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9d684e3ec7de3bf5466b32bd75303ac16f0736426e5a4e0d6e489559ce1249c" +dependencies = [ + "indexmap", + "itoa 1.0.6", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static 1.4.0", +] + +[[package]] +name = "shared_child" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static 1.4.0", + "libc", +] + +[[package]] +name = "signal-hook" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "smol" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "soup2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" +dependencies = [ + "bitflags", + "gio", + "glib", + "libc", + "once_cell", + "soup2-sys", +] + +[[package]] +name = "soup2-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf" +dependencies = [ + "bitflags", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "state" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" +dependencies = [ + "loom", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sysinfo" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02f1dc6930a439cc5d154221b5387d153f8183529b07c19aca24ea31e0a167e1" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] + +[[package]] +name = "sysproxy" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9707a79d3b95683aa5a9521e698ffd878b8fb289727c25a69157fb85d529ffff" +dependencies = [ + "interfaces", + "thiserror", + "winapi", + "winreg 0.10.1", +] + +[[package]] +name = "system-deps" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18db855554db7bd0e73e06cf7ba3df39f97812cb11d3f75e71c39bf45171797e" +dependencies = [ + "cfg-expr 0.9.1", + "heck 0.3.3", + "pkg-config", + "toml 0.5.11", + "version-compare 0.0.11", +] + +[[package]] +name = "system-deps" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fa6fb9ee296c0dc2df41a656ca7948546d061958115ddb0bcaae43ad0d17d2" +dependencies = [ + "cfg-expr 0.15.1", + "heck 0.4.1", + "pkg-config", + "toml 0.7.4", + "version-compare 0.1.1", +] + +[[package]] +name = "tao" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6d198e01085564cea63e976ad1566c1ba2c2e4cc79578e35d9f05521505e31" +dependencies = [ + "bitflags", + "cairo-rs", + "cc", + "cocoa", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dirs-next", + "dispatch", + "gdk", + "gdk-pixbuf", + "gdk-sys", + "gdkwayland-sys", + "gdkx11-sys", + "gio", + "glib", + "glib-sys", + "gtk", + "image", + "instant", + "jni", + "lazy_static 1.4.0", + "libappindicator", + "libc", + "log 0.4.17", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "once_cell", + "parking_lot", + "png", + "raw-window-handle", + "scopeguard", + "serde", + "tao-macros", + "unicode-segmentation", + "uuid", + "windows 0.39.0", + "windows-implement", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b27a4bcc5eb524658234589bdffc7e7bfb996dbae6ce9393bfd39cb4159b445" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tar" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" + +[[package]] +name = "tauri" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d42ba3a2e8556722f31336a0750c10dbb6a81396a1c452977f515da83f69f842" +dependencies = [ + "anyhow", + "attohttpc", + "base64 0.21.2", + "bytes", + "cocoa", + "dirs-next", + "embed_plist", + "encoding_rs", + "flate2", + "futures-util", + "glib", + "glob", + "gtk", + "heck 0.4.1", + "http", + "ignore", + "infer 0.9.0", + "minisign-verify", + "objc", + "once_cell", + "open 3.2.0", + "os_pipe", + "percent-encoding", + "png", + "rand 0.8.5", + "raw-window-handle", + "regex 1.8.3", + "reqwest", + "rfd", + "semver 1.0.17", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "shared_child", + "state", + "tar", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "tempfile", + "thiserror", + "time 0.3.15", + "tokio", + "url", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", + "zip", +] + +[[package]] +name = "tauri-build" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8807c85d656b2b93927c19fe5a5f1f1f348f96c2de8b90763b3c2d561511f9b4" +dependencies = [ + "anyhow", + "cargo_toml", + "heck 0.4.1", + "json-patch 0.2.7", + "semver 1.0.17", + "serde_json", + "tauri-utils", + "winres", +] + +[[package]] +name = "tauri-codegen" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a2105f807c6f50b2fa2ce5abd62ef207bc6f14c9fcc6b8caec437f6fb13bde" +dependencies = [ + "base64 0.21.2", + "brotli", + "ico", + "json-patch 1.0.0", + "plist", + "png", + "proc-macro2", + "quote", + "regex 1.8.3", + "semver 1.0.17", + "serde", + "serde_json", + "sha2 0.10.6", + "tauri-utils", + "thiserror", + "time 0.3.15", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8784cfe6f5444097e93c69107d1ac5e8f13d02850efa8d8f2a40fe79674cef46" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b80ea3fcd5fefb60739a3b577b277e8fc30434538a2f5bba82ad7d4368c422" +dependencies = [ + "gtk", + "http", + "http-range", + "rand 0.8.5", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror", + "url", + "uuid", + "webview2-com", + "windows 0.39.0", +] + +[[package]] +name = "tauri-runtime-wry" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c396950b1ba06aee1b4ffe6c7cd305ff433ca0e30acbc5fa1a2f92a4ce70f1" +dependencies = [ + "cocoa", + "gtk", + "percent-encoding", + "rand 0.8.5", + "raw-window-handle", + "tauri-runtime", + "tauri-utils", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6f9c2dafef5cbcf52926af57ce9561bd33bb41d7394f8bb849c0330260d864" +dependencies = [ + "brotli", + "ctor", + "glob", + "heck 0.4.1", + "html5ever", + "infer 0.12.0", + "json-patch 1.0.0", + "kuchiki", + "memchr", + "phf 0.10.1", + "proc-macro2", + "quote", + "semver 1.0.17", + "serde", + "serde_json", + "serde_with", + "thiserror", + "url", + "walkdir", + "windows 0.39.0", +] + +[[package]] +name = "tempfile" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.45.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "terminfo" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da31aef70da0f6352dbcb462683eb4dd2bfad01cf3fc96cf204547b9a839a585" +dependencies = [ + "dirs 4.0.0", + "fnv", + "nom 5.1.3", + "phf 0.11.1", + "phf_codegen 0.11.1", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ef6892cc0348a9b3b8c377addba91e0f6365863d92354bf27559dca81ee8c5" +dependencies = [ + "anyhow", + "base64 0.13.1", + "bitflags", + "cfg-if", + "filedescriptor", + "hex", + "lazy_static 1.4.0", + "libc", + "log 0.4.17", + "memmem", + "num-derive", + "num-traits", + "ordered-float", + "regex 1.8.3", + "semver 0.11.0", + "sha2 0.9.9", + "signal-hook 0.1.17", + "terminfo", + "termios", + "thiserror", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "winapi", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "thread-id" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee93aa2b8331c0fec9091548843f2c90019571814057da3b783f9de09349d73" +dependencies = [ + "libc", + "redox_syscall 0.2.16", + "winapi", +] + +[[package]] +name = "thread_local" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +dependencies = [ + "lazy_static 1.4.0", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c" +dependencies = [ + "itoa 1.0.6", + "libc", + "num_threads", + "serde", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +dependencies = [ + "autocfg", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" +dependencies = [ + "futures-util", + "log 0.4.17", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log 0.4.17", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static 1.4.0", + "log 0.4.17", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex 1.8.3", + "sharded-slab", + "smallvec", + "thread_local 1.1.7", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "treediff" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761e8d5ad7ce14bb82b7e61ccc0ca961005a275a060b9644a2431aa11553c2ff" +dependencies = [ + "serde_json", +] + +[[package]] +name = "treediff" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303" +dependencies = [ + "serde_json", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes", + "http", + "httparse", + "log 0.4.17", + "rand 0.8.5", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typemap-ors" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68c24b707f02dd18f1e4ccceb9d49f2058c2fb86384ef9972592904d7a28867" +dependencies = [ + "unsafe-any-ors", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + +[[package]] +name = "ucd-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unsafe-any-ors" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a303d30665362d9680d7d91d78b23f5f899504d4f08b3c4cf08d055d87c0ad" +dependencies = [ + "destructure_traitobject", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" +dependencies = [ + "getrandom 0.2.9", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log 0.4.17", + "try-lock", +] + +[[package]] +name = "warp" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba431ef570df1287f7f8b07e376491ad54f84d26ac473489427231e1718e1f69" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers", + "http", + "hyper", + "log 0.4.17", + "mime", + "mime_guess", + "multer", + "percent-encoding", + "pin-project", + "rustls-pemfile", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tokio-util", + "tower-service", + "tracing", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +dependencies = [ + "bumpalo", + "log 0.4.17", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" + +[[package]] +name = "wasm-streams" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370" +dependencies = [ + "bitflags", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup2", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3" +dependencies = [ + "atk-sys", + "bitflags", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pango-sys", + "pkg-config", + "soup2-sys", + "system-deps 6.1.0", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "webview2-com" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a769c9f1a64a8734bde70caafac2b96cada12cd4aefa49196b3a386b8b4178" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "webview2-com-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaebe196c01691db62e9e4ca52c5ef1e4fd837dcae27dae3ada599b5a8fd05ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "webview2-com-sys" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac48ef20ddf657755fdcda8dfed2a7b4fc7e4581acce6fe9b88c3d64f29dee7" +dependencies = [ + "regex 1.8.3", + "serde", + "serde_json", + "thiserror", + "windows 0.39.0", + "windows-bindgen", + "windows-metadata", +] + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-shadows" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29d30320647cfc3dc45554c8ad825b84831def81f967a2f7589931328ff9b16d" +dependencies = [ + "cocoa", + "objc", + "raw-window-handle", + "windows-sys 0.42.0", +] + +[[package]] +name = "window-vibrancy" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f762d9cc392fb85e6b1b5eed1ef13d73fed5149a5cbb017a7137497d14ef612" +dependencies = [ + "cocoa", + "objc", + "raw-window-handle", + "windows-sys 0.42.0", +] + +[[package]] +name = "windows" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +dependencies = [ + "windows_aarch64_msvc 0.37.0", + "windows_i686_gnu 0.37.0", + "windows_i686_msvc 0.37.0", + "windows_x86_64_gnu 0.37.0", + "windows_x86_64_msvc 0.37.0", +] + +[[package]] +name = "windows" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" +dependencies = [ + "windows-implement", + "windows_aarch64_msvc 0.39.0", + "windows_i686_gnu 0.39.0", + "windows_i686_msvc 0.39.0", + "windows_x86_64_gnu 0.39.0", + "windows_x86_64_msvc 0.39.0", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-bindgen" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68003dbd0e38abc0fb85b939240f4bce37c43a5981d3df37ccbaaa981b47cb41" +dependencies = [ + "windows-metadata", + "windows-tokens", +] + +[[package]] +name = "windows-implement" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7" +dependencies = [ + "syn 1.0.109", + "windows-tokens", +] + +[[package]] +name = "windows-metadata" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-tokens" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" + +[[package]] +name = "windows_i686_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" + +[[package]] +name = "windows_i686_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" + +[[package]] +name = "windows_x86_64_msvc" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winres" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "wry" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33748f35413c8a98d45f7a08832d848c0c5915501803d1faade5a4ebcd258cea" +dependencies = [ + "base64 0.13.1", + "block", + "cocoa", + "core-graphics", + "crossbeam-channel", + "dunce", + "gdk", + "gio", + "glib", + "gtk", + "html5ever", + "http", + "kuchiki", + "libc", + "log 0.4.17", + "objc", + "objc_id", + "once_cell", + "serde", + "serde_json", + "sha2 0.10.6", + "soup2", + "tao", + "thiserror", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..440a398 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,69 @@ +[package] +name = "clash-verge" +version = "0.1.0" +description = "clash verge" +authors = ["zzzgydi"] +license = "GPL-3.0" +repository = "https://github.com/zzzgydi/clash-verge.git" +default-run = "clash-verge" +edition = "2021" +build = "build.rs" + +[build-dependencies] +tauri-build = { version = "1", features = [] } + +[dependencies] +warp = "0.3" +which = "4.2.2" +anyhow = "1.0" +dirs = "5.0.0" +open = "4.0.1" +log = "0.4.14" +ctrlc = "3.2.3" +dunce = "1.0.2" +log4rs = "1.0.0" +nanoid = "0.4.0" +chrono = "0.4.19" +sysinfo = "0.29" +sysproxy = "0.3" +rquickjs = "0.1.7" +serde_json = "1.0" +serde_yaml = "0.9" +auto-launch = "0.5" +once_cell = "1.14.0" +port_scanner = "0.1.5" +delay_timer = "0.11.1" +parking_lot = "0.12.0" +tokio = { version = "1", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +reqwest = { version = "0.11", features = ["json","rustls-tls"] } +tauri = { version = "1.2.4", features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] } +window-vibrancy = { version = "0.3.0" } +window-shadows = { version = "0.2.0" } +wry = { version = "0.24.3" } + + +[target.'cfg(windows)'.dependencies] +runas = "1.1.0" +deelevate = "0.2.0" +winreg = { version = "0.50", features = ["transactions"] } +windows-sys = { version = "0.48", features = ["Win32_System_LibraryLoader", "Win32_System_SystemInformation"] } + +[target.'cfg(windows)'.dependencies.tauri] +features = ["global-shortcut-all", "icon-png", "process-all", "shell-all", "system-tray", "updater", "window-all"] + +[target.'cfg(linux)'.dependencies.tauri] +features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all", "native-tls-vendored", "reqwest-native-tls-vendored"] + + +[features] +default = ["custom-protocol"] +custom-protocol = ["tauri/custom-protocol"] +verge-dev = [] +default-meta = [] + +[profile.release] +panic = "abort" +codegen-units = 1 +lto = true +opt-level = "s" diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000..be2533a Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..3245891 Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000..1a93685 Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..4240ea5 Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..822b49b Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..bb7e494 Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..9b0ec9f Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..496a662 Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..c02df10 Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..8ff531d Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..7829892 Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..6b64990 Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..15344c6 Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon-new.icns b/src-tauri/icons/icon-new.icns new file mode 100644 index 0000000..b66e74e Binary files /dev/null and b/src-tauri/icons/icon-new.icns differ diff --git a/src-tauri/icons/icon-shrink.png b/src-tauri/icons/icon-shrink.png new file mode 100644 index 0000000..5ebfa38 Binary files /dev/null and b/src-tauri/icons/icon-shrink.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000..88df4a0 Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000..e406a78 Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..cf91cf6 Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/tray-icon.ico b/src-tauri/icons/tray-icon.ico new file mode 100644 index 0000000..611c958 Binary files /dev/null and b/src-tauri/icons/tray-icon.ico differ diff --git a/src-tauri/icons/tray-icon.png b/src-tauri/icons/tray-icon.png new file mode 100644 index 0000000..be2533a Binary files /dev/null and b/src-tauri/icons/tray-icon.png differ diff --git a/src-tauri/icons/win-tray-icon-activated.png b/src-tauri/icons/win-tray-icon-activated.png new file mode 100644 index 0000000..441ff02 Binary files /dev/null and b/src-tauri/icons/win-tray-icon-activated.png differ diff --git a/src-tauri/icons/win-tray-icon.png b/src-tauri/icons/win-tray-icon.png new file mode 100644 index 0000000..29616c8 Binary files /dev/null and b/src-tauri/icons/win-tray-icon.png differ diff --git a/src-tauri/rustfmt.toml b/src-tauri/rustfmt.toml new file mode 100644 index 0000000..11eda88 --- /dev/null +++ b/src-tauri/rustfmt.toml @@ -0,0 +1,14 @@ +max_width = 100 +hard_tabs = false +tab_spaces = 4 +newline_style = "Auto" +use_small_heuristics = "Default" +reorder_imports = true +reorder_modules = true +remove_nested_parens = true +edition = "2021" +merge_derives = true +use_try_shorthand = false +use_field_init_shorthand = false +force_explicit_abi = true +imports_granularity = "Crate" diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs new file mode 100644 index 0000000..4b42ebe --- /dev/null +++ b/src-tauri/src/cmds.rs @@ -0,0 +1,280 @@ +use crate::{ + config::*, + core::*, + feat, + utils::{dirs, help}, +}; +use crate::{ret_err, wrap_err}; +use anyhow::{Context, Result}; +use serde_yaml::Mapping; +use std::collections::{HashMap, VecDeque}; +use sysproxy::Sysproxy; + +type CmdResult = Result; + +#[tauri::command] +pub fn get_profiles() -> CmdResult { + Ok(Config::profiles().data().clone()) +} + +#[tauri::command] +pub async fn enhance_profiles() -> CmdResult { + wrap_err!(CoreManager::global().update_config().await)?; + handle::Handle::refresh_clash(); + Ok(()) +} + +#[tauri::command] +pub async fn import_profile(url: String, option: Option) -> CmdResult { + let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?; + wrap_err!(Config::profiles().data().append_item(item)) +} + +#[tauri::command] +pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResult { + let item = wrap_err!(PrfItem::from(item, file_data).await)?; + wrap_err!(Config::profiles().data().append_item(item)) +} + +#[tauri::command] +pub async fn update_profile(index: String, option: Option) -> CmdResult { + wrap_err!(feat::update_profile(index, option).await) +} + +#[tauri::command] +pub async fn delete_profile(index: String) -> CmdResult { + let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?; + if should_update { + wrap_err!(CoreManager::global().update_config().await)?; + handle::Handle::refresh_clash(); + } + + Ok(()) +} + +/// 修改profiles的 +#[tauri::command] +pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { + wrap_err!({ Config::profiles().draft().patch_config(profiles) })?; + + match CoreManager::global().update_config().await { + Ok(_) => { + handle::Handle::refresh_clash(); + Config::profiles().apply(); + wrap_err!(Config::profiles().data().save_file())?; + Ok(()) + } + Err(err) => { + Config::profiles().discard(); + log::error!(target: "app", "{err}"); + Err(format!("{err}")) + } + } +} + +/// 修改某个profile item的 +#[tauri::command] +pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult { + wrap_err!(Config::profiles().data().patch_item(index, profile))?; + wrap_err!(timer::Timer::global().refresh()) +} + +#[tauri::command] +pub fn view_profile(index: String) -> CmdResult { + let file = { + wrap_err!(Config::profiles().latest().get_item(&index))? + .file + .clone() + .ok_or("the file field is null") + }?; + + let path = wrap_err!(dirs::app_profiles_dir())?.join(file); + if !path.exists() { + ret_err!("the file not found"); + } + + wrap_err!(help::open_file(path)) +} + +#[tauri::command] +pub fn read_profile_file(index: String) -> CmdResult { + let profiles = Config::profiles(); + let profiles = profiles.latest(); + let item = wrap_err!(profiles.get_item(&index))?; + let data = wrap_err!(item.read_file())?; + Ok(data) +} + +#[tauri::command] +pub fn save_profile_file(index: String, file_data: Option) -> CmdResult { + if file_data.is_none() { + return Ok(()); + } + + let profiles = Config::profiles(); + let profiles = profiles.latest(); + let item = wrap_err!(profiles.get_item(&index))?; + wrap_err!(item.save_file(file_data.unwrap())) +} + +#[tauri::command] +pub fn get_clash_info() -> CmdResult { + Ok(Config::clash().latest().get_client_info()) +} + +#[tauri::command] +pub fn get_runtime_config() -> CmdResult> { + Ok(Config::runtime().latest().config.clone()) +} + +#[tauri::command] +pub fn get_runtime_yaml() -> CmdResult { + let runtime = Config::runtime(); + let runtime = runtime.latest(); + let config = runtime.config.as_ref(); + wrap_err!(config + .ok_or(anyhow::anyhow!("failed to parse config to yaml file")) + .and_then( + |config| serde_yaml::to_string(config).context("failed to convert config to yaml") + )) +} + +#[tauri::command] +pub fn get_runtime_exists() -> CmdResult> { + Ok(Config::runtime().latest().exists_keys.clone()) +} + +#[tauri::command] +pub fn get_runtime_logs() -> CmdResult>> { + Ok(Config::runtime().latest().chain_logs.clone()) +} + +#[tauri::command] +pub async fn patch_clash_config(payload: Mapping) -> CmdResult { + wrap_err!(feat::patch_clash(payload).await) +} + +#[tauri::command] +pub fn get_verge_config() -> CmdResult { + Ok(Config::verge().data().clone()) +} + +#[tauri::command] +pub async fn patch_verge_config(payload: IVerge) -> CmdResult { + wrap_err!(feat::patch_verge(payload).await) +} + +#[tauri::command] +pub async fn change_clash_core(clash_core: Option) -> CmdResult { + wrap_err!(CoreManager::global().change_core(clash_core).await) +} + +/// restart the sidecar +#[tauri::command] +pub async fn restart_sidecar() -> CmdResult { + wrap_err!(CoreManager::global().run_core().await) +} + +#[tauri::command] +pub fn grant_permission(core: String) -> CmdResult { + #[cfg(any(target_os = "macos", target_os = "linux"))] + return wrap_err!(manager::grant_permission(core)); + + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + return Err("Unsupported target".into()); +} + +/// get the system proxy +#[tauri::command] +pub fn get_sys_proxy() -> CmdResult { + let current = wrap_err!(Sysproxy::get_system_proxy())?; + + let mut map = Mapping::new(); + map.insert("enable".into(), current.enable.into()); + map.insert( + "server".into(), + format!("{}:{}", current.host, current.port).into(), + ); + map.insert("bypass".into(), current.bypass.into()); + + Ok(map) +} + +#[tauri::command] +pub fn get_clash_logs() -> CmdResult> { + Ok(logger::Logger::global().get_log()) +} + +#[tauri::command] +pub fn open_app_dir() -> CmdResult<()> { + let app_dir = wrap_err!(dirs::app_home_dir())?; + wrap_err!(open::that(app_dir)) +} + +#[tauri::command] +pub fn open_core_dir() -> CmdResult<()> { + let core_dir = wrap_err!(tauri::utils::platform::current_exe())?; + let core_dir = core_dir.parent().ok_or(format!("failed to get core dir"))?; + wrap_err!(open::that(core_dir)) +} + +#[tauri::command] +pub fn open_logs_dir() -> CmdResult<()> { + let log_dir = wrap_err!(dirs::app_logs_dir())?; + wrap_err!(open::that(log_dir)) +} + +#[tauri::command] +pub fn open_web_url(url: String) -> CmdResult<()> { + wrap_err!(open::that(url)) +} + +#[tauri::command] +pub async fn clash_api_get_proxy_delay( + name: String, + url: Option, +) -> CmdResult { + match clash_api::get_proxy_delay(name, url).await { + Ok(res) => Ok(res), + Err(err) => Err(format!("{}", err.to_string())), + } +} + +#[cfg(windows)] +pub mod service { + use super::*; + use crate::core::win_service; + + #[tauri::command] + pub async fn check_service() -> CmdResult { + wrap_err!(win_service::check_service().await) + } + + #[tauri::command] + pub async fn install_service() -> CmdResult { + wrap_err!(win_service::install_service().await) + } + + #[tauri::command] + pub async fn uninstall_service() -> CmdResult { + wrap_err!(win_service::uninstall_service().await) + } +} + +#[cfg(not(windows))] +pub mod service { + use super::*; + + #[tauri::command] + pub async fn check_service() -> CmdResult { + Ok(()) + } + #[tauri::command] + pub async fn install_service() -> CmdResult { + Ok(()) + } + #[tauri::command] + pub async fn uninstall_service() -> CmdResult { + Ok(()) + } +} diff --git a/src-tauri/src/config/clash.rs b/src-tauri/src/config/clash.rs new file mode 100644 index 0000000..6a4cf5e --- /dev/null +++ b/src-tauri/src/config/clash.rs @@ -0,0 +1,262 @@ +use crate::utils::{dirs, help}; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_yaml::{Mapping, Value}; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + str::FromStr, +}; + +#[derive(Default, Debug, Clone)] +pub struct IClashTemp(pub Mapping); + +impl IClashTemp { + pub fn new() -> Self { + match dirs::clash_path().and_then(|path| help::read_merge_mapping(&path)) { + Ok(map) => Self(Self::guard(map)), + Err(err) => { + log::error!(target: "app", "{err}"); + Self::template() + } + } + } + + pub fn template() -> Self { + let mut map = Mapping::new(); + + map.insert( + "mixed-port".into(), + match cfg!(feature = "default-meta") { + false => 7890.into(), + true => 7898.into(), + }, + ); + map.insert("log-level".into(), "info".into()); + map.insert("allow-lan".into(), false.into()); + map.insert("mode".into(), "rule".into()); + map.insert( + "external-controller".into(), + match cfg!(feature = "default-meta") { + false => "127.0.0.1:9090".into(), + true => "127.0.0.1:9098".into(), + }, + ); + map.insert("secret".into(), "".into()); + + Self(map) + } + + fn guard(mut config: Mapping) -> Mapping { + let port = Self::guard_mixed_port(&config); + let ctrl = Self::guard_server_ctrl(&config); + + config.insert("mixed-port".into(), port.into()); + config.insert("external-controller".into(), ctrl.into()); + config + } + + pub fn patch_config(&mut self, patch: Mapping) { + for (key, value) in patch.into_iter() { + self.0.insert(key, value); + } + } + + pub fn save_config(&self) -> Result<()> { + help::save_yaml( + &dirs::clash_path()?, + &self.0, + Some("# Generated by Clash Verge"), + ) + } + + pub fn get_mixed_port(&self) -> u16 { + Self::guard_mixed_port(&self.0) + } + + pub fn get_client_info(&self) -> ClashInfo { + let config = &self.0; + + ClashInfo { + port: Self::guard_mixed_port(&config), + server: Self::guard_client_ctrl(&config), + secret: config.get("secret").and_then(|value| match value { + Value::String(val_str) => Some(val_str.clone()), + Value::Bool(val_bool) => Some(val_bool.to_string()), + Value::Number(val_num) => Some(val_num.to_string()), + _ => None, + }), + } + } + + pub fn guard_mixed_port(config: &Mapping) -> u16 { + let mut port = config + .get("mixed-port") + .and_then(|value| match value { + Value::String(val_str) => val_str.parse().ok(), + Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), + _ => None, + }) + .unwrap_or(7890); + if port == 0 { + port = 7890; + } + port + } + + pub fn guard_server_ctrl(config: &Mapping) -> String { + config + .get("external-controller") + .and_then(|value| match value.as_str() { + Some(val_str) => { + let val_str = val_str.trim(); + + let val = match val_str.starts_with(":") { + true => format!("127.0.0.1{val_str}"), + false => val_str.to_owned(), + }; + + SocketAddr::from_str(val.as_str()) + .ok() + .map(|s| s.to_string()) + } + None => None, + }) + .unwrap_or("127.0.0.1:9090".into()) + } + + pub fn guard_client_ctrl(config: &Mapping) -> String { + let value = Self::guard_server_ctrl(config); + match SocketAddr::from_str(value.as_str()) { + Ok(mut socket) => { + if socket.ip().is_unspecified() { + socket.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); + } + socket.to_string() + } + Err(_) => "127.0.0.1:9090".into(), + } + } +} + +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct ClashInfo { + /// clash core port + pub port: u16, + /// same as `external-controller` + pub server: String, + /// clash secret + pub secret: Option, +} + +#[test] +fn test_clash_info() { + fn get_case, D: Into>(mp: T, ec: D) -> ClashInfo { + let mut map = Mapping::new(); + map.insert("mixed-port".into(), mp.into()); + map.insert("external-controller".into(), ec.into()); + + IClashTemp(IClashTemp::guard(map)).get_client_info() + } + + fn get_result>(port: u16, server: S) -> ClashInfo { + ClashInfo { + port, + server: server.into(), + secret: None, + } + } + + assert_eq!( + IClashTemp(IClashTemp::guard(Mapping::new())).get_client_info(), + get_result(7890, "127.0.0.1:9090") + ); + + assert_eq!(get_case("", ""), get_result(7890, "127.0.0.1:9090")); + + assert_eq!(get_case(65537, ""), get_result(1, "127.0.0.1:9090")); + + assert_eq!( + get_case(8888, "127.0.0.1:8888"), + get_result(8888, "127.0.0.1:8888") + ); + + assert_eq!( + get_case(8888, " :98888 "), + get_result(8888, "127.0.0.1:9090") + ); + + assert_eq!( + get_case(8888, "0.0.0.0:8080 "), + get_result(8888, "127.0.0.1:8080") + ); + + assert_eq!( + get_case(8888, "0.0.0.0:8080"), + get_result(8888, "127.0.0.1:8080") + ); + + assert_eq!( + get_case(8888, "[::]:8080"), + get_result(8888, "127.0.0.1:8080") + ); + + assert_eq!( + get_case(8888, "192.168.1.1:8080"), + get_result(8888, "192.168.1.1:8080") + ); + + assert_eq!( + get_case(8888, "192.168.1.1:80800"), + get_result(8888, "127.0.0.1:9090") + ); +} + +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct IClash { + pub mixed_port: Option, + pub allow_lan: Option, + pub log_level: Option, + pub ipv6: Option, + pub mode: Option, + pub external_controller: Option, + pub secret: Option, + pub dns: Option, + pub tun: Option, + pub interface_name: Option, +} + +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct IClashTUN { + pub enable: Option, + pub stack: Option, + pub auto_route: Option, + pub auto_detect_interface: Option, + pub dns_hijack: Option>, +} + +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct IClashDNS { + pub enable: Option, + pub listen: Option, + pub default_nameserver: Option>, + pub enhanced_mode: Option, + pub fake_ip_range: Option, + pub use_hosts: Option, + pub fake_ip_filter: Option>, + pub nameserver: Option>, + pub fallback: Option>, + pub fallback_filter: Option, + pub nameserver_policy: Option>, +} + +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct IClashFallbackFilter { + pub geoip: Option, + pub geoip_code: Option, + pub ipcidr: Option>, + pub domain: Option>, +} diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs new file mode 100644 index 0000000..e5af8e5 --- /dev/null +++ b/src-tauri/src/config/config.rs @@ -0,0 +1,103 @@ +use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge}; +use crate::{ + enhance, + utils::{dirs, help}, +}; +use anyhow::{anyhow, Result}; +use once_cell::sync::OnceCell; +use std::{env::temp_dir, path::PathBuf}; + +pub const RUNTIME_CONFIG: &str = "clash-verge.yaml"; +pub const CHECK_CONFIG: &str = "clash-verge-check.yaml"; + +pub struct Config { + clash_config: Draft, + verge_config: Draft, + profiles_config: Draft, + runtime_config: Draft, +} + +impl Config { + pub fn global() -> &'static Config { + static CONFIG: OnceCell = OnceCell::new(); + + CONFIG.get_or_init(|| Config { + clash_config: Draft::from(IClashTemp::new()), + verge_config: Draft::from(IVerge::new()), + profiles_config: Draft::from(IProfiles::new()), + runtime_config: Draft::from(IRuntime::new()), + }) + } + + pub fn clash() -> Draft { + Self::global().clash_config.clone() + } + + pub fn verge() -> Draft { + Self::global().verge_config.clone() + } + + pub fn profiles() -> Draft { + Self::global().profiles_config.clone() + } + + pub fn runtime() -> Draft { + Self::global().runtime_config.clone() + } + + /// 初始化配置 + pub fn init_config() -> Result<()> { + crate::log_err!(Self::generate()); + if let Err(err) = Self::generate_file(ConfigType::Run) { + log::error!(target: "app", "{err}"); + + let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG); + // 如果不存在就将默认的clash文件拿过来 + if !runtime_path.exists() { + help::save_yaml( + &runtime_path, + &Config::clash().latest().0, + Some("# Clash Verge Runtime"), + )?; + } + } + Ok(()) + } + + /// 将配置丢到对应的文件中 + pub fn generate_file(typ: ConfigType) -> Result { + let path = match typ { + ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG), + ConfigType::Check => temp_dir().join(CHECK_CONFIG), + }; + + let runtime = Config::runtime(); + let runtime = runtime.latest(); + let config = runtime + .config + .as_ref() + .ok_or(anyhow!("failed to get runtime config"))?; + + help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?; + Ok(path) + } + + /// 生成配置存好 + pub fn generate() -> Result<()> { + let (config, exists_keys, logs) = enhance::enhance(); + + *Config::runtime().draft() = IRuntime { + config: Some(config), + exists_keys, + chain_logs: logs, + }; + + Ok(()) + } +} + +#[derive(Debug)] +pub enum ConfigType { + Run, + Check, +} diff --git a/src-tauri/src/config/draft.rs b/src-tauri/src/config/draft.rs new file mode 100644 index 0000000..5876f1b --- /dev/null +++ b/src-tauri/src/config/draft.rs @@ -0,0 +1,127 @@ +use super::{IClashTemp, IProfiles, IRuntime, IVerge}; +use parking_lot::{MappedMutexGuard, Mutex, MutexGuard}; +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub struct Draft { + inner: Arc)>>, +} + +macro_rules! draft_define { + ($id: ident) => { + impl Draft<$id> { + #[allow(unused)] + pub fn data(&self) -> MappedMutexGuard<$id> { + MutexGuard::map(self.inner.lock(), |guard| &mut guard.0) + } + + pub fn latest(&self) -> MappedMutexGuard<$id> { + MutexGuard::map(self.inner.lock(), |inner| { + if inner.1.is_none() { + &mut inner.0 + } else { + inner.1.as_mut().unwrap() + } + }) + } + + pub fn draft(&self) -> MappedMutexGuard<$id> { + MutexGuard::map(self.inner.lock(), |inner| { + if inner.1.is_none() { + inner.1 = Some(inner.0.clone()); + } + + inner.1.as_mut().unwrap() + }) + } + + pub fn apply(&self) -> Option<$id> { + let mut inner = self.inner.lock(); + + match inner.1.take() { + Some(draft) => { + let old_value = inner.0.to_owned(); + inner.0 = draft.to_owned(); + Some(old_value) + } + None => None, + } + } + + pub fn discard(&self) -> Option<$id> { + let mut inner = self.inner.lock(); + inner.1.take() + } + } + + impl From<$id> for Draft<$id> { + fn from(data: $id) -> Self { + Draft { + inner: Arc::new(Mutex::new((data, None))), + } + } + } + }; +} + +// draft_define!(IClash); +draft_define!(IClashTemp); +draft_define!(IProfiles); +draft_define!(IRuntime); +draft_define!(IVerge); + +#[test] +fn test_draft() { + let verge = IVerge { + enable_auto_launch: Some(true), + enable_tun_mode: Some(false), + ..IVerge::default() + }; + + let draft = Draft::from(verge); + + assert_eq!(draft.data().enable_auto_launch, Some(true)); + assert_eq!(draft.data().enable_tun_mode, Some(false)); + + assert_eq!(draft.draft().enable_auto_launch, Some(true)); + assert_eq!(draft.draft().enable_tun_mode, Some(false)); + + let mut d = draft.draft(); + d.enable_auto_launch = Some(false); + d.enable_tun_mode = Some(true); + drop(d); + + assert_eq!(draft.data().enable_auto_launch, Some(true)); + assert_eq!(draft.data().enable_tun_mode, Some(false)); + + assert_eq!(draft.draft().enable_auto_launch, Some(false)); + assert_eq!(draft.draft().enable_tun_mode, Some(true)); + + assert_eq!(draft.latest().enable_auto_launch, Some(false)); + assert_eq!(draft.latest().enable_tun_mode, Some(true)); + + assert!(draft.apply().is_some()); + assert!(draft.apply().is_none()); + + assert_eq!(draft.data().enable_auto_launch, Some(false)); + assert_eq!(draft.data().enable_tun_mode, Some(true)); + + assert_eq!(draft.draft().enable_auto_launch, Some(false)); + assert_eq!(draft.draft().enable_tun_mode, Some(true)); + + let mut d = draft.draft(); + d.enable_auto_launch = Some(true); + drop(d); + + assert_eq!(draft.data().enable_auto_launch, Some(false)); + + assert_eq!(draft.draft().enable_auto_launch, Some(true)); + + assert!(draft.discard().is_some()); + + assert_eq!(draft.data().enable_auto_launch, Some(false)); + + assert!(draft.discard().is_none()); + + assert_eq!(draft.draft().enable_auto_launch, Some(false)); +} diff --git a/src-tauri/src/config/mod.rs b/src-tauri/src/config/mod.rs new file mode 100644 index 0000000..b246a76 --- /dev/null +++ b/src-tauri/src/config/mod.rs @@ -0,0 +1,15 @@ +mod clash; +mod config; +mod draft; +mod prfitem; +mod profiles; +mod runtime; +mod verge; + +pub use self::clash::*; +pub use self::config::*; +pub use self::draft::*; +pub use self::prfitem::*; +pub use self::profiles::*; +pub use self::runtime::*; +pub use self::verge::*; diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs new file mode 100644 index 0000000..1e1d4a8 --- /dev/null +++ b/src-tauri/src/config/prfitem.rs @@ -0,0 +1,374 @@ +use crate::utils::{dirs, help, tmpl}; +use anyhow::{bail, Context, Result}; +use reqwest::StatusCode; +use serde::{Deserialize, Serialize}; +use serde_yaml::Mapping; +use std::fs; +use sysproxy::Sysproxy; + +use super::Config; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PrfItem { + pub uid: Option, + + /// profile item type + /// enum value: remote | local | script | merge + #[serde(rename = "type")] + pub itype: Option, + + /// profile name + pub name: Option, + + /// profile file + pub file: Option, + + /// profile description + #[serde(skip_serializing_if = "Option::is_none")] + pub desc: Option, + + /// source url + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + + /// selected information + #[serde(skip_serializing_if = "Option::is_none")] + pub selected: Option>, + + /// subscription user info + #[serde(skip_serializing_if = "Option::is_none")] + pub extra: Option, + + /// updated time + pub updated: Option, + + /// some options of the item + #[serde(skip_serializing_if = "Option::is_none")] + pub option: Option, + + /// the file data + #[serde(skip)] + pub file_data: Option, +} + +#[derive(Default, Debug, Clone, Deserialize, Serialize)] +pub struct PrfSelected { + pub name: Option, + pub now: Option, +} + +#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)] +pub struct PrfExtra { + pub upload: usize, + pub download: usize, + pub total: usize, + pub expire: usize, +} + +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct PrfOption { + /// for `remote` profile's http request + /// see issue #13 + #[serde(skip_serializing_if = "Option::is_none")] + pub user_agent: Option, + + /// for `remote` profile + /// use system proxy + #[serde(skip_serializing_if = "Option::is_none")] + pub with_proxy: Option, + + /// for `remote` profile + /// use self proxy + #[serde(skip_serializing_if = "Option::is_none")] + pub self_proxy: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub update_interval: Option, +} + +impl PrfOption { + pub fn merge(one: Option, other: Option) -> Option { + match (one, other) { + (Some(mut a), Some(b)) => { + a.user_agent = b.user_agent.or(a.user_agent); + a.with_proxy = b.with_proxy.or(a.with_proxy); + a.self_proxy = b.self_proxy.or(a.self_proxy); + a.update_interval = b.update_interval.or(a.update_interval); + Some(a) + } + t @ _ => t.0.or(t.1), + } + } +} + +impl Default for PrfItem { + fn default() -> Self { + PrfItem { + uid: None, + itype: None, + name: None, + desc: None, + file: None, + url: None, + selected: None, + extra: None, + updated: None, + option: None, + file_data: None, + } + } +} + +impl PrfItem { + /// From partial item + /// must contain `itype` + pub async fn from(item: PrfItem, file_data: Option) -> Result { + if item.itype.is_none() { + bail!("type should not be null"); + } + + match item.itype.unwrap().as_str() { + "remote" => { + if item.url.is_none() { + bail!("url should not be null"); + } + let url = item.url.as_ref().unwrap().as_str(); + let name = item.name; + let desc = item.desc; + PrfItem::from_url(url, name, desc, item.option).await + } + "local" => { + let name = item.name.unwrap_or("Local File".into()); + let desc = item.desc.unwrap_or("".into()); + PrfItem::from_local(name, desc, file_data) + } + "merge" => { + let name = item.name.unwrap_or("Merge".into()); + let desc = item.desc.unwrap_or("".into()); + PrfItem::from_merge(name, desc) + } + "script" => { + let name = item.name.unwrap_or("Script".into()); + let desc = item.desc.unwrap_or("".into()); + PrfItem::from_script(name, desc) + } + typ @ _ => bail!("invalid profile item type \"{typ}\""), + } + } + + /// ## Local type + /// create a new item from name/desc + pub fn from_local(name: String, desc: String, file_data: Option) -> Result { + let uid = help::get_uid("l"); + let file = format!("{uid}.yaml"); + + Ok(PrfItem { + uid: Some(uid), + itype: Some("local".into()), + name: Some(name), + desc: Some(desc), + file: Some(file), + url: None, + selected: None, + extra: None, + option: None, + updated: Some(chrono::Local::now().timestamp() as usize), + file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())), + }) + } + + /// ## Remote type + /// create a new item from url + pub async fn from_url( + url: &str, + name: Option, + desc: Option, + option: Option, + ) -> Result { + let opt_ref = option.as_ref(); + let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false)); + let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false)); + let user_agent = opt_ref.map_or(None, |o| o.user_agent.clone()); + + let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy(); + + // 使用软件自己的代理 + if self_proxy { + let port = Config::clash().data().get_mixed_port(); + + let proxy_scheme = format!("http://127.0.0.1:{port}"); + + if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) { + builder = builder.proxy(proxy); + } + if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) { + builder = builder.proxy(proxy); + } + if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) { + builder = builder.proxy(proxy); + } + } + // 使用系统代理 + else if with_proxy { + match Sysproxy::get_system_proxy() { + Ok(p @ Sysproxy { enable: true, .. }) => { + let proxy_scheme = format!("http://{}:{}", p.host, p.port); + + if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) { + builder = builder.proxy(proxy); + } + if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) { + builder = builder.proxy(proxy); + } + if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) { + builder = builder.proxy(proxy); + } + } + _ => {} + }; + } + + let version = unsafe { dirs::APP_VERSION }; + let version = format!("clash-verge/{version}"); + builder = builder.user_agent(user_agent.unwrap_or(version)); + + let resp = builder.build()?.get(url).send().await?; + + let status_code = resp.status(); + if !StatusCode::is_success(&status_code) { + bail!("failed to fetch remote profile with status {status_code}") + } + + let header = resp.headers(); + + // parse the Subscription UserInfo + let extra = match header.get("Subscription-Userinfo") { + Some(value) => { + let sub_info = value.to_str().unwrap_or(""); + + Some(PrfExtra { + upload: help::parse_str(sub_info, "upload=").unwrap_or(0), + download: help::parse_str(sub_info, "download=").unwrap_or(0), + total: help::parse_str(sub_info, "total=").unwrap_or(0), + expire: help::parse_str(sub_info, "expire=").unwrap_or(0), + }) + } + None => None, + }; + + // parse the Content-Disposition + let filename = match header.get("Content-Disposition") { + Some(value) => { + let filename = value.to_str().unwrap_or(""); + help::parse_str::(filename, "filename=") + } + None => None, + }; + + // parse the profile-update-interval + let option = match header.get("profile-update-interval") { + Some(value) => match value.to_str().unwrap_or("").parse::() { + Ok(val) => Some(PrfOption { + update_interval: Some(val * 60), // hour -> min + ..PrfOption::default() + }), + Err(_) => None, + }, + None => None, + }; + + let uid = help::get_uid("r"); + let file = format!("{uid}.yaml"); + let name = name.unwrap_or(filename.unwrap_or("Remote File".into())); + let data = resp.text_with_charset("utf-8").await?; + + // process the charset "UTF-8 with BOM" + let data = data.trim_start_matches('\u{feff}'); + + // check the data whether the valid yaml format + let yaml = serde_yaml::from_str::(data) + .context("the remote profile data is invalid yaml")?; + + if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") { + bail!("profile does not contain `proxies` or `proxy-providers`"); + } + + Ok(PrfItem { + uid: Some(uid), + itype: Some("remote".into()), + name: Some(name), + desc, + file: Some(file), + url: Some(url.into()), + selected: None, + extra, + option, + updated: Some(chrono::Local::now().timestamp() as usize), + file_data: Some(data.into()), + }) + } + + /// ## Merge type (enhance) + /// create the enhanced item by using `merge` rule + pub fn from_merge(name: String, desc: String) -> Result { + let uid = help::get_uid("m"); + let file = format!("{uid}.yaml"); + + Ok(PrfItem { + uid: Some(uid), + itype: Some("merge".into()), + name: Some(name), + desc: Some(desc), + file: Some(file), + url: None, + selected: None, + extra: None, + option: None, + updated: Some(chrono::Local::now().timestamp() as usize), + file_data: Some(tmpl::ITEM_MERGE.into()), + }) + } + + /// ## Script type (enhance) + /// create the enhanced item by using javascript quick.js + pub fn from_script(name: String, desc: String) -> Result { + let uid = help::get_uid("s"); + let file = format!("{uid}.js"); // js ext + + Ok(PrfItem { + uid: Some(uid), + itype: Some("script".into()), + name: Some(name), + desc: Some(desc), + file: Some(file), + url: None, + selected: None, + extra: None, + option: None, + updated: Some(chrono::Local::now().timestamp() as usize), + file_data: Some(tmpl::ITEM_SCRIPT.into()), + }) + } + + /// get the file data + pub fn read_file(&self) -> Result { + if self.file.is_none() { + bail!("could not find the file"); + } + + let file = self.file.clone().unwrap(); + let path = dirs::app_profiles_dir()?.join(file); + fs::read_to_string(path).context("failed to read the file") + } + + /// save the file data + pub fn save_file(&self, data: String) -> Result<()> { + if self.file.is_none() { + bail!("could not find the file"); + } + + let file = self.file.clone().unwrap(); + let path = dirs::app_profiles_dir()?.join(file); + fs::write(path, data.as_bytes()).context("failed to save the file") + } +} diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs new file mode 100644 index 0000000..788386a --- /dev/null +++ b/src-tauri/src/config/profiles.rs @@ -0,0 +1,280 @@ +use super::prfitem::PrfItem; +use crate::utils::{dirs, help}; +use anyhow::{bail, Context, Result}; +use serde::{Deserialize, Serialize}; +use serde_yaml::Mapping; +use std::{fs, io::Write}; + +/// Define the `profiles.yaml` schema +#[derive(Default, Debug, Clone, Deserialize, Serialize)] +pub struct IProfiles { + /// same as PrfConfig.current + pub current: Option, + + /// same as PrfConfig.chain + pub chain: Option>, + + /// record valid fields for clash + pub valid: Option>, + + /// profile list + pub items: Option>, +} + +macro_rules! patch { + ($lv: expr, $rv: expr, $key: tt) => { + if ($rv.$key).is_some() { + $lv.$key = $rv.$key; + } + }; +} + +impl IProfiles { + pub fn new() -> Self { + match dirs::profiles_path().and_then(|path| help::read_yaml::(&path)) { + Ok(mut profiles) => { + if profiles.items.is_none() { + profiles.items = Some(vec![]); + } + // compatible with the old old old version + profiles.items.as_mut().map(|items| { + for item in items.iter_mut() { + if item.uid.is_none() { + item.uid = Some(help::get_uid("d")); + } + } + }); + profiles + } + Err(err) => { + log::error!(target: "app", "{err}"); + Self::template() + } + } + } + + pub fn template() -> Self { + Self { + valid: Some(vec!["dns".into(), "sub-rules".into(), "unified-delay".into()]), + items: Some(vec![]), + ..Self::default() + } + } + + pub fn save_file(&self) -> Result<()> { + help::save_yaml( + &dirs::profiles_path()?, + self, + Some("# Profiles Config for Clash Verge"), + ) + } + + /// 只修改current,valid和chain + pub fn patch_config(&mut self, patch: IProfiles) -> Result<()> { + if self.items.is_none() { + self.items = Some(vec![]); + } + + if let Some(current) = patch.current { + let items = self.items.as_ref().unwrap(); + let some_uid = Some(current); + + if items.iter().any(|e| e.uid == some_uid) { + self.current = some_uid; + } + } + + if let Some(chain) = patch.chain { + self.chain = Some(chain); + } + + if let Some(valid) = patch.valid { + self.valid = Some(valid); + } + + Ok(()) + } + + pub fn get_current(&self) -> Option { + self.current.clone() + } + + /// get items ref + pub fn get_items(&self) -> Option<&Vec> { + self.items.as_ref() + } + + /// find the item by the uid + pub fn get_item(&self, uid: &String) -> Result<&PrfItem> { + if let Some(items) = self.items.as_ref() { + let some_uid = Some(uid.clone()); + + for each in items.iter() { + if each.uid == some_uid { + return Ok(each); + } + } + } + + bail!("failed to get the profile item \"uid:{uid}\""); + } + + /// append new item + /// if the file_data is some + /// then should save the data to file + pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> { + if item.uid.is_none() { + bail!("the uid should not be null"); + } + + // save the file data + // move the field value after save + if let Some(file_data) = item.file_data.take() { + if item.file.is_none() { + bail!("the file should not be null"); + } + + let file = item.file.clone().unwrap(); + let path = dirs::app_profiles_dir()?.join(&file); + + fs::File::create(path) + .with_context(|| format!("failed to create file \"{}\"", file))? + .write(file_data.as_bytes()) + .with_context(|| format!("failed to write to file \"{}\"", file))?; + } + + if self.items.is_none() { + self.items = Some(vec![]); + } + + self.items.as_mut().map(|items| items.push(item)); + self.save_file() + } + + /// update the item value + pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> { + let mut items = self.items.take().unwrap_or(vec![]); + + for each in items.iter_mut() { + if each.uid == Some(uid.clone()) { + patch!(each, item, itype); + patch!(each, item, name); + patch!(each, item, desc); + patch!(each, item, file); + patch!(each, item, url); + patch!(each, item, selected); + patch!(each, item, extra); + patch!(each, item, updated); + patch!(each, item, option); + + self.items = Some(items); + return self.save_file(); + } + } + + self.items = Some(items); + bail!("failed to find the profile item \"uid:{uid}\"") + } + + /// be used to update the remote item + /// only patch `updated` `extra` `file_data` + pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> { + if self.items.is_none() { + self.items = Some(vec![]); + } + + // find the item + let _ = self.get_item(&uid)?; + + if let Some(items) = self.items.as_mut() { + let some_uid = Some(uid.clone()); + + for each in items.iter_mut() { + if each.uid == some_uid { + each.extra = item.extra; + each.updated = item.updated; + + // save the file data + // move the field value after save + if let Some(file_data) = item.file_data.take() { + let file = each.file.take(); + let file = + file.unwrap_or(item.file.take().unwrap_or(format!("{}.yaml", &uid))); + + // the file must exists + each.file = Some(file.clone()); + + let path = dirs::app_profiles_dir()?.join(&file); + + fs::File::create(path) + .with_context(|| format!("failed to create file \"{}\"", file))? + .write(file_data.as_bytes()) + .with_context(|| format!("failed to write to file \"{}\"", file))?; + } + + break; + } + } + } + + self.save_file() + } + + /// delete item + /// if delete the current then return true + pub fn delete_item(&mut self, uid: String) -> Result { + let current = self.current.as_ref().unwrap_or(&uid); + let current = current.clone(); + + let mut items = self.items.take().unwrap_or(vec![]); + let mut index = None; + + // get the index + for i in 0..items.len() { + if items[i].uid == Some(uid.clone()) { + index = Some(i); + break; + } + } + + if let Some(index) = index { + items.remove(index).file.map(|file| { + let _ = dirs::app_profiles_dir().map(|path| { + let path = path.join(file); + if path.exists() { + let _ = fs::remove_file(path); + } + }); + }); + } + + // delete the original uid + if current == uid { + self.current = match items.len() > 0 { + true => items[0].uid.clone(), + false => None, + }; + } + + self.items = Some(items); + self.save_file()?; + Ok(current == uid) + } + + /// 获取current指向的配置内容 + pub fn current_mapping(&self) -> Result { + match (self.current.as_ref(), self.items.as_ref()) { + (Some(current), Some(items)) => { + if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) { + let file_path = match item.file.as_ref() { + Some(file) => dirs::app_profiles_dir()?.join(file), + None => bail!("failed to get the file field"), + }; + return Ok(help::read_merge_mapping(&file_path)?); + } + bail!("failed to find the current profile \"uid:{current}\""); + } + _ => Ok(Mapping::new()), + } + } +} diff --git a/src-tauri/src/config/runtime.rs b/src-tauri/src/config/runtime.rs new file mode 100644 index 0000000..cce376b --- /dev/null +++ b/src-tauri/src/config/runtime.rs @@ -0,0 +1,31 @@ +use serde::{Deserialize, Serialize}; +use serde_yaml::Mapping; +use std::collections::HashMap; + +#[derive(Default, Debug, Clone, Deserialize, Serialize)] +pub struct IRuntime { + pub config: Option, + // 记录在配置中(包括merge和script生成的)出现过的keys + // 这些keys不一定都生效 + pub exists_keys: Vec, + pub chain_logs: HashMap>, +} + +impl IRuntime { + pub fn new() -> Self { + Self::default() + } + + // 这里只更改 allow-lan | ipv6 | log-level + pub fn patch_config(&mut self, patch: Mapping) { + if let Some(config) = self.config.as_mut() { + ["allow-lan", "ipv6", "log-level"] + .into_iter() + .for_each(|key| { + if let Some(value) = patch.get(key).to_owned() { + config.insert(key.into(), value.clone()); + } + }); + } + } +} diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs new file mode 100644 index 0000000..961280e --- /dev/null +++ b/src-tauri/src/config/verge.rs @@ -0,0 +1,224 @@ +use crate::utils::{dirs, help}; +use anyhow::Result; +use log::LevelFilter; +use serde::{Deserialize, Serialize}; + +/// ### `verge.yaml` schema +#[derive(Default, Debug, Clone, Deserialize, Serialize)] +pub struct IVerge { + /// app listening port for app singleton + pub app_singleton_port: Option, + + /// app log level + /// silent | error | warn | info | debug | trace + pub app_log_level: Option, + + // i18n + pub language: Option, + + /// `light` or `dark` or `system` + pub theme_mode: Option, + + /// enable blur mode + /// maybe be able to set the alpha + pub theme_blur: Option, + + /// enable traffic graph default is true + pub traffic_graph: Option, + + /// show memory info (only for Clash Meta) + pub enable_memory_usage: Option, + + /// clash tun mode + pub enable_tun_mode: Option, + + /// windows service mode + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_service_mode: Option, + + /// can the app auto startup + pub enable_auto_launch: Option, + + /// not show the window on launch + pub enable_silent_start: Option, + + /// set system proxy + pub enable_system_proxy: Option, + + /// enable proxy guard + pub enable_proxy_guard: Option, + + /// set system proxy bypass + pub system_proxy_bypass: Option, + + /// proxy guard duration + pub proxy_guard_duration: Option, + + /// theme setting + pub theme_setting: Option, + + /// web ui list + pub web_ui_list: Option>, + + /// clash core path + #[serde(skip_serializing_if = "Option::is_none")] + pub clash_core: Option, + + /// hotkey map + /// format: {func},{key} + pub hotkeys: Option>, + + /// 切换代理时自动关闭连接 + pub auto_close_connection: Option, + + /// 默认的延迟测试连接 + pub default_latency_test: Option, + + /// 支持关闭字段过滤,避免meta的新字段都被过滤掉,默认为真 + pub enable_clash_fields: Option, + + /// 是否使用内部的脚本支持,默认为真 + pub enable_builtin_enhanced: Option, + + /// proxy 页面布局 列数 + pub proxy_layout_column: Option, + + /// 日志清理 + /// 0: 不清理; 1: 7天; 2: 30天; 3: 90天 + pub auto_log_clean: Option, + + /// window size and position + #[serde(skip_serializing_if = "Option::is_none")] + pub window_size_position: Option>, +} + +#[derive(Default, Debug, Clone, Deserialize, Serialize)] +pub struct IVergeTheme { + pub primary_color: Option, + pub secondary_color: Option, + pub primary_text: Option, + pub secondary_text: Option, + + pub info_color: Option, + pub error_color: Option, + pub warning_color: Option, + pub success_color: Option, + + pub font_family: Option, + pub css_injection: Option, +} + +impl IVerge { + pub fn new() -> Self { + match dirs::verge_path().and_then(|path| help::read_yaml::(&path)) { + Ok(config) => config, + Err(err) => { + log::error!(target: "app", "{err}"); + Self::template() + } + } + } + + pub fn template() -> Self { + Self { + clash_core: match cfg!(feature = "default-meta") { + false => Some("clash".into()), + true => Some("clash-meta".into()), + }, + language: match cfg!(feature = "default-meta") { + false => Some("en".into()), + true => Some("zh".into()), + }, + theme_mode: Some("system".into()), + theme_blur: Some(false), + traffic_graph: Some(true), + enable_memory_usage: Some(true), + enable_auto_launch: Some(false), + enable_silent_start: Some(false), + enable_system_proxy: Some(false), + enable_proxy_guard: Some(false), + proxy_guard_duration: Some(30), + auto_close_connection: Some(true), + enable_builtin_enhanced: Some(true), + enable_clash_fields: Some(true), + auto_log_clean: Some(3), + ..Self::default() + } + } + + /// Save IVerge App Config + pub fn save_file(&self) -> Result<()> { + help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config")) + } + + /// patch verge config + /// only save to file + pub fn patch_config(&mut self, patch: IVerge) { + macro_rules! patch { + ($key: tt) => { + if patch.$key.is_some() { + self.$key = patch.$key; + } + }; + } + + patch!(app_log_level); + patch!(language); + patch!(theme_mode); + patch!(theme_blur); + patch!(traffic_graph); + patch!(enable_memory_usage); + + patch!(enable_tun_mode); + patch!(enable_service_mode); + patch!(enable_auto_launch); + patch!(enable_silent_start); + patch!(enable_system_proxy); + patch!(enable_proxy_guard); + patch!(system_proxy_bypass); + patch!(proxy_guard_duration); + + patch!(theme_setting); + patch!(web_ui_list); + patch!(clash_core); + patch!(hotkeys); + + patch!(auto_close_connection); + patch!(default_latency_test); + patch!(enable_builtin_enhanced); + patch!(proxy_layout_column); + patch!(enable_clash_fields); + patch!(auto_log_clean); + patch!(window_size_position); + } + + /// 在初始化前尝试拿到单例端口的值 + pub fn get_singleton_port() -> u16 { + #[cfg(not(feature = "verge-dev"))] + const SERVER_PORT: u16 = 33331; + #[cfg(feature = "verge-dev")] + const SERVER_PORT: u16 = 11233; + + match dirs::verge_path().and_then(|path| help::read_yaml::(&path)) { + Ok(config) => config.app_singleton_port.unwrap_or(SERVER_PORT), + Err(_) => SERVER_PORT, // 这里就不log错误了 + } + } + + /// 获取日志等级 + pub fn get_log_level(&self) -> LevelFilter { + if let Some(level) = self.app_log_level.as_ref() { + match level.to_lowercase().as_str() { + "silent" => LevelFilter::Off, + "error" => LevelFilter::Error, + "warn" => LevelFilter::Warn, + "info" => LevelFilter::Info, + "debug" => LevelFilter::Debug, + "trace" => LevelFilter::Trace, + _ => LevelFilter::Info, + } + } else { + LevelFilter::Info + } + } +} diff --git a/src-tauri/src/core/clash_api.rs b/src-tauri/src/core/clash_api.rs new file mode 100644 index 0000000..0636b41 --- /dev/null +++ b/src-tauri/src/core/clash_api.rs @@ -0,0 +1,141 @@ +use crate::config::Config; +use anyhow::{bail, Result}; +use reqwest::header::HeaderMap; +use serde::{Deserialize, Serialize}; +use serde_yaml::Mapping; +use std::collections::HashMap; + +/// PUT /configs +/// path 是绝对路径 +pub async fn put_configs(path: &str) -> Result<()> { + let (url, headers) = clash_client_info()?; + let url = format!("{url}/configs"); + + let mut data = HashMap::new(); + data.insert("path", path); + + let client = reqwest::ClientBuilder::new().no_proxy().build()?; + let builder = client.put(&url).headers(headers).json(&data); + let response = builder.send().await?; + + match response.status().as_u16() { + 204 => Ok(()), + status @ _ => { + bail!("failed to put configs with status \"{status}\"") + } + } +} + +/// PATCH /configs +pub async fn patch_configs(config: &Mapping) -> Result<()> { + let (url, headers) = clash_client_info()?; + let url = format!("{url}/configs"); + + let client = reqwest::ClientBuilder::new().no_proxy().build()?; + let builder = client.patch(&url).headers(headers.clone()).json(config); + builder.send().await?; + Ok(()) +} + +#[derive(Default, Debug, Clone, Deserialize, Serialize)] +pub struct DelayRes { + delay: u64, +} + +/// GET /proxies/{name}/delay +/// 获取代理延迟 +pub async fn get_proxy_delay(name: String, test_url: Option) -> Result { + let (url, headers) = clash_client_info()?; + let url = format!("{url}/proxies/{name}/delay"); + + let default_url = "http://www.gstatic.com/generate_204"; + let test_url = test_url + .map(|s| if s.is_empty() { default_url.into() } else { s }) + .unwrap_or(default_url.into()); + + let client = reqwest::ClientBuilder::new().no_proxy().build()?; + let builder = client + .get(&url) + .headers(headers) + .query(&[("timeout", "10000"), ("url", &test_url)]); + let response = builder.send().await?; + + Ok(response.json::().await?) +} + +/// 根据clash info获取clash服务地址和请求头 +fn clash_client_info() -> Result<(String, HeaderMap)> { + let client = { Config::clash().data().get_client_info() }; + + let server = format!("http://{}", client.server); + + let mut headers = HeaderMap::new(); + headers.insert("Content-Type", "application/json".parse()?); + + if let Some(secret) = client.secret { + let secret = format!("Bearer {}", secret).parse()?; + headers.insert("Authorization", secret); + } + + Ok((server, headers)) +} + +/// 缩短clash的日志 +pub fn parse_log(log: String) -> String { + if log.starts_with("time=") && log.len() > 33 { + return (&log[33..]).to_owned(); + } + if log.len() > 9 { + return (&log[9..]).to_owned(); + } + return log; +} + +/// 缩短clash -t的错误输出 +/// 仅适配 clash p核 8-26、clash meta 1.13.1 +pub fn parse_check_output(log: String) -> String { + let t = log.find("time="); + let m = log.find("msg="); + let mr = log.rfind('"'); + + if let (Some(_), Some(m), Some(mr)) = (t, m, mr) { + let e = match log.find("level=error msg=") { + Some(e) => e + 17, + None => m + 5, + }; + + if mr > m { + return (&log[e..mr]).to_owned(); + } + } + + let l = log.find("error="); + let r = log.find("path=").or(Some(log.len())); + + if let (Some(l), Some(r)) = (l, r) { + return (&log[(l + 6)..(r - 1)]).to_owned(); + } + + log +} + +#[test] +fn test_parse_check_output() { + let str1 = r#"xxxx\n time="2022-11-18T20:42:58+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'""#; + let str2 = r#"20:43:49 ERR [Config] configuration file test failed error=proxy 0: unsupport proxy type: hysteria path=xxx"#; + let str3 = r#" + "time="2022-11-18T21:38:01+08:00" level=info msg="Start initial configuration in progress" + time="2022-11-18T21:38:01+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'" + configuration file xxx\n + "#; + + let res1 = parse_check_output(str1.into()); + let res2 = parse_check_output(str2.into()); + let res3 = parse_check_output(str3.into()); + + println!("res1: {res1}"); + println!("res2: {res2}"); + println!("res3: {res3}"); + + assert_eq!(res1, res3); +} diff --git a/src-tauri/src/core/core.rs b/src-tauri/src/core/core.rs new file mode 100644 index 0000000..6b3053b --- /dev/null +++ b/src-tauri/src/core/core.rs @@ -0,0 +1,325 @@ +use super::{clash_api, logger::Logger}; +use crate::log_err; +use crate::{config::*, utils::dirs}; +use anyhow::{bail, Context, Result}; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use std::{fs, io::Write, sync::Arc, time::Duration}; +use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt}; +use tauri::api::process::{Command, CommandChild, CommandEvent}; +use tokio::time::sleep; + +#[derive(Debug)] +pub struct CoreManager { + sidecar: Arc>>, + + #[allow(unused)] + use_service_mode: Arc>, +} + +impl CoreManager { + pub fn global() -> &'static CoreManager { + static CORE_MANAGER: OnceCell = OnceCell::new(); + + CORE_MANAGER.get_or_init(|| CoreManager { + sidecar: Arc::new(Mutex::new(None)), + use_service_mode: Arc::new(Mutex::new(false)), + }) + } + + pub fn init(&self) -> Result<()> { + // kill old clash process + let _ = dirs::clash_pid_path() + .and_then(|path| fs::read(path).map(|p| p.to_vec()).context("")) + .and_then(|pid| String::from_utf8_lossy(&pid).parse().context("")) + .map(|pid| { + let mut system = System::new(); + system.refresh_all(); + system.process(Pid::from_u32(pid)).map(|proc| { + if proc.name().contains("clash") { + log::debug!(target: "app", "kill old clash process"); + proc.kill(); + } + }); + }); + + tauri::async_runtime::spawn(async { + // 启动clash + log_err!(Self::global().run_core().await); + }); + + Ok(()) + } + + /// 检查配置是否正确 + pub fn check_config(&self) -> Result<()> { + let config_path = Config::generate_file(ConfigType::Check)?; + let config_path = dirs::path_to_str(&config_path)?; + + let clash_core = { Config::verge().latest().clash_core.clone() }; + let clash_core = clash_core.unwrap_or("clash".into()); + + let app_dir = dirs::app_home_dir()?; + let app_dir = dirs::path_to_str(&app_dir)?; + + let output = Command::new_sidecar(clash_core)? + .args(["-t", "-d", app_dir, "-f", config_path]) + .output()?; + + if !output.status.success() { + let error = clash_api::parse_check_output(output.stdout.clone()); + let error = match error.len() > 0 { + true => error, + false => output.stdout.clone(), + }; + Logger::global().set_log(output.stdout); + bail!("{error}"); + } + + Ok(()) + } + + /// 启动核心 + pub async fn run_core(&self) -> Result<()> { + let config_path = Config::generate_file(ConfigType::Run)?; + + #[allow(unused_mut)] + let mut should_kill = match self.sidecar.lock().take() { + Some(child) => { + log::debug!(target: "app", "stop the core by sidecar"); + let _ = child.kill(); + true + } + None => false, + }; + + #[cfg(target_os = "windows")] + if *self.use_service_mode.lock() { + log::debug!(target: "app", "stop the core by service"); + log_err!(super::win_service::stop_core_by_service().await); + should_kill = true; + } + + // 这里得等一会儿 + if should_kill { + sleep(Duration::from_millis(500)).await; + } + + #[cfg(target_os = "windows")] + { + use super::win_service; + + // 服务模式 + let enable = { Config::verge().latest().enable_service_mode.clone() }; + let enable = enable.unwrap_or(false); + + *self.use_service_mode.lock() = enable; + + if enable { + // 服务模式启动失败就直接运行sidecar + log::debug!(target: "app", "try to run core in service mode"); + + match (|| async { + win_service::check_service().await?; + win_service::run_core_by_service(&config_path).await + })() + .await + { + Ok(_) => return Ok(()), + Err(err) => { + // 修改这个值,免得stop出错 + *self.use_service_mode.lock() = false; + log::error!(target: "app", "{err}"); + } + } + } + } + + let app_dir = dirs::app_home_dir()?; + let app_dir = dirs::path_to_str(&app_dir)?; + + let clash_core = { Config::verge().latest().clash_core.clone() }; + let clash_core = clash_core.unwrap_or("clash".into()); + let is_clash = clash_core == "clash"; + + let config_path = dirs::path_to_str(&config_path)?; + + // fix #212 + let args = match clash_core.as_str() { + "clash-meta" => vec!["-m", "-d", app_dir, "-f", config_path], + _ => vec!["-d", app_dir, "-f", config_path], + }; + + let cmd = Command::new_sidecar(clash_core)?; + let (mut rx, cmd_child) = cmd.args(args).spawn()?; + + // 将pid写入文件中 + crate::log_err!((|| { + let pid = cmd_child.pid(); + let path = dirs::clash_pid_path()?; + fs::File::create(path) + .context("failed to create the pid file")? + .write(format!("{pid}").as_bytes()) + .context("failed to write pid to the file")?; + >::Ok(()) + })()); + + let mut sidecar = self.sidecar.lock(); + *sidecar = Some(cmd_child); + drop(sidecar); + + tauri::async_runtime::spawn(async move { + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Stdout(line) => { + if is_clash { + let stdout = clash_api::parse_log(line.clone()); + log::info!(target: "app", "[clash]: {stdout}"); + } else { + log::info!(target: "app", "[clash]: {line}"); + }; + Logger::global().set_log(line); + } + CommandEvent::Stderr(err) => { + // let stdout = clash_api::parse_log(err.clone()); + log::error!(target: "app", "[clash]: {err}"); + Logger::global().set_log(err); + } + CommandEvent::Error(err) => { + log::error!(target: "app", "[clash]: {err}"); + Logger::global().set_log(err); + } + CommandEvent::Terminated(_) => { + log::info!(target: "app", "clash core terminated"); + let _ = CoreManager::global().recover_core(); + break; + } + _ => {} + } + } + }); + + Ok(()) + } + + /// 重启内核 + pub fn recover_core(&'static self) -> Result<()> { + // 服务模式不管 + #[cfg(target_os = "windows")] + if *self.use_service_mode.lock() { + return Ok(()); + } + + // 清空原来的sidecar值 + if let Some(sidecar) = self.sidecar.lock().take() { + let _ = sidecar.kill(); + } + + tauri::async_runtime::spawn(async move { + // 6秒之后再查看服务是否正常 (时间随便搞的) + // terminated 可能是切换内核 (切换内核已经有500ms的延迟) + sleep(Duration::from_millis(6666)).await; + + if self.sidecar.lock().is_none() { + log::info!(target: "app", "recover clash core"); + + // 重新启动app + if let Err(err) = self.run_core().await { + log::error!(target: "app", "failed to recover clash core"); + log::error!(target: "app", "{err}"); + + let _ = self.recover_core(); + } + } + }); + + Ok(()) + } + + /// 停止核心运行 + pub fn stop_core(&self) -> Result<()> { + #[cfg(target_os = "windows")] + if *self.use_service_mode.lock() { + log::debug!(target: "app", "stop the core by service"); + tauri::async_runtime::block_on(async move { + log_err!(super::win_service::stop_core_by_service().await); + }); + return Ok(()); + } + + let mut sidecar = self.sidecar.lock(); + if let Some(child) = sidecar.take() { + log::debug!(target: "app", "stop the core by sidecar"); + let _ = child.kill(); + } + Ok(()) + } + + /// 切换核心 + pub async fn change_core(&self, clash_core: Option) -> Result<()> { + let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?; + + if &clash_core != "clash" && &clash_core != "clash-meta" { + bail!("invalid clash core name \"{clash_core}\""); + } + + log::debug!(target: "app", "change core to `{clash_core}`"); + + Config::verge().draft().clash_core = Some(clash_core); + + // 更新配置 + Config::generate()?; + + self.check_config()?; + + // 清掉旧日志 + Logger::global().clear_log(); + + match self.run_core().await { + Ok(_) => { + Config::verge().apply(); + Config::runtime().apply(); + log_err!(Config::verge().latest().save_file()); + Ok(()) + } + Err(err) => { + Config::verge().discard(); + Config::runtime().discard(); + Err(err) + } + } + } + + /// 更新proxies那些 + /// 如果涉及端口和外部控制则需要重启 + pub async fn update_config(&self) -> Result<()> { + log::debug!(target: "app", "try to update clash config"); + + // 更新配置 + Config::generate()?; + + // 检查配置是否正常 + self.check_config()?; + + // 更新运行时配置 + let path = Config::generate_file(ConfigType::Run)?; + let path = dirs::path_to_str(&path)?; + + // 发送请求 发送5次 + for i in 0..5 { + match clash_api::put_configs(path).await { + Ok(_) => break, + Err(err) => { + if i < 4 { + log::info!(target: "app", "{err}"); + } else { + bail!(err); + } + } + } + sleep(Duration::from_millis(250)).await; + } + + Ok(()) + } +} diff --git a/src-tauri/src/core/handle.rs b/src-tauri/src/core/handle.rs new file mode 100644 index 0000000..5b46cea --- /dev/null +++ b/src-tauri/src/core/handle.rs @@ -0,0 +1,77 @@ +use super::tray::Tray; +use crate::log_err; +use anyhow::{bail, Result}; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use std::sync::Arc; +use tauri::{AppHandle, Manager, Window}; + +#[derive(Debug, Default, Clone)] +pub struct Handle { + pub app_handle: Arc>>, +} + +impl Handle { + pub fn global() -> &'static Handle { + static HANDLE: OnceCell = OnceCell::new(); + + HANDLE.get_or_init(|| Handle { + app_handle: Arc::new(Mutex::new(None)), + }) + } + + pub fn init(&self, app_handle: AppHandle) { + *self.app_handle.lock() = Some(app_handle); + } + + pub fn get_window(&self) -> Option { + self.app_handle + .lock() + .as_ref() + .map_or(None, |a| a.get_window("main")) + } + + pub fn refresh_clash() { + if let Some(window) = Self::global().get_window() { + log_err!(window.emit("verge://refresh-clash-config", "yes")); + } + } + + pub fn refresh_verge() { + if let Some(window) = Self::global().get_window() { + log_err!(window.emit("verge://refresh-verge-config", "yes")); + } + } + + #[allow(unused)] + pub fn refresh_profiles() { + if let Some(window) = Self::global().get_window() { + log_err!(window.emit("verge://refresh-profiles-config", "yes")); + } + } + + pub fn notice_message, M: Into>(status: S, msg: M) { + if let Some(window) = Self::global().get_window() { + log_err!(window.emit("verge://notice-message", (status.into(), msg.into()))); + } + } + + pub fn update_systray() -> Result<()> { + let app_handle = Self::global().app_handle.lock(); + if app_handle.is_none() { + bail!("update_systray unhandled error"); + } + Tray::update_systray(app_handle.as_ref().unwrap())?; + Ok(()) + } + + /// update the system tray state + pub fn update_systray_part() -> Result<()> { + let app_handle = Self::global().app_handle.lock(); + if app_handle.is_none() { + bail!("update_systray unhandled error"); + } + Tray::update_part(app_handle.as_ref().unwrap())?; + Ok(()) + } +} diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs new file mode 100644 index 0000000..cd4c149 --- /dev/null +++ b/src-tauri/src/core/hotkey.rs @@ -0,0 +1,181 @@ +use crate::{config::Config, feat, log_err}; +use anyhow::{bail, Result}; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use std::{collections::HashMap, sync::Arc}; +use tauri::{AppHandle, GlobalShortcutManager}; +use wry::application::accelerator::Accelerator; + +pub struct Hotkey { + current: Arc>>, // 保存当前的热键设置 + + app_handle: Arc>>, +} + +impl Hotkey { + pub fn global() -> &'static Hotkey { + static HOTKEY: OnceCell = OnceCell::new(); + + HOTKEY.get_or_init(|| Hotkey { + current: Arc::new(Mutex::new(Vec::new())), + app_handle: Arc::new(Mutex::new(None)), + }) + } + + pub fn init(&self, app_handle: AppHandle) -> Result<()> { + *self.app_handle.lock() = Some(app_handle); + + let verge = Config::verge(); + + if let Some(hotkeys) = verge.latest().hotkeys.as_ref() { + for hotkey in hotkeys.iter() { + let mut iter = hotkey.split(','); + let func = iter.next(); + let key = iter.next(); + + match (key, func) { + (Some(key), Some(func)) => { + log_err!(Self::check_key(key).and_then(|_| self.register(key, func))); + } + _ => { + let key = key.unwrap_or("None"); + let func = func.unwrap_or("None"); + log::error!(target: "app", "invalid hotkey `{key}`:`{func}`"); + } + } + } + *self.current.lock() = hotkeys.clone(); + } + + Ok(()) + } + + /// 检查一个键是否合法 + fn check_key(hotkey: &str) -> Result<()> { + // fix #287 + // tauri的这几个方法全部有Result expect,会panic,先检测一遍避免挂了 + if hotkey.parse::().is_err() { + bail!("invalid hotkey `{hotkey}`"); + } + Ok(()) + } + + fn get_manager(&self) -> Result { + let app_handle = self.app_handle.lock(); + if app_handle.is_none() { + bail!("failed to get the hotkey manager"); + } + Ok(app_handle.as_ref().unwrap().global_shortcut_manager()) + } + + fn register(&self, hotkey: &str, func: &str) -> Result<()> { + let mut manager = self.get_manager()?; + + if manager.is_registered(hotkey)? { + manager.unregister(hotkey)?; + } + + let f = match func.trim() { + "open_dashboard" => || feat::open_dashboard(), + "clash_mode_rule" => || feat::change_clash_mode("rule".into()), + "clash_mode_global" => || feat::change_clash_mode("global".into()), + "clash_mode_direct" => || feat::change_clash_mode("direct".into()), + "clash_mode_script" => || feat::change_clash_mode("script".into()), + "toggle_system_proxy" => || feat::toggle_system_proxy(), + "enable_system_proxy" => || feat::enable_system_proxy(), + "disable_system_proxy" => || feat::disable_system_proxy(), + "toggle_tun_mode" => || feat::toggle_tun_mode(), + "enable_tun_mode" => || feat::enable_tun_mode(), + "disable_tun_mode" => || feat::disable_tun_mode(), + + _ => bail!("invalid function \"{func}\""), + }; + + manager.register(hotkey, f)?; + log::info!(target: "app", "register hotkey {hotkey} {func}"); + Ok(()) + } + + fn unregister(&self, hotkey: &str) -> Result<()> { + self.get_manager()?.unregister(&hotkey)?; + log::info!(target: "app", "unregister hotkey {hotkey}"); + Ok(()) + } + + pub fn update(&self, new_hotkeys: Vec) -> Result<()> { + let mut current = self.current.lock(); + let old_map = Self::get_map_from_vec(¤t); + let new_map = Self::get_map_from_vec(&new_hotkeys); + + let (del, add) = Self::get_diff(old_map, new_map); + + // 先检查一遍所有新的热键是不是可以用的 + for (hotkey, _) in add.iter() { + Self::check_key(hotkey)?; + } + + del.iter().for_each(|key| { + let _ = self.unregister(key); + }); + + add.iter().for_each(|(key, func)| { + log_err!(self.register(key, func)); + }); + + *current = new_hotkeys; + Ok(()) + } + + fn get_map_from_vec<'a>(hotkeys: &'a Vec) -> HashMap<&'a str, &'a str> { + let mut map = HashMap::new(); + + hotkeys.iter().for_each(|hotkey| { + let mut iter = hotkey.split(','); + let func = iter.next(); + let key = iter.next(); + + if func.is_some() && key.is_some() { + let func = func.unwrap().trim(); + let key = key.unwrap().trim(); + map.insert(key, func); + } + }); + map + } + + fn get_diff<'a>( + old_map: HashMap<&'a str, &'a str>, + new_map: HashMap<&'a str, &'a str>, + ) -> (Vec<&'a str>, Vec<(&'a str, &'a str)>) { + let mut del_list = vec![]; + let mut add_list = vec![]; + + old_map.iter().for_each(|(&key, func)| { + match new_map.get(key) { + Some(new_func) => { + if new_func != func { + del_list.push(key); + add_list.push((key, *new_func)); + } + } + None => del_list.push(key), + }; + }); + + new_map.iter().for_each(|(&key, &func)| { + if old_map.get(key).is_none() { + add_list.push((key, func)); + } + }); + + (del_list, add_list) + } +} + +impl Drop for Hotkey { + fn drop(&mut self) { + if let Ok(mut manager) = self.get_manager() { + let _ = manager.unregister_all(); + } + } +} diff --git a/src-tauri/src/core/logger.rs b/src-tauri/src/core/logger.rs new file mode 100644 index 0000000..b426415 --- /dev/null +++ b/src-tauri/src/core/logger.rs @@ -0,0 +1,36 @@ +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use std::{collections::VecDeque, sync::Arc}; + +const LOGS_QUEUE_LEN: usize = 100; + +pub struct Logger { + log_data: Arc>>, +} + +impl Logger { + pub fn global() -> &'static Logger { + static LOGGER: OnceCell = OnceCell::new(); + + LOGGER.get_or_init(|| Logger { + log_data: Arc::new(Mutex::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))), + }) + } + + pub fn get_log(&self) -> VecDeque { + self.log_data.lock().clone() + } + + pub fn set_log(&self, text: String) { + let mut logs = self.log_data.lock(); + if logs.len() > LOGS_QUEUE_LEN { + logs.pop_front(); + } + logs.push_back(text); + } + + pub fn clear_log(&self) { + let mut logs = self.log_data.lock(); + logs.clear(); + } +} diff --git a/src-tauri/src/core/manager.rs b/src-tauri/src/core/manager.rs new file mode 100644 index 0000000..fdb9269 --- /dev/null +++ b/src-tauri/src/core/manager.rs @@ -0,0 +1,82 @@ +use std::borrow::Cow; + +/// 给clash内核的tun模式授权 +#[cfg(any(target_os = "macos", target_os = "linux"))] +pub fn grant_permission(core: String) -> anyhow::Result<()> { + use std::process::Command; + use tauri::utils::platform::current_exe; + + let path = current_exe()?.with_file_name(core).canonicalize()?; + let path = path.display().to_string(); + + log::debug!("grant_permission path: {path}"); + + #[cfg(target_os = "macos")] + let output = { + // the path of clash /Applications/Clash Verge.app/Contents/MacOS/clash + // https://apple.stackexchange.com/questions/82967/problem-with-empty-spaces-when-executing-shell-commands-in-applescript + // let path = escape(&path); + let path = path.replace(' ', "\\\\ "); + let shell = format!("chown root:admin {path}\nchmod +sx {path}"); + let command = format!(r#"do shell script "{shell}" with administrator privileges"#); + Command::new("osascript") + .args(vec!["-e", &command]) + .output()? + }; + + #[cfg(target_os = "linux")] + let output = { + let path = path.replace(' ', "\\ "); // 避免路径中有空格 + let shell = format!("setcap cap_net_bind_service,cap_net_admin=+ep {path}"); + + let sudo = match Command::new("which").arg("pkexec").output() { + Ok(output) => { + if output.stdout.is_empty() { + "sudo" + } else { + "pkexec" + } + } + Err(_) => "sudo", + }; + + Command::new(sudo).arg("sh").arg("-c").arg(shell).output()? + }; + + if output.status.success() { + Ok(()) + } else { + let stderr = std::str::from_utf8(&output.stderr).unwrap_or(""); + anyhow::bail!("{stderr}"); + } +} + +#[allow(unused)] +pub fn escape<'a>(text: &'a str) -> Cow<'a, str> { + let bytes = text.as_bytes(); + + let mut owned = None; + + for pos in 0..bytes.len() { + let special = match bytes[pos] { + b' ' => Some(b' '), + _ => None, + }; + if let Some(s) = special { + if owned.is_none() { + owned = Some(bytes[0..pos].to_owned()); + } + owned.as_mut().unwrap().push(b'\\'); + owned.as_mut().unwrap().push(b'\\'); + owned.as_mut().unwrap().push(s); + } else if let Some(owned) = owned.as_mut() { + owned.push(bytes[pos]); + } + } + + if let Some(owned) = owned { + unsafe { Cow::Owned(String::from_utf8_unchecked(owned)) } + } else { + unsafe { Cow::Borrowed(std::str::from_utf8_unchecked(bytes)) } + } +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs new file mode 100644 index 0000000..4221721 --- /dev/null +++ b/src-tauri/src/core/mod.rs @@ -0,0 +1,12 @@ +pub mod clash_api; +mod core; +pub mod handle; +pub mod hotkey; +pub mod logger; +pub mod manager; +pub mod sysopt; +pub mod timer; +pub mod tray; +pub mod win_service; + +pub use self::core::*; diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs new file mode 100644 index 0000000..c43114e --- /dev/null +++ b/src-tauri/src/core/sysopt.rs @@ -0,0 +1,304 @@ +use crate::{config::Config, log_err}; +use anyhow::{anyhow, Result}; +use auto_launch::{AutoLaunch, AutoLaunchBuilder}; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use std::sync::Arc; +use sysproxy::Sysproxy; +use tauri::{async_runtime::Mutex as TokioMutex, utils::platform::current_exe}; + +pub struct Sysopt { + /// current system proxy setting + cur_sysproxy: Arc>>, + + /// record the original system proxy + /// recover it when exit + old_sysproxy: Arc>>, + + /// helps to auto launch the app + auto_launch: Arc>>, + + /// record whether the guard async is running or not + guard_state: Arc>, +} + +#[cfg(target_os = "windows")] +static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;"; +#[cfg(target_os = "linux")] +static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,::1"; +#[cfg(target_os = "macos")] +static DEFAULT_BYPASS: &str = "127.0.0.1,localhost,"; + +impl Sysopt { + pub fn global() -> &'static Sysopt { + static SYSOPT: OnceCell = OnceCell::new(); + + SYSOPT.get_or_init(|| Sysopt { + cur_sysproxy: Arc::new(Mutex::new(None)), + old_sysproxy: Arc::new(Mutex::new(None)), + auto_launch: Arc::new(Mutex::new(None)), + guard_state: Arc::new(TokioMutex::new(false)), + }) + } + + /// init the sysproxy + pub fn init_sysproxy(&self) -> Result<()> { + let port = { Config::clash().latest().get_mixed_port() }; + + let (enable, bypass) = { + let verge = Config::verge(); + let verge = verge.latest(); + ( + verge.enable_system_proxy.clone().unwrap_or(false), + verge.system_proxy_bypass.clone(), + ) + }; + + let current = Sysproxy { + enable, + host: String::from("127.0.0.1"), + port, + bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()), + }; + + if enable { + let old = Sysproxy::get_system_proxy().map_or(None, |p| Some(p)); + current.set_system_proxy()?; + + *self.old_sysproxy.lock() = old; + *self.cur_sysproxy.lock() = Some(current); + } + + // run the system proxy guard + self.guard_proxy(); + Ok(()) + } + + /// update the system proxy + pub fn update_sysproxy(&self) -> Result<()> { + let mut cur_sysproxy = self.cur_sysproxy.lock(); + let old_sysproxy = self.old_sysproxy.lock(); + + if cur_sysproxy.is_none() || old_sysproxy.is_none() { + drop(cur_sysproxy); + drop(old_sysproxy); + return self.init_sysproxy(); + } + + let (enable, bypass) = { + let verge = Config::verge(); + let verge = verge.latest(); + ( + verge.enable_system_proxy.clone().unwrap_or(false), + verge.system_proxy_bypass.clone(), + ) + }; + let mut sysproxy = cur_sysproxy.take().unwrap(); + + sysproxy.enable = enable; + sysproxy.bypass = bypass.unwrap_or(DEFAULT_BYPASS.into()); + + sysproxy.set_system_proxy()?; + *cur_sysproxy = Some(sysproxy); + + Ok(()) + } + + /// reset the sysproxy + pub fn reset_sysproxy(&self) -> Result<()> { + let mut cur_sysproxy = self.cur_sysproxy.lock(); + let mut old_sysproxy = self.old_sysproxy.lock(); + + let cur_sysproxy = cur_sysproxy.take(); + + if let Some(mut old) = old_sysproxy.take() { + // 如果原代理和当前代理 端口一致,就disable关闭,否则就恢复原代理设置 + // 当前没有设置代理的时候,不确定旧设置是否和当前一致,全关了 + let port_same = cur_sysproxy.map_or(true, |cur| old.port == cur.port); + + if old.enable && port_same { + old.enable = false; + log::info!(target: "app", "reset proxy by disabling the original proxy"); + } else { + log::info!(target: "app", "reset proxy to the original proxy"); + } + + old.set_system_proxy()?; + } else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur_sysproxy { + // 没有原代理,就按现在的代理设置disable即可 + log::info!(target: "app", "reset proxy by disabling the current proxy"); + cur.enable = false; + cur.set_system_proxy()?; + } else { + log::info!(target: "app", "reset proxy with no action"); + } + + Ok(()) + } + + /// init the auto launch + pub fn init_launch(&self) -> Result<()> { + let enable = { Config::verge().latest().enable_auto_launch.clone() }; + let enable = enable.unwrap_or(false); + + let app_exe = current_exe()?; + let app_exe = dunce::canonicalize(app_exe)?; + let app_name = app_exe + .file_stem() + .and_then(|f| f.to_str()) + .ok_or(anyhow!("failed to get file stem"))?; + + let app_path = app_exe + .as_os_str() + .to_str() + .ok_or(anyhow!("failed to get app_path"))? + .to_string(); + + // fix issue #26 + #[cfg(target_os = "windows")] + let app_path = format!("\"{app_path}\""); + + // use the /Applications/Clash Verge.app path + #[cfg(target_os = "macos")] + let app_path = (|| -> Option { + let path = std::path::PathBuf::from(&app_path); + let path = path.parent()?.parent()?.parent()?; + let extension = path.extension()?.to_str()?; + match extension == "app" { + true => Some(path.as_os_str().to_str()?.to_string()), + false => None, + } + })() + .unwrap_or(app_path); + + // fix #403 + #[cfg(target_os = "linux")] + let app_path = { + use crate::core::handle::Handle; + use tauri::Manager; + + let handle = Handle::global(); + match handle.app_handle.lock().as_ref() { + Some(app_handle) => { + let appimage = app_handle.env().appimage; + appimage + .and_then(|p| p.to_str().map(|s| s.to_string())) + .unwrap_or(app_path) + } + None => app_path, + } + }; + + let auto = AutoLaunchBuilder::new() + .set_app_name(app_name) + .set_app_path(&app_path) + .build()?; + + // 避免在开发时将自启动关了 + #[cfg(feature = "verge-dev")] + if !enable { + return Ok(()); + } + + #[cfg(target_os = "macos")] + { + if enable && !auto.is_enabled().unwrap_or(false) { + // 避免重复设置登录项 + let _ = auto.disable(); + auto.enable()?; + } else if !enable { + let _ = auto.disable(); + } + } + + #[cfg(not(target_os = "macos"))] + if enable { + auto.enable()?; + } + + *self.auto_launch.lock() = Some(auto); + + Ok(()) + } + + /// update the startup + pub fn update_launch(&self) -> Result<()> { + let auto_launch = self.auto_launch.lock(); + + if auto_launch.is_none() { + drop(auto_launch); + return self.init_launch(); + } + let enable = { Config::verge().latest().enable_auto_launch.clone() }; + let enable = enable.unwrap_or(false); + let auto_launch = auto_launch.as_ref().unwrap(); + + match enable { + true => auto_launch.enable()?, + false => log_err!(auto_launch.disable()), // 忽略关闭的错误 + }; + + Ok(()) + } + + /// launch a system proxy guard + /// read config from file directly + pub fn guard_proxy(&self) { + use tokio::time::{sleep, Duration}; + + let guard_state = self.guard_state.clone(); + + tauri::async_runtime::spawn(async move { + // if it is running, exit + let mut state = guard_state.lock().await; + if *state { + return; + } + *state = true; + drop(state); + + // default duration is 10s + let mut wait_secs = 10u64; + + loop { + sleep(Duration::from_secs(wait_secs)).await; + + let (enable, guard, guard_duration, bypass) = { + let verge = Config::verge(); + let verge = verge.latest(); + ( + verge.enable_system_proxy.clone().unwrap_or(false), + verge.enable_proxy_guard.clone().unwrap_or(false), + verge.proxy_guard_duration.clone().unwrap_or(10), + verge.system_proxy_bypass.clone(), + ) + }; + + // stop loop + if !enable || !guard { + break; + } + + // update duration + wait_secs = guard_duration; + + log::debug!(target: "app", "try to guard the system proxy"); + + let port = { Config::clash().latest().get_mixed_port() }; + + let sysproxy = Sysproxy { + enable: true, + host: "127.0.0.1".into(), + port, + bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()), + }; + + log_err!(sysproxy.set_system_proxy()); + } + + let mut state = guard_state.lock().await; + *state = false; + drop(state); + }); + } +} diff --git a/src-tauri/src/core/timer.rs b/src-tauri/src/core/timer.rs new file mode 100644 index 0000000..1b40f0f --- /dev/null +++ b/src-tauri/src/core/timer.rs @@ -0,0 +1,184 @@ +use crate::config::Config; +use crate::feat; +use anyhow::{Context, Result}; +use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder}; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use std::collections::HashMap; +use std::sync::Arc; + +type TaskID = u64; + +pub struct Timer { + /// cron manager + delay_timer: Arc>, + + /// save the current state + timer_map: Arc>>, + + /// increment id + timer_count: Arc>, +} + +impl Timer { + pub fn global() -> &'static Timer { + static TIMER: OnceCell = OnceCell::new(); + + TIMER.get_or_init(|| Timer { + delay_timer: Arc::new(Mutex::new(DelayTimerBuilder::default().build())), + timer_map: Arc::new(Mutex::new(HashMap::new())), + timer_count: Arc::new(Mutex::new(1)), + }) + } + + /// restore timer + pub fn init(&self) -> Result<()> { + self.refresh()?; + + let cur_timestamp = chrono::Local::now().timestamp(); + + let timer_map = self.timer_map.lock(); + let delay_timer = self.delay_timer.lock(); + + Config::profiles().latest().get_items().map(|items| { + items + .iter() + .filter_map(|item| { + // mins to seconds + let interval = ((item.option.as_ref()?.update_interval?) as i64) * 60; + let updated = item.updated? as i64; + + if interval > 0 && cur_timestamp - updated >= interval { + Some(item) + } else { + None + } + }) + .for_each(|item| { + if let Some(uid) = item.uid.as_ref() { + if let Some((task_id, _)) = timer_map.get(uid) { + crate::log_err!(delay_timer.advance_task(*task_id)); + } + } + }) + }); + + Ok(()) + } + + /// Correctly update all cron tasks + pub fn refresh(&self) -> Result<()> { + let diff_map = self.gen_diff(); + + let mut timer_map = self.timer_map.lock(); + let mut delay_timer = self.delay_timer.lock(); + + for (uid, diff) in diff_map.into_iter() { + match diff { + DiffFlag::Del(tid) => { + let _ = timer_map.remove(&uid); + crate::log_err!(delay_timer.remove_task(tid)); + } + DiffFlag::Add(tid, val) => { + let _ = timer_map.insert(uid.clone(), (tid, val)); + crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val)); + } + DiffFlag::Mod(tid, val) => { + let _ = timer_map.insert(uid.clone(), (tid, val)); + crate::log_err!(delay_timer.remove_task(tid)); + crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val)); + } + } + } + + Ok(()) + } + + /// generate a uid -> update_interval map + fn gen_map(&self) -> HashMap { + let mut new_map = HashMap::new(); + + if let Some(items) = Config::profiles().latest().get_items() { + for item in items.iter() { + if item.option.is_some() { + let option = item.option.as_ref().unwrap(); + let interval = option.update_interval.unwrap_or(0); + + if interval > 0 { + new_map.insert(item.uid.clone().unwrap(), interval); + } + } + } + } + + new_map + } + + /// generate the diff map for refresh + fn gen_diff(&self) -> HashMap { + let mut diff_map = HashMap::new(); + + let timer_map = self.timer_map.lock(); + + let new_map = self.gen_map(); + let cur_map = &timer_map; + + cur_map.iter().for_each(|(uid, (tid, val))| { + let new_val = new_map.get(uid).unwrap_or(&0); + + if *new_val == 0 { + diff_map.insert(uid.clone(), DiffFlag::Del(*tid)); + } else if new_val != val { + diff_map.insert(uid.clone(), DiffFlag::Mod(*tid, *new_val)); + } + }); + + let mut count = self.timer_count.lock(); + + new_map.iter().for_each(|(uid, val)| { + if cur_map.get(uid).is_none() { + diff_map.insert(uid.clone(), DiffFlag::Add(*count, *val)); + + *count += 1; + } + }); + + diff_map + } + + /// add a cron task + fn add_task( + &self, + delay_timer: &mut DelayTimer, + uid: String, + tid: TaskID, + minutes: u64, + ) -> Result<()> { + let task = TaskBuilder::default() + .set_task_id(tid) + .set_maximum_parallel_runnable_num(1) + .set_frequency_repeated_by_minutes(minutes) + // .set_frequency_repeated_by_seconds(minutes) // for test + .spawn_async_routine(move || Self::async_task(uid.to_owned())) + .context("failed to create timer task")?; + + delay_timer + .add_task(task) + .context("failed to add timer task")?; + + Ok(()) + } + + /// the task runner + async fn async_task(uid: String) { + log::info!(target: "app", "running timer task `{uid}`"); + crate::log_err!(feat::update_profile(uid, None).await); + } +} + +#[derive(Debug)] +enum DiffFlag { + Del(TaskID), + Add(TaskID, u64), + Mod(TaskID, u64), +} diff --git a/src-tauri/src/core/tray.rs b/src-tauri/src/core/tray.rs new file mode 100644 index 0000000..307e35d --- /dev/null +++ b/src-tauri/src/core/tray.rs @@ -0,0 +1,175 @@ +use crate::{cmds, config::Config, feat, utils::resolve}; +use anyhow::Result; +use tauri::{ + api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, + SystemTraySubmenu, +}; + +pub struct Tray {} + +impl Tray { + pub fn tray_menu(app_handle: &AppHandle) -> SystemTrayMenu { + let zh = { Config::verge().latest().language == Some("zh".into()) }; + + let version = app_handle.package_info().version.to_string(); + + macro_rules! t { + ($en: expr, $zh: expr) => { + if zh { + $zh + } else { + $en + } + }; + } + + SystemTrayMenu::new() + .add_item(CustomMenuItem::new( + "open_window", + t!("Dashboard", "打开面板"), + )) + .add_native_item(SystemTrayMenuItem::Separator) + .add_item(CustomMenuItem::new( + "rule_mode", + t!("Rule Mode", "规则模式"), + )) + .add_item(CustomMenuItem::new( + "global_mode", + t!("Global Mode", "全局模式"), + )) + .add_item(CustomMenuItem::new( + "direct_mode", + t!("Direct Mode", "直连模式"), + )) + .add_item(CustomMenuItem::new( + "script_mode", + t!("Script Mode", "脚本模式"), + )) + .add_native_item(SystemTrayMenuItem::Separator) + .add_item(CustomMenuItem::new( + "system_proxy", + t!("System Proxy", "系统代理"), + )) + .add_item(CustomMenuItem::new("tun_mode", t!("TUN Mode", "Tun 模式"))) + .add_item(CustomMenuItem::new( + "copy_env", + t!("Copy Env", "复制环境变量"), + )) + .add_submenu(SystemTraySubmenu::new( + t!("Open Dir", "打开目录"), + SystemTrayMenu::new() + .add_item(CustomMenuItem::new( + "open_app_dir", + t!("App Dir", "应用目录"), + )) + .add_item(CustomMenuItem::new( + "open_core_dir", + t!("Core Dir", "内核目录"), + )) + .add_item(CustomMenuItem::new( + "open_logs_dir", + t!("Logs Dir", "日志目录"), + )), + )) + .add_submenu(SystemTraySubmenu::new( + t!("More", "更多"), + SystemTrayMenu::new() + .add_item(CustomMenuItem::new( + "restart_clash", + t!("Restart Clash", "重启 Clash"), + )) + .add_item(CustomMenuItem::new( + "restart_app", + t!("Restart App", "重启应用"), + )) + .add_item( + CustomMenuItem::new("app_version", format!("Version {version}")).disabled(), + ), + )) + .add_native_item(SystemTrayMenuItem::Separator) + .add_item(CustomMenuItem::new("quit", t!("Quit", "退出")).accelerator("CmdOrControl+Q")) + } + + pub fn update_systray(app_handle: &AppHandle) -> Result<()> { + app_handle + .tray_handle() + .set_menu(Tray::tray_menu(app_handle))?; + Tray::update_part(app_handle)?; + Ok(()) + } + + pub fn update_part(app_handle: &AppHandle) -> Result<()> { + let mode = { + Config::clash() + .latest() + .0 + .get("mode") + .map(|val| val.as_str().unwrap_or("rule")) + .unwrap_or("rule") + .to_owned() + }; + + let tray = app_handle.tray_handle(); + + let _ = tray.get_item("rule_mode").set_selected(mode == "rule"); + let _ = tray.get_item("global_mode").set_selected(mode == "global"); + let _ = tray.get_item("direct_mode").set_selected(mode == "direct"); + let _ = tray.get_item("script_mode").set_selected(mode == "script"); + + let verge = Config::verge(); + let verge = verge.latest(); + let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); + let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); + + #[cfg(target_os = "windows")] + { + let indication_icon = if *system_proxy { + include_bytes!("../../icons/win-tray-icon-activated.png").to_vec() + } else { + include_bytes!("../../icons/win-tray-icon.png").to_vec() + }; + + let _ = tray.set_icon(tauri::Icon::Raw(indication_icon)); + } + + let _ = tray.get_item("system_proxy").set_selected(*system_proxy); + let _ = tray.get_item("tun_mode").set_selected(*tun_mode); + + Ok(()) + } + + pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) { + match event { + SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { + mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => { + let mode = &mode[0..mode.len() - 5]; + feat::change_clash_mode(mode.into()); + } + + "open_window" => resolve::create_window(app_handle), + "system_proxy" => feat::toggle_system_proxy(), + "tun_mode" => feat::toggle_tun_mode(), + "copy_env" => feat::copy_clash_env(), + "open_app_dir" => crate::log_err!(cmds::open_app_dir()), + "open_core_dir" => crate::log_err!(cmds::open_core_dir()), + "open_logs_dir" => crate::log_err!(cmds::open_logs_dir()), + "restart_clash" => feat::restart_clash_core(), + "restart_app" => api::process::restart(&app_handle.env()), + "quit" => { + let _ = resolve::save_window_size_position(app_handle, true); + + resolve::resolve_reset(); + api::process::kill_children(); + app_handle.exit(0); + std::process::exit(0); + } + _ => {} + }, + #[cfg(target_os = "windows")] + SystemTrayEvent::LeftClick { .. } => { + resolve::create_window(app_handle); + } + _ => {} + } + } +} diff --git a/src-tauri/src/core/win_service.rs b/src-tauri/src/core/win_service.rs new file mode 100644 index 0000000..865895b --- /dev/null +++ b/src-tauri/src/core/win_service.rs @@ -0,0 +1,178 @@ +#![cfg(target_os = "windows")] + +use crate::config::Config; +use crate::utils::dirs; +use anyhow::{bail, Context, Result}; +use deelevate::{PrivilegeLevel, Token}; +use runas::Command as RunasCommand; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::os::windows::process::CommandExt; +use std::path::PathBuf; +use std::time::Duration; +use std::{env::current_exe, process::Command as StdCommand}; +use tokio::time::sleep; + +const SERVICE_URL: &str = "http://127.0.0.1:33211"; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ResponseBody { + pub core_type: Option, + pub bin_path: String, + pub config_dir: String, + pub log_file: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct JsonResponse { + pub code: u64, + pub msg: String, + pub data: Option, +} + +/// Install the Clash Verge Service +/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程 +pub async fn install_service() -> Result<()> { + let binary_path = dirs::service_path()?; + let install_path = binary_path.with_file_name("install-service.exe"); + + if !install_path.exists() { + bail!("installer exe not found"); + } + + let token = Token::with_current_process()?; + let level = token.privilege_level()?; + + let status = match level { + PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).show(false).status()?, + _ => StdCommand::new(install_path) + .creation_flags(0x08000000) + .status()?, + }; + + if !status.success() { + bail!( + "failed to install service with status {}", + status.code().unwrap() + ); + } + + Ok(()) +} + +/// Uninstall the Clash Verge Service +/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程 +pub async fn uninstall_service() -> Result<()> { + let binary_path = dirs::service_path()?; + let uninstall_path = binary_path.with_file_name("uninstall-service.exe"); + + if !uninstall_path.exists() { + bail!("uninstaller exe not found"); + } + + let token = Token::with_current_process()?; + let level = token.privilege_level()?; + + let status = match level { + PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?, + _ => StdCommand::new(uninstall_path) + .creation_flags(0x08000000) + .status()?, + }; + + if !status.success() { + bail!( + "failed to uninstall service with status {}", + status.code().unwrap() + ); + } + + Ok(()) +} + +/// check the windows service status +pub async fn check_service() -> Result { + let url = format!("{SERVICE_URL}/get_clash"); + let response = reqwest::ClientBuilder::new() + .no_proxy() + .build()? + .get(url) + .send() + .await + .context("failed to connect to the Clash Verge Service")? + .json::() + .await + .context("failed to parse the Clash Verge Service response")?; + + Ok(response) +} + +/// start the clash by service +pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> { + let status = check_service().await?; + + if status.code == 0 { + stop_core_by_service().await?; + sleep(Duration::from_secs(1)).await; + } + + let clash_core = { Config::verge().latest().clash_core.clone() }; + let clash_core = clash_core.unwrap_or("clash".into()); + + let clash_bin = format!("{clash_core}.exe"); + let bin_path = current_exe()?.with_file_name(clash_bin); + let bin_path = dirs::path_to_str(&bin_path)?; + + let config_dir = dirs::app_home_dir()?; + let config_dir = dirs::path_to_str(&config_dir)?; + + let log_path = dirs::service_log_file()?; + let log_path = dirs::path_to_str(&log_path)?; + + let config_file = dirs::path_to_str(config_file)?; + + let mut map = HashMap::new(); + map.insert("core_type", clash_core.as_str()); + map.insert("bin_path", bin_path); + map.insert("config_dir", config_dir); + map.insert("config_file", config_file); + map.insert("log_file", log_path); + + let url = format!("{SERVICE_URL}/start_clash"); + let res = reqwest::ClientBuilder::new() + .no_proxy() + .build()? + .post(url) + .json(&map) + .send() + .await? + .json::() + .await + .context("failed to connect to the Clash Verge Service")?; + + if res.code != 0 { + bail!(res.msg); + } + + Ok(()) +} + +/// stop the clash by service +pub(super) async fn stop_core_by_service() -> Result<()> { + let url = format!("{SERVICE_URL}/stop_clash"); + let res = reqwest::ClientBuilder::new() + .no_proxy() + .build()? + .post(url) + .send() + .await? + .json::() + .await + .context("failed to connect to the Clash Verge Service")?; + + if res.code != 0 { + bail!(res.msg); + } + + Ok(()) +} diff --git a/src-tauri/src/enhance/builtin/meta_guard.js b/src-tauri/src/enhance/builtin/meta_guard.js new file mode 100644 index 0000000..be4183b --- /dev/null +++ b/src-tauri/src/enhance/builtin/meta_guard.js @@ -0,0 +1,6 @@ +function main(params) { + if (params.mode === "script") { + params.mode = "rule"; + } + return params; +} diff --git a/src-tauri/src/enhance/builtin/meta_hy_alpn.js b/src-tauri/src/enhance/builtin/meta_hy_alpn.js new file mode 100644 index 0000000..da1fac0 --- /dev/null +++ b/src-tauri/src/enhance/builtin/meta_hy_alpn.js @@ -0,0 +1,10 @@ +function main(params) { + if (Array.isArray(params.proxies)) { + params.proxies.forEach((p, i) => { + if (p.type === "hysteria" && typeof p.alpn === "string") { + params.proxies[i].alpn = [p.alpn]; + } + }); + } + return params; +} diff --git a/src-tauri/src/enhance/chain.rs b/src-tauri/src/enhance/chain.rs new file mode 100644 index 0000000..75c61fb --- /dev/null +++ b/src-tauri/src/enhance/chain.rs @@ -0,0 +1,89 @@ +use crate::{ + config::PrfItem, + utils::{dirs, help}, +}; +use serde_yaml::Mapping; +use std::fs; + +#[derive(Debug, Clone)] +pub struct ChainItem { + pub uid: String, + pub data: ChainType, +} + +#[derive(Debug, Clone)] +pub enum ChainType { + Merge(Mapping), + Script(String), +} + +#[derive(Debug, Clone)] +pub enum ChainSupport { + Clash, + ClashMeta, + All, +} + +impl From<&PrfItem> for Option { + fn from(item: &PrfItem) -> Self { + let itype = item.itype.as_ref()?.as_str(); + let file = item.file.clone()?; + let uid = item.uid.clone().unwrap_or("".into()); + let path = dirs::app_profiles_dir().ok()?.join(file); + + if !path.exists() { + return None; + } + + match itype { + "script" => Some(ChainItem { + uid, + data: ChainType::Script(fs::read_to_string(path).ok()?), + }), + "merge" => Some(ChainItem { + uid, + data: ChainType::Merge(help::read_merge_mapping(&path).ok()?), + }), + _ => None, + } + } +} + +impl ChainItem { + /// 内建支持一些脚本 + pub fn builtin() -> Vec<(ChainSupport, ChainItem)> { + // meta 的一些处理 + let meta_guard = + ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js")); + + // meta 1.13.2 alpn string 转 数组 + let hy_alpn = + ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js")); + + vec![ + (ChainSupport::ClashMeta, hy_alpn), + (ChainSupport::ClashMeta, meta_guard), + ] + } + + pub fn to_script, D: Into>(uid: U, data: D) -> Self { + Self { + uid: uid.into(), + data: ChainType::Script(data.into()), + } + } +} + +impl ChainSupport { + pub fn is_support(&self, core: Option<&String>) -> bool { + match core { + Some(core) => match (self, core.as_str()) { + (ChainSupport::All, _) => true, + (ChainSupport::Clash, "clash") => true, + (ChainSupport::ClashMeta, "clash-meta") => true, + _ => false, + }, + None => true, + } + } +} diff --git a/src-tauri/src/enhance/field.rs b/src-tauri/src/enhance/field.rs new file mode 100644 index 0000000..2130b41 --- /dev/null +++ b/src-tauri/src/enhance/field.rs @@ -0,0 +1,155 @@ +use serde_yaml::{Mapping, Value}; +use std::collections::HashSet; + +pub const HANDLE_FIELDS: [&str; 9] = [ + "mode", + "port", + "socks-port", + "mixed-port", + "allow-lan", + "log-level", + "ipv6", + "secret", + "external-controller", +]; + +pub const DEFAULT_FIELDS: [&str; 5] = [ + "proxies", + "proxy-groups", + "proxy-providers", + "rules", + "rule-providers", +]; + +pub const OTHERS_FIELDS: [&str; 30] = [ + "dns", + "tun", + "ebpf", + "hosts", + "script", + "profile", + "payload", + "tunnels", + "auto-redir", + "experimental", + "interface-name", + "routing-mark", + "redir-port", + "tproxy-port", + "iptables", + "external-ui", + "bind-address", + "authentication", + "tls", // meta + "sniffer", // meta + "geox-url", // meta + "listeners", // meta + "sub-rules", // meta + "geodata-mode", // meta + "unified-delay", // meta + "tcp-concurrent", // meta + "enable-process", // meta + "find-process-mode", // meta + "external-controller-tls", // meta + "global-client-fingerprint", // meta +]; + +pub fn use_clash_fields() -> Vec { + DEFAULT_FIELDS + .into_iter() + .chain(HANDLE_FIELDS) + .chain(OTHERS_FIELDS) + .map(|s| s.to_string()) + .collect() +} + +pub fn use_valid_fields(mut valid: Vec) -> Vec { + let others = Vec::from(OTHERS_FIELDS); + + valid.iter_mut().for_each(|s| s.make_ascii_lowercase()); + valid + .into_iter() + .filter(|s| others.contains(&s.as_str())) + .chain(DEFAULT_FIELDS.iter().map(|s| s.to_string())) + .collect() +} + +pub fn use_filter(config: Mapping, filter: &Vec, enable: bool) -> Mapping { + if !enable { + return config; + } + + let mut ret = Mapping::new(); + + for (key, value) in config.into_iter() { + if let Some(key) = key.as_str() { + if filter.contains(&key.to_string()) { + ret.insert(Value::from(key), value); + } + } + } + ret +} + +pub fn use_lowercase(config: Mapping) -> Mapping { + let mut ret = Mapping::new(); + + for (key, value) in config.into_iter() { + if let Some(key_str) = key.as_str() { + let mut key_str = String::from(key_str); + key_str.make_ascii_lowercase(); + ret.insert(Value::from(key_str), value); + } + } + ret +} + +pub fn use_sort(config: Mapping, enable_filter: bool) -> Mapping { + let mut ret = Mapping::new(); + + HANDLE_FIELDS + .into_iter() + .chain(OTHERS_FIELDS) + .chain(DEFAULT_FIELDS) + .for_each(|key| { + let key = Value::from(key); + config.get(&key).map(|value| { + ret.insert(key, value.clone()); + }); + }); + + if !enable_filter { + let supported_keys: HashSet<&str> = HANDLE_FIELDS + .into_iter() + .chain(OTHERS_FIELDS) + .chain(DEFAULT_FIELDS) + .collect(); + + let config_keys: HashSet<&str> = config + .keys() + .filter_map(|e| e.as_str()) + .into_iter() + .collect(); + + config_keys.difference(&supported_keys).for_each(|&key| { + let key = Value::from(key); + config.get(&key).map(|value| { + ret.insert(key, value.clone()); + }); + }); + } + + ret +} + +pub fn use_keys(config: &Mapping) -> Vec { + config + .iter() + .filter_map(|(key, _)| key.as_str()) + .map(|s| { + let mut s = s.to_string(); + s.make_ascii_lowercase(); + return s; + }) + .collect() +} diff --git a/src-tauri/src/enhance/merge.rs b/src-tauri/src/enhance/merge.rs new file mode 100644 index 0000000..20342c9 --- /dev/null +++ b/src-tauri/src/enhance/merge.rs @@ -0,0 +1,92 @@ +use super::{use_filter, use_lowercase}; +use serde_yaml::{self, Mapping, Sequence, Value}; + +const MERGE_FIELDS: [&str; 6] = [ + "prepend-rules", + "append-rules", + "prepend-proxies", + "append-proxies", + "prepend-proxy-groups", + "append-proxy-groups", +]; + +pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping { + // 直接覆盖原字段 + use_lowercase(merge.clone()) + .into_iter() + .for_each(|(key, value)| { + config.insert(key, value); + }); + + let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string()); + let merge = use_filter(merge, &merge_list.collect(), true); + + ["rules", "proxies", "proxy-groups"] + .iter() + .for_each(|key_str| { + let key_val = Value::from(key_str.to_string()); + + let mut list = Sequence::default(); + list = config.get(&key_val).map_or(list.clone(), |val| { + val.as_sequence().map_or(list, |v| v.clone()) + }); + + let pre_key = Value::from(format!("prepend-{key_str}")); + let post_key = Value::from(format!("append-{key_str}")); + + if let Some(pre_val) = merge.get(&pre_key) { + if pre_val.is_sequence() { + let mut pre_val = pre_val.as_sequence().unwrap().clone(); + pre_val.extend(list); + list = pre_val; + } + } + + if let Some(post_val) = merge.get(&post_key) { + if post_val.is_sequence() { + list.extend(post_val.as_sequence().unwrap().clone()); + } + } + + config.insert(key_val, Value::from(list)); + }); + config +} + +#[test] +fn test_merge() -> anyhow::Result<()> { + let merge = r" + prepend-rules: + - prepend + - 1123123 + append-rules: + - append + prepend-proxies: + - 9999 + append-proxies: + - 1111 + rules: + - replace + proxy-groups: + - 123781923810 + tun: + enable: true + dns: + enable: true + "; + + let config = r" + rules: + - aaaaa + script1: test + "; + + let merge = serde_yaml::from_str::(merge)?; + let config = serde_yaml::from_str::(config)?; + + let result = serde_yaml::to_string(&use_merge(merge, config))?; + + println!("{result}"); + + Ok(()) +} diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs new file mode 100644 index 0000000..0a41400 --- /dev/null +++ b/src-tauri/src/enhance/mod.rs @@ -0,0 +1,126 @@ +mod chain; +mod field; +mod merge; +mod script; +mod tun; + +pub(self) use self::field::*; + +use self::chain::*; +use self::merge::*; +use self::script::*; +use self::tun::*; +use crate::config::Config; +use serde_yaml::Mapping; +use std::collections::HashMap; +use std::collections::HashSet; + +type ResultLog = Vec<(String, String)>; + +/// Enhance mode +/// 返回最终配置、该配置包含的键、和script执行的结果 +pub fn enhance() -> (Mapping, Vec, HashMap) { + // config.yaml 的配置 + let clash_config = { Config::clash().latest().0.clone() }; + + let (clash_core, enable_tun, enable_builtin, enable_filter) = { + let verge = Config::verge(); + let verge = verge.latest(); + ( + verge.clash_core.clone(), + verge.enable_tun_mode.clone().unwrap_or(false), + verge.enable_builtin_enhanced.clone().unwrap_or(true), + verge.enable_clash_fields.clone().unwrap_or(true), + ) + }; + + // 从profiles里拿东西 + let (mut config, chain, valid) = { + let profiles = Config::profiles(); + let profiles = profiles.latest(); + + let current = profiles.current_mapping().unwrap_or(Mapping::new()); + + let chain = match profiles.chain.as_ref() { + Some(chain) => chain + .iter() + .filter_map(|uid| profiles.get_item(uid).ok()) + .filter_map(|item| >::from(item)) + .collect::>(), + None => vec![], + }; + + let valid = profiles.valid.clone().unwrap_or(vec![]); + + (current, chain, valid) + }; + + let mut result_map = HashMap::new(); // 保存脚本日志 + let mut exists_keys = use_keys(&config); // 保存出现过的keys + + let valid = use_valid_fields(valid); + config = use_filter(config, &valid, enable_filter); + + // 处理用户的profile + chain.into_iter().for_each(|item| match item.data { + ChainType::Merge(merge) => { + exists_keys.extend(use_keys(&merge)); + config = use_merge(merge, config.to_owned()); + config = use_filter(config.to_owned(), &valid, enable_filter); + } + ChainType::Script(script) => { + let mut logs = vec![]; + + match use_script(script, config.to_owned()) { + Ok((res_config, res_logs)) => { + exists_keys.extend(use_keys(&res_config)); + config = use_filter(res_config, &valid, enable_filter); + logs.extend(res_logs); + } + Err(err) => logs.push(("exception".into(), err.to_string())), + } + + result_map.insert(item.uid, logs); + } + }); + + // 合并默认的config + for (key, value) in clash_config.into_iter() { + config.insert(key, value); + } + + let clash_fields = use_clash_fields(); + + // 内建脚本最后跑 + if enable_builtin { + ChainItem::builtin() + .into_iter() + .filter(|(s, _)| s.is_support(clash_core.as_ref())) + .map(|(_, c)| c) + .for_each(|item| { + log::debug!(target: "app", "run builtin script {}", item.uid); + + match item.data { + ChainType::Script(script) => match use_script(script, config.to_owned()) { + Ok((res_config, _)) => { + config = use_filter(res_config, &clash_fields, enable_filter); + } + Err(err) => { + log::error!(target: "app", "builtin script error `{err}`"); + } + }, + _ => {} + } + }); + } + + config = use_filter(config, &clash_fields, enable_filter); + config = use_tun(config, enable_tun); + config = use_sort(config, enable_filter); + + let mut exists_set = HashSet::new(); + exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s))); + exists_keys = exists_set.into_iter().collect(); + + (config, exists_keys, result_map) +} diff --git a/src-tauri/src/enhance/script.rs b/src-tauri/src/enhance/script.rs new file mode 100644 index 0000000..97b34c2 --- /dev/null +++ b/src-tauri/src/enhance/script.rs @@ -0,0 +1,94 @@ +use super::use_lowercase; +use anyhow::Result; +use serde_yaml::Mapping; + +pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> { + use rquickjs::{Context, Func, Runtime}; + use std::sync::{Arc, Mutex}; + + let runtime = Runtime::new().unwrap(); + let context = Context::full(&runtime).unwrap(); + let outputs = Arc::new(Mutex::new(vec![])); + + let copy_outputs = outputs.clone(); + let result = context.with(|ctx| -> Result { + ctx.globals().set( + "__verge_log__", + Func::from(move |level: String, data: String| { + let mut out = copy_outputs.lock().unwrap(); + out.push((level, data)); + }), + )?; + + ctx.eval( + r#"var console = Object.freeze({ + log(data){__verge_log__("log",JSON.stringify(data))}, + info(data){__verge_log__("info",JSON.stringify(data))}, + error(data){__verge_log__("error",JSON.stringify(data))}, + debug(data){__verge_log__("debug",JSON.stringify(data))}, + });"#, + )?; + + let config = use_lowercase(config.clone()); + let config_str = serde_json::to_string(&config)?; + + let code = format!( + r#"try{{ + {script}; + JSON.stringify(main({config_str})||'') + }} catch(err) {{ + `__error_flag__ ${{err.toString()}}` + }}"# + ); + let result: String = ctx.eval(code.as_str())?; + if result.starts_with("__error_flag__") { + anyhow::bail!(result[15..].to_owned()); + } + if result == "\"\"" { + anyhow::bail!("main function should return object"); + } + return Ok(serde_json::from_str::(result.as_str())?); + }); + + let mut out = outputs.lock().unwrap(); + match result { + Ok(config) => Ok((use_lowercase(config), out.to_vec())), + Err(err) => { + out.push(("exception".into(), err.to_string())); + Ok((config, out.to_vec())) + } + } +} + +#[test] +fn test_script() { + let script = r#" + function main(config) { + if (Array.isArray(config.rules)) { + config.rules = [...config.rules, "add"]; + } + console.log(config); + config.proxies = ["111"]; + return config; + } + "#; + + let config = r#" + rules: + - 111 + - 222 + tun: + enable: false + dns: + enable: false + "#; + + let config = serde_yaml::from_str(config).unwrap(); + let (config, results) = use_script(script.into(), config).unwrap(); + + let config_str = serde_yaml::to_string(&config).unwrap(); + + println!("{config_str}"); + + dbg!(results); +} diff --git a/src-tauri/src/enhance/tun.rs b/src-tauri/src/enhance/tun.rs new file mode 100644 index 0000000..b72823a --- /dev/null +++ b/src-tauri/src/enhance/tun.rs @@ -0,0 +1,81 @@ +use serde_yaml::{Mapping, Value}; + +macro_rules! revise { + ($map: expr, $key: expr, $val: expr) => { + let ret_key = Value::String($key.into()); + $map.insert(ret_key, Value::from($val)); + }; +} + +// if key not exists then append value +macro_rules! append { + ($map: expr, $key: expr, $val: expr) => { + let ret_key = Value::String($key.into()); + if !$map.contains_key(&ret_key) { + $map.insert(ret_key, Value::from($val)); + } + }; +} + +pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping { + let tun_key = Value::from("tun"); + let tun_val = config.get(&tun_key); + + if !enable && tun_val.is_none() { + return config; + } + + let mut tun_val = tun_val.map_or(Mapping::new(), |val| { + val.as_mapping().cloned().unwrap_or(Mapping::new()) + }); + + revise!(tun_val, "enable", enable); + if enable { + append!(tun_val, "stack", "gvisor"); + append!(tun_val, "dns-hijack", vec!["any:53"]); + append!(tun_val, "auto-route", true); + append!(tun_val, "auto-detect-interface", true); + } + + revise!(config, "tun", tun_val); + + if enable { + use_dns_for_tun(config) + } else { + config + } +} + +fn use_dns_for_tun(mut config: Mapping) -> Mapping { + let dns_key = Value::from("dns"); + let dns_val = config.get(&dns_key); + + let mut dns_val = dns_val.map_or(Mapping::new(), |val| { + val.as_mapping().cloned().unwrap_or(Mapping::new()) + }); + + // 开启tun将同时开启dns + revise!(dns_val, "enable", true); + + append!(dns_val, "enhanced-mode", "fake-ip"); + append!(dns_val, "fake-ip-range", "198.18.0.1/16"); + append!( + dns_val, + "nameserver", + vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"] + ); + append!(dns_val, "fallback", vec![] as Vec<&str>); + + #[cfg(target_os = "windows")] + append!( + dns_val, + "fake-ip-filter", + vec![ + "dns.msftncsi.com", + "www.msftncsi.com", + "www.msftconnecttest.com" + ] + ); + revise!(config, "dns", dns_val); + config +} diff --git a/src-tauri/src/feat.rs b/src-tauri/src/feat.rs new file mode 100644 index 0000000..a446c27 --- /dev/null +++ b/src-tauri/src/feat.rs @@ -0,0 +1,341 @@ +//! +//! feat mod 里的函数主要用于 +//! - hotkey 快捷键 +//! - timer 定时器 +//! - cmds 页面调用 +//! +use crate::config::*; +use crate::core::*; +use crate::log_err; +use crate::utils::resolve; +use anyhow::{bail, Result}; +use serde_yaml::{Mapping, Value}; +use wry::application::clipboard::Clipboard; + +// 打开面板 +pub fn open_dashboard() { + let handle = handle::Handle::global(); + let app_handle = handle.app_handle.lock(); + if let Some(app_handle) = app_handle.as_ref() { + resolve::create_window(app_handle); + } +} + +// 重启clash +pub fn restart_clash_core() { + tauri::async_runtime::spawn(async { + match CoreManager::global().run_core().await { + Ok(_) => { + handle::Handle::refresh_clash(); + handle::Handle::notice_message("set_config::ok", "ok"); + } + Err(err) => { + handle::Handle::notice_message("set_config::error", format!("{err}")); + log::error!(target:"app", "{err}"); + } + } + }); +} + +// 切换模式 rule/global/direct/script mode +pub fn change_clash_mode(mode: String) { + let mut mapping = Mapping::new(); + mapping.insert(Value::from("mode"), mode.clone().into()); + + tauri::async_runtime::spawn(async move { + log::debug!(target: "app", "change clash mode to {mode}"); + + match clash_api::patch_configs(&mapping).await { + Ok(_) => { + // 更新配置 + Config::clash().data().patch_config(mapping); + + if Config::clash().data().save_config().is_ok() { + handle::Handle::refresh_clash(); + log_err!(handle::Handle::update_systray_part()); + } + } + Err(err) => log::error!(target: "app", "{err}"), + } + }); +} + +// 切换系统代理 +pub fn toggle_system_proxy() { + let enable = Config::verge().draft().enable_system_proxy.clone(); + let enable = enable.unwrap_or(false); + + tauri::async_runtime::spawn(async move { + match patch_verge(IVerge { + enable_system_proxy: Some(!enable), + ..IVerge::default() + }) + .await + { + Ok(_) => handle::Handle::refresh_verge(), + Err(err) => log::error!(target: "app", "{err}"), + } + }); +} + +// 打开系统代理 +pub fn enable_system_proxy() { + tauri::async_runtime::spawn(async { + match patch_verge(IVerge { + enable_system_proxy: Some(true), + ..IVerge::default() + }) + .await + { + Ok(_) => handle::Handle::refresh_verge(), + Err(err) => log::error!(target: "app", "{err}"), + } + }); +} + +// 关闭系统代理 +pub fn disable_system_proxy() { + tauri::async_runtime::spawn(async { + match patch_verge(IVerge { + enable_system_proxy: Some(false), + ..IVerge::default() + }) + .await + { + Ok(_) => handle::Handle::refresh_verge(), + Err(err) => log::error!(target: "app", "{err}"), + } + }); +} + +// 切换tun模式 +pub fn toggle_tun_mode() { + let enable = Config::verge().data().enable_tun_mode.clone(); + let enable = enable.unwrap_or(false); + + tauri::async_runtime::spawn(async move { + match patch_verge(IVerge { + enable_tun_mode: Some(!enable), + ..IVerge::default() + }) + .await + { + Ok(_) => handle::Handle::refresh_verge(), + Err(err) => log::error!(target: "app", "{err}"), + } + }); +} + +// 打开tun模式 +pub fn enable_tun_mode() { + tauri::async_runtime::spawn(async { + match patch_verge(IVerge { + enable_tun_mode: Some(true), + ..IVerge::default() + }) + .await + { + Ok(_) => handle::Handle::refresh_verge(), + Err(err) => log::error!(target: "app", "{err}"), + } + }); +} + +// 关闭tun模式 +pub fn disable_tun_mode() { + tauri::async_runtime::spawn(async { + match patch_verge(IVerge { + enable_tun_mode: Some(false), + ..IVerge::default() + }) + .await + { + Ok(_) => handle::Handle::refresh_verge(), + Err(err) => log::error!(target: "app", "{err}"), + } + }); +} + +/// 修改clash的配置 +pub async fn patch_clash(patch: Mapping) -> Result<()> { + Config::clash().draft().patch_config(patch.clone()); + + match { + let mixed_port = patch.get("mixed-port"); + if mixed_port.is_some() { + let changed = mixed_port != Config::clash().data().0.get("mixed-port"); + // 检查端口占用 + if changed { + if let Some(port) = mixed_port.clone().unwrap().as_u64() { + if !port_scanner::local_port_available(port as u16) { + Config::clash().discard(); + bail!("port already in use"); + } + } + } + }; + + // 激活配置 + if mixed_port.is_some() + || patch.get("secret").is_some() + || patch.get("external-controller").is_some() + { + Config::generate()?; + CoreManager::global().run_core().await?; + handle::Handle::refresh_clash(); + } + + // 更新系统代理 + if mixed_port.is_some() { + log_err!(sysopt::Sysopt::global().init_sysproxy()); + } + + if patch.get("mode").is_some() { + log_err!(handle::Handle::update_systray_part()); + } + + Config::runtime().latest().patch_config(patch); + + >::Ok(()) + } { + Ok(()) => { + Config::clash().apply(); + Config::clash().data().save_config()?; + Ok(()) + } + Err(err) => { + Config::clash().discard(); + Err(err) + } + } +} + +/// 修改verge的配置 +/// 一般都是一个个的修改 +pub async fn patch_verge(patch: IVerge) -> Result<()> { + Config::verge().draft().patch_config(patch.clone()); + + let tun_mode = patch.enable_tun_mode; + let auto_launch = patch.enable_auto_launch; + let system_proxy = patch.enable_system_proxy; + let proxy_bypass = patch.system_proxy_bypass; + let language = patch.language; + + match { + #[cfg(target_os = "windows")] + { + let service_mode = patch.enable_service_mode; + + if service_mode.is_some() { + log::debug!(target: "app", "change service mode to {}", service_mode.unwrap()); + + Config::generate()?; + CoreManager::global().run_core().await?; + } else if tun_mode.is_some() { + update_core_config().await?; + } + } + + #[cfg(not(target_os = "windows"))] + if tun_mode.is_some() { + update_core_config().await?; + } + + if auto_launch.is_some() { + sysopt::Sysopt::global().update_launch()?; + } + if system_proxy.is_some() || proxy_bypass.is_some() { + sysopt::Sysopt::global().update_sysproxy()?; + sysopt::Sysopt::global().guard_proxy(); + } + + if let Some(true) = patch.enable_proxy_guard { + sysopt::Sysopt::global().guard_proxy(); + } + + if let Some(hotkeys) = patch.hotkeys { + hotkey::Hotkey::global().update(hotkeys)?; + } + + if language.is_some() { + handle::Handle::update_systray()?; + } else if system_proxy.or(tun_mode).is_some() { + handle::Handle::update_systray_part()?; + } + + >::Ok(()) + } { + Ok(()) => { + Config::verge().apply(); + Config::verge().data().save_file()?; + Ok(()) + } + Err(err) => { + Config::verge().discard(); + Err(err) + } + } +} + +/// 更新某个profile +/// 如果更新当前配置就激活配置 +pub async fn update_profile(uid: String, option: Option) -> Result<()> { + let url_opt = { + let profiles = Config::profiles(); + let profiles = profiles.latest(); + let item = profiles.get_item(&uid)?; + let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote"); + + if !is_remote { + None // 直接更新 + } else if item.url.is_none() { + bail!("failed to get the profile item url"); + } else { + Some((item.url.clone().unwrap(), item.option.clone())) + } + }; + + let should_update = match url_opt { + Some((url, opt)) => { + let merged_opt = PrfOption::merge(opt, option); + let item = PrfItem::from_url(&url, None, None, merged_opt).await?; + + let profiles = Config::profiles(); + let mut profiles = profiles.latest(); + profiles.update_item(uid.clone(), item)?; + + Some(uid) == profiles.get_current() + } + None => true, + }; + + if should_update { + update_core_config().await?; + } + + Ok(()) +} + +/// 更新配置 +async fn update_core_config() -> Result<()> { + match CoreManager::global().update_config().await { + Ok(_) => { + handle::Handle::refresh_clash(); + handle::Handle::notice_message("set_config::ok", "ok"); + Ok(()) + } + Err(err) => { + handle::Handle::notice_message("set_config::error", format!("{err}")); + Err(err) + } + } +} + +/// copy env variable +pub fn copy_clash_env() { + let port = { Config::clash().data().get_client_info().port }; + let text = format!("export https_proxy=http://127.0.0.1:{port} http_proxy=http://127.0.0.1:{port} all_proxy=socks5://127.0.0.1:{port}"); + + let mut cliboard = Clipboard::new(); + cliboard.write_text(text); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..0c20bec --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,142 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +mod cmds; +mod config; +mod core; +mod enhance; +mod feat; +mod utils; + +use crate::utils::{init, resolve, server}; +use tauri::{api, SystemTray}; + +fn main() -> std::io::Result<()> { + // 单例检测 + if server::check_singleton().is_err() { + println!("app exists"); + return Ok(()); + } + + crate::log_err!(init::init_config()); + + #[allow(unused_mut)] + let mut builder = tauri::Builder::default() + .system_tray(SystemTray::new()) + .setup(|app| Ok(resolve::resolve_setup(app))) + .on_system_tray_event(core::tray::Tray::on_system_tray_event) + .invoke_handler(tauri::generate_handler![ + // common + cmds::get_sys_proxy, + cmds::open_app_dir, + cmds::open_logs_dir, + cmds::open_web_url, + cmds::open_core_dir, + // cmds::kill_sidecar, + cmds::restart_sidecar, + cmds::grant_permission, + // clash + cmds::get_clash_info, + cmds::get_clash_logs, + cmds::patch_clash_config, + cmds::change_clash_core, + cmds::get_runtime_config, + cmds::get_runtime_yaml, + cmds::get_runtime_exists, + cmds::get_runtime_logs, + // verge + cmds::get_verge_config, + cmds::patch_verge_config, + // cmds::update_hotkeys, + // profile + cmds::get_profiles, + cmds::enhance_profiles, + cmds::patch_profiles_config, + cmds::view_profile, + cmds::patch_profile, + cmds::create_profile, + cmds::import_profile, + cmds::update_profile, + cmds::delete_profile, + cmds::read_profile_file, + cmds::save_profile_file, + // service mode + cmds::service::check_service, + cmds::service::install_service, + cmds::service::uninstall_service, + // clash api + cmds::clash_api_get_proxy_delay + ]); + + #[cfg(target_os = "macos")] + { + use tauri::{Menu, MenuItem, Submenu}; + + builder = builder.menu( + Menu::new().add_submenu(Submenu::new( + "Edit", + Menu::new() + .add_native_item(MenuItem::Undo) + .add_native_item(MenuItem::Redo) + .add_native_item(MenuItem::Copy) + .add_native_item(MenuItem::Paste) + .add_native_item(MenuItem::Cut) + .add_native_item(MenuItem::SelectAll) + .add_native_item(MenuItem::CloseWindow) + .add_native_item(MenuItem::Quit), + )), + ); + } + + let app = builder + .build(tauri::generate_context!()) + .expect("error while running tauri application"); + + app.run(|app_handle, e| match e { + tauri::RunEvent::ExitRequested { api, .. } => { + api.prevent_exit(); + } + tauri::RunEvent::Exit => { + resolve::resolve_reset(); + api::process::kill_children(); + app_handle.exit(0); + } + #[cfg(target_os = "macos")] + tauri::RunEvent::WindowEvent { label, event, .. } => { + use tauri::Manager; + + if label == "main" { + match event { + tauri::WindowEvent::CloseRequested { api, .. } => { + api.prevent_close(); + let _ = resolve::save_window_size_position(&app_handle, true); + + app_handle.get_window("main").map(|win| { + let _ = win.hide(); + }); + } + _ => {} + } + } + } + #[cfg(not(target_os = "macos"))] + tauri::RunEvent::WindowEvent { label, event, .. } => { + if label == "main" { + match event { + tauri::WindowEvent::CloseRequested { .. } => { + let _ = resolve::save_window_size_position(&app_handle, true); + } + tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => { + let _ = resolve::save_window_size_position(&app_handle, false); + } + _ => {} + } + } + } + _ => {} + }); + + Ok(()) +} diff --git a/src-tauri/src/utils/dirs.rs b/src-tauri/src/utils/dirs.rs new file mode 100644 index 0000000..696831d --- /dev/null +++ b/src-tauri/src/utils/dirs.rs @@ -0,0 +1,159 @@ +use anyhow::Result; +use std::path::PathBuf; +use tauri::{ + api::path::{home_dir, resource_dir}, + Env, PackageInfo, +}; + +#[cfg(not(feature = "verge-dev"))] +static APP_DIR: &str = "clash-verge"; +#[cfg(feature = "verge-dev")] +static APP_DIR: &str = "clash-verge-dev"; + +static CLASH_CONFIG: &str = "config.yaml"; +static VERGE_CONFIG: &str = "verge.yaml"; +static PROFILE_YAML: &str = "profiles.yaml"; + +static mut RESOURCE_DIR: Option = None; + +/// portable flag +#[allow(unused)] +static mut PORTABLE_FLAG: bool = false; + +pub static mut APP_VERSION: &str = "v1.2.0"; + +/// initialize portable flag +#[cfg(target_os = "windows")] +pub unsafe fn init_portable_flag() -> Result<()> { + use tauri::utils::platform::current_exe; + + let exe = current_exe()?; + + if let Some(dir) = exe.parent() { + let dir = PathBuf::from(dir).join(".config/PORTABLE"); + + if dir.exists() { + PORTABLE_FLAG = true; + } + } + + Ok(()) +} + +/// get the verge app home dir +pub fn app_home_dir() -> Result { + #[cfg(target_os = "windows")] + unsafe { + use tauri::utils::platform::current_exe; + + if !PORTABLE_FLAG { + Ok(home_dir() + .ok_or(anyhow::anyhow!("failed to get app home dir"))? + .join(".config") + .join(APP_DIR)) + } else { + let app_exe = current_exe()?; + let app_exe = dunce::canonicalize(app_exe)?; + let app_dir = app_exe + .parent() + .ok_or(anyhow::anyhow!("failed to get the portable app dir"))?; + Ok(PathBuf::from(app_dir).join(".config").join(APP_DIR)) + } + } + + #[cfg(not(target_os = "windows"))] + Ok(home_dir() + .ok_or(anyhow::anyhow!("failed to get the app home dir"))? + .join(".config") + .join(APP_DIR)) +} + +/// get the resources dir +pub fn app_resources_dir(package_info: &PackageInfo) -> Result { + let res_dir = resource_dir(package_info, &Env::default()) + .ok_or(anyhow::anyhow!("failed to get the resource dir"))? + .join("resources"); + + unsafe { + RESOURCE_DIR = Some(res_dir.clone()); + + let ver = package_info.version.to_string(); + let ver_str = format!("v{ver}"); + APP_VERSION = Box::leak(Box::new(ver_str)); + } + + Ok(res_dir) +} + +/// profiles dir +pub fn app_profiles_dir() -> Result { + Ok(app_home_dir()?.join("profiles")) +} + +/// logs dir +pub fn app_logs_dir() -> Result { + Ok(app_home_dir()?.join("logs")) +} + +pub fn clash_path() -> Result { + Ok(app_home_dir()?.join(CLASH_CONFIG)) +} + +pub fn verge_path() -> Result { + Ok(app_home_dir()?.join(VERGE_CONFIG)) +} + +pub fn profiles_path() -> Result { + Ok(app_home_dir()?.join(PROFILE_YAML)) +} + +#[allow(unused)] +pub fn app_res_dir() -> Result { + unsafe { + Ok(RESOURCE_DIR + .clone() + .ok_or(anyhow::anyhow!("failed to get the resource dir"))?) + } +} + +pub fn clash_pid_path() -> Result { + unsafe { + Ok(RESOURCE_DIR + .clone() + .ok_or(anyhow::anyhow!("failed to get the resource dir"))? + .join("clash.pid")) + } +} + +#[cfg(windows)] +pub fn service_path() -> Result { + unsafe { + let res_dir = RESOURCE_DIR + .clone() + .ok_or(anyhow::anyhow!("failed to get the resource dir"))?; + Ok(res_dir.join("clash-verge-service.exe")) + } +} + +#[cfg(windows)] +pub fn service_log_file() -> Result { + use chrono::Local; + + let log_dir = app_logs_dir()?.join("service"); + + let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string(); + let log_file = format!("{}.log", local_time); + let log_file = log_dir.join(log_file); + + let _ = std::fs::create_dir_all(&log_dir); + + Ok(log_file) +} + +pub fn path_to_str(path: &PathBuf) -> Result<&str> { + let path_str = path + .as_os_str() + .to_str() + .ok_or(anyhow::anyhow!("failed to get path from {:?}", path))?; + Ok(path_str) +} diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs new file mode 100644 index 0000000..03a89f4 --- /dev/null +++ b/src-tauri/src/utils/help.rs @@ -0,0 +1,172 @@ +use anyhow::{anyhow, bail, Context, Result}; +use nanoid::nanoid; +use serde::{de::DeserializeOwned, Serialize}; +use serde_yaml::{Mapping, Value}; +use std::{fs, path::PathBuf, str::FromStr}; + +/// read data from yaml as struct T +pub fn read_yaml(path: &PathBuf) -> Result { + if !path.exists() { + bail!("file not found \"{}\"", path.display()); + } + + let yaml_str = fs::read_to_string(&path) + .with_context(|| format!("failed to read the file \"{}\"", path.display()))?; + + serde_yaml::from_str::(&yaml_str).with_context(|| { + format!( + "failed to read the file with yaml format \"{}\"", + path.display() + ) + }) +} + +/// read mapping from yaml fix #165 +pub fn read_merge_mapping(path: &PathBuf) -> Result { + let mut val: Value = read_yaml(path)?; + val.apply_merge() + .with_context(|| format!("failed to apply merge \"{}\"", path.display()))?; + + Ok(val + .as_mapping() + .ok_or(anyhow!( + "failed to transform to yaml mapping \"{}\"", + path.display() + ))? + .to_owned()) +} + +/// save the data to the file +/// can set `prefix` string to add some comments +pub fn save_yaml(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> { + let data_str = serde_yaml::to_string(data)?; + + let yaml_str = match prefix { + Some(prefix) => format!("{prefix}\n\n{data_str}"), + None => data_str, + }; + + let path_str = path.as_os_str().to_string_lossy().to_string(); + fs::write(path, yaml_str.as_bytes()) + .with_context(|| format!("failed to save file \"{path_str}\"")) +} + +const ALPHABET: [char; 62] = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', + 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', + 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', + 'V', 'W', 'X', 'Y', 'Z', +]; + +/// generate the uid +pub fn get_uid(prefix: &str) -> String { + let id = nanoid!(11, &ALPHABET); + format!("{prefix}{id}") +} + +/// parse the string +/// xxx=123123; => 123123 +pub fn parse_str(target: &str, key: &str) -> Option { + target.find(key).and_then(|idx| { + let idx = idx + key.len(); + let value = &target[idx..]; + + match value.split(';').nth(0) { + Some(value) => value.trim().parse(), + None => value.trim().parse(), + } + .ok() + }) +} + +/// open file +/// use vscode by default +pub fn open_file(path: PathBuf) -> Result<()> { + #[cfg(target_os = "macos")] + let code = "Visual Studio Code"; + #[cfg(not(target_os = "macos"))] + let code = "code"; + + // use vscode first + if let Err(err) = open::with(&path, code) { + log::error!(target: "app", "failed to open file with VScode `{err}`"); + // default open + open::that(path)?; + } + + Ok(()) +} + +#[macro_export] +macro_rules! error { + ($result: expr) => { + log::error!(target: "app", "{}", $result); + }; +} + +#[macro_export] +macro_rules! log_err { + ($result: expr) => { + if let Err(err) = $result { + log::error!(target: "app", "{err}"); + } + }; + + ($result: expr, $err_str: expr) => { + if let Err(_) = $result { + log::error!(target: "app", "{}", $err_str); + } + }; +} + +#[macro_export] +macro_rules! trace_err { + ($result: expr, $err_str: expr) => { + if let Err(err) = $result { + log::trace!(target: "app", "{}, err {}", $err_str, err); + } + } +} + +/// wrap the anyhow error +/// transform the error to String +#[macro_export] +macro_rules! wrap_err { + ($stat: expr) => { + match $stat { + Ok(a) => Ok(a), + Err(err) => { + log::error!(target: "app", "{}", err.to_string()); + Err(format!("{}", err.to_string())) + } + } + }; +} + +/// return the string literal error +#[macro_export] +macro_rules! ret_err { + ($str: expr) => { + return Err($str.into()) + }; +} + +#[test] +fn test_parse_value() { + let test_1 = "upload=111; download=2222; total=3333; expire=444"; + let test_2 = "attachment; filename=Clash.yaml"; + + assert_eq!(parse_str::(test_1, "upload=").unwrap(), 111); + assert_eq!(parse_str::(test_1, "download=").unwrap(), 2222); + assert_eq!(parse_str::(test_1, "total=").unwrap(), 3333); + assert_eq!(parse_str::(test_1, "expire=").unwrap(), 444); + assert_eq!( + parse_str::(test_2, "filename=").unwrap(), + format!("Clash.yaml") + ); + + assert_eq!(parse_str::(test_1, "aaa="), None); + assert_eq!(parse_str::(test_1, "upload1="), None); + assert_eq!(parse_str::(test_1, "expire1="), None); + assert_eq!(parse_str::(test_2, "attachment="), None); +} diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs new file mode 100644 index 0000000..e9bcd42 --- /dev/null +++ b/src-tauri/src/utils/init.rs @@ -0,0 +1,243 @@ +use crate::config::*; +use crate::utils::{dirs, help}; +use anyhow::Result; +use chrono::{DateTime, Local}; +use log::LevelFilter; +use log4rs::append::console::ConsoleAppender; +use log4rs::append::file::FileAppender; +use log4rs::config::{Appender, Logger, Root}; +use log4rs::encode::pattern::PatternEncoder; +use std::fs::{self, DirEntry}; +use std::str::FromStr; +use tauri::PackageInfo; + +/// initialize this instance's log file +fn init_log() -> Result<()> { + let log_dir = dirs::app_logs_dir()?; + if !log_dir.exists() { + let _ = fs::create_dir_all(&log_dir); + } + + let log_level = Config::verge().data().get_log_level(); + if log_level == LevelFilter::Off { + return Ok(()); + } + + let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string(); + let log_file = format!("{}.log", local_time); + let log_file = log_dir.join(log_file); + + let log_pattern = match log_level { + LevelFilter::Trace => "{d(%Y-%m-%d %H:%M:%S)} {l} [{M}] - {m}{n}", + _ => "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}", + }; + + let encode = Box::new(PatternEncoder::new(log_pattern)); + + let stdout = ConsoleAppender::builder().encoder(encode.clone()).build(); + let tofile = FileAppender::builder().encoder(encode).build(log_file)?; + + let mut logger_builder = Logger::builder(); + let mut root_builder = Root::builder(); + + let log_more = log_level == LevelFilter::Trace || log_level == LevelFilter::Debug; + + #[cfg(feature = "verge-dev")] + { + logger_builder = logger_builder.appenders(["file", "stdout"]); + if log_more { + root_builder = root_builder.appenders(["file", "stdout"]); + } else { + root_builder = root_builder.appenders(["stdout"]); + } + } + #[cfg(not(feature = "verge-dev"))] + { + logger_builder = logger_builder.appenders(["file"]); + if log_more { + root_builder = root_builder.appenders(["file"]); + } + } + + let (config, _) = log4rs::config::Config::builder() + .appender(Appender::builder().build("stdout", Box::new(stdout))) + .appender(Appender::builder().build("file", Box::new(tofile))) + .logger(logger_builder.additive(false).build("app", log_level)) + .build_lossy(root_builder.build(log_level)); + + log4rs::init_config(config)?; + + Ok(()) +} + +/// 删除log文件 +pub fn delete_log() -> Result<()> { + let log_dir = dirs::app_logs_dir()?; + if !log_dir.exists() { + return Ok(()); + } + + let auto_log_clean = { + let verge = Config::verge(); + let verge = verge.data(); + verge.auto_log_clean.clone().unwrap_or(0) + }; + + let day = match auto_log_clean { + 1 => 7, + 2 => 30, + 3 => 90, + _ => return Ok(()), + }; + + log::debug!(target: "app", "try to delete log files, day: {day}"); + + // %Y-%m-%d to NaiveDateTime + let parse_time_str = |s: &str| { + let sa: Vec<&str> = s.split('-').collect(); + if sa.len() != 4 { + return Err(anyhow::anyhow!("invalid time str")); + } + + let year = i32::from_str(sa[0])?; + let month = u32::from_str(sa[1])?; + let day = u32::from_str(sa[2])?; + let time = chrono::NaiveDate::from_ymd_opt(year, month, day) + .ok_or(anyhow::anyhow!("invalid time str"))? + .and_hms_opt(0, 0, 0) + .ok_or(anyhow::anyhow!("invalid time str"))?; + Ok(time) + }; + + let process_file = |file: DirEntry| -> Result<()> { + let file_name = file.file_name(); + let file_name = file_name.to_str().unwrap_or_default(); + + if file_name.ends_with(".log") { + let now = Local::now(); + let created_time = parse_time_str(&file_name[0..file_name.len() - 4])?; + let file_time = DateTime::::from_local(created_time, now.offset().clone()); + + let duration = now.signed_duration_since(file_time); + if duration.num_days() > day { + let file_path = file.path(); + let _ = fs::remove_file(file_path); + log::info!(target: "app", "delete log file: {file_name}"); + } + } + Ok(()) + }; + + for file in fs::read_dir(&log_dir)? { + if let Ok(file) = file { + let _ = process_file(file); + } + } + Ok(()) +} + +/// Initialize all the config files +/// before tauri setup +pub fn init_config() -> Result<()> { + #[cfg(target_os = "windows")] + unsafe { + let _ = dirs::init_portable_flag(); + } + + let _ = init_log(); + let _ = delete_log(); + + crate::log_err!(dirs::app_home_dir().map(|app_dir| { + if !app_dir.exists() { + let _ = fs::create_dir_all(&app_dir); + } + })); + + crate::log_err!(dirs::app_profiles_dir().map(|profiles_dir| { + if !profiles_dir.exists() { + let _ = fs::create_dir_all(&profiles_dir); + } + })); + + crate::log_err!(dirs::clash_path().map(|path| { + if !path.exists() { + help::save_yaml(&path, &IClashTemp::template().0, Some("# Clash Verge"))?; + } + >::Ok(()) + })); + + crate::log_err!(dirs::verge_path().map(|path| { + if !path.exists() { + help::save_yaml(&path, &IVerge::template(), Some("# Clash Verge"))?; + } + >::Ok(()) + })); + + crate::log_err!(dirs::profiles_path().map(|path| { + if !path.exists() { + help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?; + } + >::Ok(()) + })); + + Ok(()) +} + +/// initialize app resources +/// after tauri setup +pub fn init_resources(package_info: &PackageInfo) -> Result<()> { + let app_dir = dirs::app_home_dir()?; + let res_dir = dirs::app_resources_dir(package_info)?; + + if !app_dir.exists() { + let _ = fs::create_dir_all(&app_dir); + } + if !res_dir.exists() { + let _ = fs::create_dir_all(&res_dir); + } + + #[cfg(target_os = "windows")] + let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat", "wintun.dll"]; + #[cfg(not(target_os = "windows"))] + let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"]; + + // copy the resource file + // if the source file is newer than the destination file, copy it over + for file in file_list.iter() { + let src_path = res_dir.join(file); + let dest_path = app_dir.join(file); + + let handle_copy = || { + match fs::copy(&src_path, &dest_path) { + Ok(_) => log::debug!(target: "app", "resources copied '{file}'"), + Err(err) => { + log::error!(target: "app", "failed to copy resources '{file}', {err}") + } + }; + }; + + if src_path.exists() && !dest_path.exists() { + handle_copy(); + continue; + } + + let src_modified = fs::metadata(&src_path).and_then(|m| m.modified()); + let dest_modified = fs::metadata(&dest_path).and_then(|m| m.modified()); + + match (src_modified, dest_modified) { + (Ok(src_modified), Ok(dest_modified)) => { + if src_modified > dest_modified { + handle_copy(); + } else { + log::debug!(target: "app", "skipping resource copy '{file}'"); + } + } + _ => { + log::debug!(target: "app", "failed to get modified '{file}'"); + handle_copy(); + } + }; + } + + Ok(()) +} diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs new file mode 100644 index 0000000..aeb0a60 --- /dev/null +++ b/src-tauri/src/utils/mod.rs @@ -0,0 +1,7 @@ +pub mod dirs; +pub mod help; +pub mod init; +pub mod resolve; +pub mod server; +pub mod tmpl; +// mod winhelp; diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs new file mode 100644 index 0000000..14d4bdd --- /dev/null +++ b/src-tauri/src/utils/resolve.rs @@ -0,0 +1,179 @@ +use crate::{config::Config, core::*, utils::init, utils::server}; +use crate::{log_err, trace_err}; +use anyhow::Result; +use tauri::{App, AppHandle, Manager}; + +/// handle something when start app +pub fn resolve_setup(app: &mut App) { + #[cfg(target_os = "macos")] + app.set_activation_policy(tauri::ActivationPolicy::Accessory); + + handle::Handle::global().init(app.app_handle()); + + log_err!(init::init_resources(app.package_info())); + + // 启动核心 + log::trace!("init config"); + log_err!(Config::init_config()); + + log::trace!("launch core"); + log_err!(CoreManager::global().init()); + + // setup a simple http server for singleton + log::trace!("launch embed server"); + server::embed_server(app.app_handle()); + + log::trace!("init system tray"); + log_err!(tray::Tray::update_systray(&app.app_handle())); + + let silent_start = { Config::verge().data().enable_silent_start.clone() }; + if !silent_start.unwrap_or(false) { + create_window(&app.app_handle()); + } + + log_err!(sysopt::Sysopt::global().init_launch()); + log_err!(sysopt::Sysopt::global().init_sysproxy()); + + log_err!(handle::Handle::update_systray_part()); + log_err!(hotkey::Hotkey::global().init(app.app_handle())); + log_err!(timer::Timer::global().init()); +} + +/// reset system proxy +pub fn resolve_reset() { + log_err!(sysopt::Sysopt::global().reset_sysproxy()); + log_err!(CoreManager::global().stop_core()); +} + +/// create main window +pub fn create_window(app_handle: &AppHandle) { + if let Some(window) = app_handle.get_window("main") { + trace_err!(window.unminimize(), "set win unminimize"); + trace_err!(window.show(), "set win visible"); + trace_err!(window.set_focus(), "set win focus"); + return; + } + + let mut builder = tauri::window::WindowBuilder::new( + app_handle, + "main".to_string(), + tauri::WindowUrl::App("index.html".into()), + ) + .title("Clash Verge") + .fullscreen(false) + .min_inner_size(600.0, 520.0); + + match Config::verge().latest().window_size_position.clone() { + Some(size_pos) if size_pos.len() == 4 => { + let size = (size_pos[0], size_pos[1]); + let pos = (size_pos[2], size_pos[3]); + let w = size.0.clamp(600.0, f64::INFINITY); + let h = size.1.clamp(520.0, f64::INFINITY); + builder = builder.inner_size(w, h).position(pos.0, pos.1); + } + _ => { + #[cfg(target_os = "windows")] + { + builder = builder.inner_size(800.0, 636.0).center(); + } + + #[cfg(target_os = "macos")] + { + builder = builder.inner_size(800.0, 642.0).center(); + } + + #[cfg(target_os = "linux")] + { + builder = builder.inner_size(800.0, 642.0).center(); + } + } + }; + + #[cfg(target_os = "windows")] + { + use std::time::Duration; + use tokio::time::sleep; + use window_shadows::set_shadow; + + match builder + .decorations(false) + .transparent(true) + .visible(false) + .build() + { + Ok(win) => { + log::trace!("try to calculate the monitor size"); + let center = (|| -> Result { + let mut center = false; + let monitor = win.current_monitor()?.ok_or(anyhow::anyhow!(""))?; + let size = monitor.size(); + let pos = win.outer_position()?; + + if pos.x < -400 + || pos.x > (size.width - 200).try_into()? + || pos.y < -200 + || pos.y > (size.height - 200).try_into()? + { + center = true; + } + Ok(center) + })(); + + if center.unwrap_or(true) { + trace_err!(win.center(), "set win center"); + } + + log::trace!("try to create window"); + let app_handle = app_handle.clone(); + + // 加点延迟避免界面闪一下 + tauri::async_runtime::spawn(async move { + sleep(Duration::from_millis(888)).await; + + if let Some(window) = app_handle.get_window("main") { + trace_err!(set_shadow(&window, true), "set win shadow"); + trace_err!(window.show(), "set win visible"); + trace_err!(window.unminimize(), "set win unminimize"); + trace_err!(window.set_focus(), "set win focus"); + } else { + log::error!(target: "app", "failed to create window, get_window is None") + } + }); + } + Err(err) => log::error!(target: "app", "failed to create window, {err}"), + } + } + + #[cfg(target_os = "macos")] + crate::log_err!(builder + .decorations(true) + .hidden_title(true) + .title_bar_style(tauri::TitleBarStyle::Overlay) + .build()); + + #[cfg(target_os = "linux")] + crate::log_err!(builder.decorations(true).transparent(false).build()); +} + +/// save window size and position +pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) -> Result<()> { + let win = app_handle + .get_window("main") + .ok_or(anyhow::anyhow!("failed to get window"))?; + + let scale = win.scale_factor()?; + let size = win.inner_size()?; + let size = size.to_logical::(scale); + let pos = win.outer_position()?; + let pos = pos.to_logical::(scale); + + let verge = Config::verge(); + let mut verge = verge.latest(); + verge.window_size_position = Some(vec![size.width, size.height, pos.x, pos.y]); + + if save_to_file { + verge.save_file()?; + } + + Ok(()) +} diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs new file mode 100644 index 0000000..f4e9836 --- /dev/null +++ b/src-tauri/src/utils/server.rs @@ -0,0 +1,44 @@ +extern crate warp; + +use super::resolve; +use crate::config::IVerge; +use anyhow::{bail, Result}; +use port_scanner::local_port_available; +use tauri::AppHandle; +use warp::Filter; + +/// check whether there is already exists +pub fn check_singleton() -> Result<()> { + let port = IVerge::get_singleton_port(); + + if !local_port_available(port) { + tauri::async_runtime::block_on(async { + let url = format!("http://127.0.0.1:{port}/commands/visible"); + let resp = reqwest::get(url).await?.text().await?; + + if &resp == "ok" { + bail!("app exists"); + } + + log::error!("failed to setup singleton listen server"); + Ok(()) + }) + } else { + Ok(()) + } +} + +/// The embed server only be used to implement singleton process +/// maybe it can be used as pac server later +pub fn embed_server(app_handle: AppHandle) { + let port = IVerge::get_singleton_port(); + + tauri::async_runtime::spawn(async move { + let commands = warp::path!("commands" / "visible").map(move || { + resolve::create_window(&app_handle); + format!("ok") + }); + + warp::serve(commands).bind(([127, 0, 0, 1], port)).await; + }); +} diff --git a/src-tauri/src/utils/tmpl.rs b/src-tauri/src/utils/tmpl.rs new file mode 100644 index 0000000..ec17c33 --- /dev/null +++ b/src-tauri/src/utils/tmpl.rs @@ -0,0 +1,36 @@ +///! Some config file template + +/// template for new a profile item +pub const ITEM_LOCAL: &str = "# Profile Template for clash verge + +proxies: + +proxy-groups: + +rules: +"; + +/// enhanced profile +pub const ITEM_MERGE: &str = "# Merge Template for clash verge +# The `Merge` format used to enhance profile + +prepend-rules: + +prepend-proxies: + +prepend-proxy-groups: + +append-rules: + +append-proxies: + +append-proxy-groups: +"; + +/// enhanced profile +pub const ITEM_SCRIPT: &str = "// Define the `main` function + +function main(params) { + return params; +} +"; diff --git a/src-tauri/src/utils/winhelp.rs b/src-tauri/src/utils/winhelp.rs new file mode 100644 index 0000000..e903d95 --- /dev/null +++ b/src-tauri/src/utils/winhelp.rs @@ -0,0 +1,69 @@ +#![cfg(target_os = "windows")] +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] + +//! +//! From https://github.com/tauri-apps/window-vibrancy/blob/dev/src/windows.rs +//! + +use windows_sys::Win32::{ + Foundation::*, + System::{LibraryLoader::*, SystemInformation::*}, +}; + +fn get_function_impl(library: &str, function: &str) -> Option { + assert_eq!(library.chars().last(), Some('\0')); + assert_eq!(function.chars().last(), Some('\0')); + + let module = unsafe { LoadLibraryA(library.as_ptr()) }; + if module == 0 { + return None; + } + Some(unsafe { GetProcAddress(module, function.as_ptr()) }) +} + +macro_rules! get_function { + ($lib:expr, $func:ident) => { + get_function_impl(concat!($lib, '\0'), concat!(stringify!($func), '\0')).map(|f| unsafe { + std::mem::transmute::<::windows_sys::Win32::Foundation::FARPROC, $func>(f) + }) + }; +} + +/// Returns a tuple of (major, minor, buildnumber) +fn get_windows_ver() -> Option<(u32, u32, u32)> { + type RtlGetVersion = unsafe extern "system" fn(*mut OSVERSIONINFOW) -> i32; + let handle = get_function!("ntdll.dll", RtlGetVersion); + if let Some(rtl_get_version) = handle { + unsafe { + let mut vi = OSVERSIONINFOW { + dwOSVersionInfoSize: 0, + dwMajorVersion: 0, + dwMinorVersion: 0, + dwBuildNumber: 0, + dwPlatformId: 0, + szCSDVersion: [0; 128], + }; + + let status = (rtl_get_version)(&mut vi as _); + + if status >= 0 { + Some((vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber)) + } else { + None + } + } + } else { + None + } +} + +pub fn is_win11() -> bool { + let v = get_windows_ver().unwrap_or_default(); + v.2 >= 22000 +} + +#[test] +fn test_version() { + dbg!(get_windows_ver().unwrap_or_default()); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..71c7200 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,81 @@ +{ + "package": { + "productName": "Clash Verge", + "version": "1.3.8" + }, + "build": { + "distDir": "../dist", + "devPath": "http://localhost:3000/", + "beforeDevCommand": "yarn run web:dev", + "beforeBuildCommand": "yarn run web:build" + }, + "tauri": { + "systemTray": { + "iconPath": "icons/tray-icon.ico", + "iconAsTemplate": true + }, + "bundle": { + "active": true, + "targets": "all", + "identifier": "top.gydi.clashverge", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon-new.icns", + "icons/icon.ico" + ], + "resources": ["resources"], + "externalBin": ["sidecar/clash", "sidecar/clash-meta"], + "copyright": "© 2022 zzzgydi All Rights Reserved", + "category": "DeveloperTool", + "shortDescription": "A Clash GUI based on tauri.", + "longDescription": "A Clash GUI based on tauri.", + "deb": { + "depends": ["openssl"] + }, + "macOS": { + "frameworks": [], + "minimumSystemVersion": "", + "exceptionDomain": "", + "signingIdentity": null, + "entitlements": null + }, + "windows": { + "certificateThumbprint": null, + "digestAlgorithm": "sha256", + "timestampUrl": "", + "wix": { + "language": ["zh-CN", "en-US", "ru-RU"] + } + } + }, + "updater": { + "active": true, + "endpoints": [ + "https://ghproxy.com/https://github.com/zzzgydi/clash-verge/releases/download/updater/update-proxy.json", + "https://github.com/zzzgydi/clash-verge/releases/download/updater/update.json" + ], + "dialog": false, + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNUFBNTBBN0FDNEFBRTUKUldUbHFzUjZDcVZhRVRJM25NS3NkSFlFVElxUkNZMzZ6bHUwRVJjb2F3alJXVzRaeDdSaTA2YWYK" + }, + "allowlist": { + "shell": { + "all": true + }, + "window": { + "all": true + }, + "process": { + "all": true + }, + "globalShortcut": { + "all": true + } + }, + "windows": [], + "security": { + "csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src data: 'self';" + } + } +} diff --git a/src/assets/image/logo-box.png b/src/assets/image/logo-box.png new file mode 100644 index 0000000..fe64a1f Binary files /dev/null and b/src/assets/image/logo-box.png differ diff --git a/src/assets/image/logo.ico b/src/assets/image/logo.ico new file mode 100644 index 0000000..e406a78 Binary files /dev/null and b/src/assets/image/logo.ico differ diff --git a/src/assets/image/logo.png b/src/assets/image/logo.png new file mode 100644 index 0000000..c384c7c Binary files /dev/null and b/src/assets/image/logo.png differ diff --git a/src/assets/image/logo.svg b/src/assets/image/logo.svg new file mode 100644 index 0000000..750fb40 --- /dev/null +++ b/src/assets/image/logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss new file mode 100644 index 0000000..2fdcc9e --- /dev/null +++ b/src/assets/styles/index.scss @@ -0,0 +1,50 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +:root { + --primary-main: #5b5c9d; + --text-primary: #637381; + --selection-color: #f5f5f5; + --scroller-color: #90939980; +} + +::selection { + color: var(--selection-color); + background-color: var(--primary-main); +} + +*::-webkit-scrollbar { + width: 6px; + height: 6px; + background: transparent; +} +*::-webkit-scrollbar-thumb { + border-radius: 6px; + background-color: var(--scroller-color); +} + +@import "./layout.scss"; +@import "./page.scss"; + +@media (prefers-color-scheme: dark) { + :root { + background-color: rgba(18, 18, 18, 1); + } +} + +.user-none { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} diff --git a/src/assets/styles/layout.scss b/src/assets/styles/layout.scss new file mode 100644 index 0000000..3d16c86 --- /dev/null +++ b/src/assets/styles/layout.scss @@ -0,0 +1,117 @@ +.layout { + width: 100%; + height: 100vh; + display: flex; + overflow: hidden; + + &__left { + flex: 1 0 25%; + display: flex; + height: 100%; + max-width: 225px; + min-width: 125px; + padding: 16px 0 8px; + flex-direction: column; + box-sizing: border-box; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + overflow: hidden; + + $maxLogo: 100px; + + .the-logo { + position: relative; + flex: 0 1 $maxLogo; + width: 100%; + max-width: $maxLogo + 32px; + max-height: $maxLogo; + margin: 0 auto; + padding: 0 16px; + text-align: center; + box-sizing: border-box; + + img, + svg { + width: 100%; + height: 100%; + pointer-events: none; + } + + .the-newbtn { + position: absolute; + right: 10px; + bottom: 0px; + transform: scale(0.8); + } + } + + .the-menu { + flex: 1 1 80%; + overflow-y: auto; + margin-bottom: 8px; + } + + .the-traffic { + flex: 0 0 60px; + + > div { + margin: 0 auto; + } + } + } + + &__right { + position: relative; + flex: 1 1 75%; + height: 100%; + + .the-bar { + position: absolute; + top: 2px; + right: 8px; + height: 36px; + display: flex; + align-items: center; + box-sizing: border-box; + z-index: 2; + } + + .the-content { + position: absolute; + top: 0; + left: 0; + right: 2px; + bottom: 10px; + } + } +} + +.linux, +.windows, +.unknown { + &.layout { + $maxLogo: 115px; + .layout__left .the-logo { + flex: 0 1 $maxLogo; + max-width: $maxLogo + 32px; + max-height: $maxLogo; + } + + .layout__right .the-content { + top: 30px; + } + } +} + +.macos { + &.layout { + .layout__left { + padding-top: 24px; + } + .layout__right .the-content { + top: 20px; + } + } +} diff --git a/src/assets/styles/page.scss b/src/assets/styles/page.scss new file mode 100644 index 0000000..ae7fce3 --- /dev/null +++ b/src/assets/styles/page.scss @@ -0,0 +1,35 @@ +.base-page { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + + > header { + flex: 0 0 58px; + width: 90%; + // max-width: 850px; + margin: 0 auto; + padding-right: 4px; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: space-between; + } + + > section { + position: relative; + flex: 1 1 100%; + width: 100%; + height: 100%; + overflow: auto; + padding: 8px 0; + box-sizing: border-box; + scrollbar-gutter: stable; + + .base-content { + width: 90%; + // max-width: 850px; + margin: 0 auto; + } + } +} diff --git a/src/components/base/base-dialog.tsx b/src/components/base/base-dialog.tsx new file mode 100644 index 0000000..db4226c --- /dev/null +++ b/src/components/base/base-dialog.tsx @@ -0,0 +1,67 @@ +import { ReactNode } from "react"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + type SxProps, + type Theme, +} from "@mui/material"; + +interface Props { + title: ReactNode; + open: boolean; + okBtn?: ReactNode; + cancelBtn?: ReactNode; + disableOk?: boolean; + disableCancel?: boolean; + disableFooter?: boolean; + contentSx?: SxProps; + children?: ReactNode; + onOk?: () => void; + onCancel?: () => void; + onClose?: () => void; +} + +export interface DialogRef { + open: () => void; + close: () => void; +} + +export const BaseDialog: React.FC = (props) => { + const { + open, + title, + children, + okBtn, + cancelBtn, + contentSx, + disableCancel, + disableOk, + disableFooter, + } = props; + + return ( + + {title} + + {children} + + {!disableFooter && ( + + {!disableCancel && ( + + )} + {!disableOk && ( + + )} + + )} + + ); +}; diff --git a/src/components/base/base-empty.tsx b/src/components/base/base-empty.tsx new file mode 100644 index 0000000..c364a50 --- /dev/null +++ b/src/components/base/base-empty.tsx @@ -0,0 +1,29 @@ +import { alpha, Box, Typography } from "@mui/material"; +import { InboxRounded } from "@mui/icons-material"; + +interface Props { + text?: React.ReactNode; + extra?: React.ReactNode; +} + +export const BaseEmpty = (props: Props) => { + const { text = "Empty", extra } = props; + + return ( + ({ + width: "100%", + height: "100%", + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + color: alpha(palette.text.secondary, 0.75), + })} + > + + {text} + {extra} + + ); +}; diff --git a/src/components/base/base-error-boundary.tsx b/src/components/base/base-error-boundary.tsx new file mode 100644 index 0000000..2475a2f --- /dev/null +++ b/src/components/base/base-error-boundary.tsx @@ -0,0 +1,29 @@ +import { ReactNode } from "react"; +import { ErrorBoundary, FallbackProps } from "react-error-boundary"; + +function ErrorFallback({ error }: FallbackProps) { + return ( +
+

Something went wrong:(

+ +
{error.message}
+ +
+ Error Stack +
{error.stack}
+
+
+ ); +} + +interface Props { + children?: ReactNode; +} + +export const BaseErrorBoundary = (props: Props) => { + return ( + + {props.children} + + ); +}; diff --git a/src/components/base/base-loading.tsx b/src/components/base/base-loading.tsx new file mode 100644 index 0000000..0fdbebf --- /dev/null +++ b/src/components/base/base-loading.tsx @@ -0,0 +1,48 @@ +import { styled } from "@mui/material"; + +const Loading = styled("div")` + position: relative; + display: flex; + height: 100%; + min-height: 18px; + box-sizing: border-box; + align-items: center; + + & > div { + box-sizing: border-box; + width: 6px; + height: 6px; + margin: 2px; + border-radius: 100%; + animation: loading 0.7s -0.15s infinite linear; + } + + & > div:nth-child(2n-1) { + animation-delay: -0.5s; + } + + @keyframes loading { + 50% { + opacity: 0.2; + transform: scale(0.75); + } + 100% { + opacity: 1; + transform: scale(1); + } + } +`; + +const LoadingItem = styled("div")(({ theme }) => ({ + background: theme.palette.text.secondary, +})); + +export const BaseLoading = () => { + return ( + + + + + + ); +}; diff --git a/src/components/base/base-notice.tsx b/src/components/base/base-notice.tsx new file mode 100644 index 0000000..5849034 --- /dev/null +++ b/src/components/base/base-notice.tsx @@ -0,0 +1,94 @@ +import { createRoot } from "react-dom/client"; +import { ReactNode, useState } from "react"; +import { Box, IconButton, Slide, Snackbar, Typography } from "@mui/material"; +import { Close, CheckCircleRounded, ErrorRounded } from "@mui/icons-material"; + +interface InnerProps { + type: string; + duration?: number; + message: ReactNode; + onClose: () => void; +} + +const NoticeInner = (props: InnerProps) => { + const { type, message, duration = 1500, onClose } = props; + const [visible, setVisible] = useState(true); + + const onBtnClose = () => { + setVisible(false); + onClose(); + }; + const onAutoClose = (_e: any, reason: string) => { + if (reason !== "clickaway") onBtnClose(); + }; + + const msgElement = + type === "info" ? ( + message + ) : ( + + {type === "error" && } + {type === "success" && } + + + {message} + + + ); + + return ( + } + transitionDuration={200} + action={ + + + + } + /> + ); +}; + +interface NoticeInstance { + (props: Omit): void; + + info(message: ReactNode, duration?: number): void; + error(message: ReactNode, duration?: number): void; + success(message: ReactNode, duration?: number): void; +} + +let parent: HTMLDivElement = null!; + +// @ts-ignore +export const Notice: NoticeInstance = (props) => { + if (!parent) { + parent = document.createElement("div"); + document.body.appendChild(parent); + } + + const container = document.createElement("div"); + parent.appendChild(container); + const root = createRoot(container); + + const onUnmount = () => { + root.unmount(); + if (parent) setTimeout(() => parent.removeChild(container), 500); + }; + + root.render(); +}; + +(["info", "error", "success"] as const).forEach((type) => { + Notice[type] = (message, duration) => { + setTimeout(() => Notice({ type, message, duration }), 0); + }; +}); diff --git a/src/components/base/base-page.tsx b/src/components/base/base-page.tsx new file mode 100644 index 0000000..869731a --- /dev/null +++ b/src/components/base/base-page.tsx @@ -0,0 +1,34 @@ +import React, { ReactNode } from "react"; +import { Typography } from "@mui/material"; +import { BaseErrorBoundary } from "./base-error-boundary"; + +interface Props { + title?: React.ReactNode; // the page title + header?: React.ReactNode; // something behind title + contentStyle?: React.CSSProperties; + children?: ReactNode; +} + +export const BasePage: React.FC = (props) => { + const { title, header, contentStyle, children } = props; + + return ( + +
+
+ + {title} + + + {header} +
+ +
+
+ {children} +
+
+
+
+ ); +}; diff --git a/src/components/base/index.ts b/src/components/base/index.ts new file mode 100644 index 0000000..3e0e324 --- /dev/null +++ b/src/components/base/index.ts @@ -0,0 +1,6 @@ +export { BaseDialog, type DialogRef } from "./base-dialog"; +export { BasePage } from "./base-page"; +export { BaseEmpty } from "./base-empty"; +export { BaseLoading } from "./base-loading"; +export { BaseErrorBoundary } from "./base-error-boundary"; +export { Notice } from "./base-notice"; diff --git a/src/components/connection/connection-detail.tsx b/src/components/connection/connection-detail.tsx new file mode 100644 index 0000000..7ed786e --- /dev/null +++ b/src/components/connection/connection-detail.tsx @@ -0,0 +1,104 @@ +import dayjs from "dayjs"; +import { forwardRef, useImperativeHandle, useState } from "react"; +import { useLockFn } from "ahooks"; +import { Box, Button, Snackbar } from "@mui/material"; +import { deleteConnection } from "@/services/api"; +import { truncateStr } from "@/utils/truncate-str"; +import parseTraffic from "@/utils/parse-traffic"; + +export interface ConnectionDetailRef { + open: (detail: IConnectionsItem) => void; +} + +export const ConnectionDetail = forwardRef( + (props, ref) => { + const [open, setOpen] = useState(false); + const [detail, setDetail] = useState(null!); + + useImperativeHandle(ref, () => ({ + open: (detail: IConnectionsItem) => { + if (open) return; + setOpen(true); + setDetail(detail); + }, + })); + + const onClose = () => setOpen(false); + + return ( + + ) : null + } + /> + ); + } +); + +interface InnerProps { + data: IConnectionsItem; + onClose?: () => void; +} + +const InnerConnectionDetail = ({ data, onClose }: InnerProps) => { + const { metadata, rulePayload } = data; + const chains = [...data.chains].reverse().join(" / "); + const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule; + const host = metadata.host + ? `${metadata.host}:${metadata.destinationPort}` + : `${metadata.destinationIP}:${metadata.destinationPort}`; + + const information = [ + { label: "Host", value: host }, + { label: "Download", value: parseTraffic(data.download).join(" ") }, + { label: "Upload", value: parseTraffic(data.upload).join(" ") }, + { + label: "DL Speed", + value: parseTraffic(data.curDownload ?? -1).join(" ") + "/s", + }, + { + label: "UL Speed", + value: parseTraffic(data.curUpload ?? -1).join(" ") + "/s", + }, + { label: "Chains", value: chains }, + { label: "Rule", value: rule }, + { + label: "Process", + value: truncateStr(metadata.process || metadata.processPath), + }, + { label: "Time", value: dayjs(data.start).fromNow() }, + { label: "Source", value: `${metadata.sourceIP}:${metadata.sourcePort}` }, + { label: "Destination IP", value: metadata.destinationIP }, + { label: "Type", value: `${metadata.type}(${metadata.network})` }, + ]; + + const onDelete = useLockFn(async () => deleteConnection(data.id)); + + return ( + + {information.map((each) => ( +
+ {each.label}: {each.value} +
+ ))} + + + + +
+ ); +}; diff --git a/src/components/connection/connection-item.tsx b/src/components/connection/connection-item.tsx new file mode 100644 index 0000000..d6c78eb --- /dev/null +++ b/src/components/connection/connection-item.tsx @@ -0,0 +1,75 @@ +import dayjs from "dayjs"; +import { useLockFn } from "ahooks"; +import { + styled, + ListItem, + IconButton, + ListItemText, + Box, + alpha, +} from "@mui/material"; +import { CloseRounded } from "@mui/icons-material"; +import { deleteConnection } from "@/services/api"; +import parseTraffic from "@/utils/parse-traffic"; + +const Tag = styled("span")(({ theme }) => ({ + fontSize: "10px", + padding: "0 4px", + lineHeight: 1.375, + border: "1px solid", + borderRadius: 4, + borderColor: alpha(theme.palette.text.secondary, 0.35), + marginRight: "4px", +})); + +interface Props { + value: IConnectionsItem; + onShowDetail?: () => void; +} + +export const ConnectionItem = (props: Props) => { + const { value, onShowDetail } = props; + + const { id, metadata, chains, start, curUpload, curDownload } = value; + + const onDelete = useLockFn(async () => deleteConnection(id)); + const showTraffic = curUpload! >= 100 || curDownload! >= 100; + + return ( + + + + } + > + + + {metadata.network} + + + {metadata.type} + + {!!metadata.process && {metadata.process}} + + {chains?.length > 0 && {chains[value.chains.length - 1]}} + + {dayjs(start).fromNow()} + + {showTraffic && ( + + {parseTraffic(curUpload!)} / {parseTraffic(curDownload!)} + + )} + + } + /> + + ); +}; diff --git a/src/components/connection/connection-table.tsx b/src/components/connection/connection-table.tsx new file mode 100644 index 0000000..143bf9f --- /dev/null +++ b/src/components/connection/connection-table.tsx @@ -0,0 +1,110 @@ +import dayjs from "dayjs"; +import { useMemo, useState } from "react"; +import { DataGrid, GridColDef } from "@mui/x-data-grid"; +import { truncateStr } from "@/utils/truncate-str"; +import parseTraffic from "@/utils/parse-traffic"; + +interface Props { + connections: IConnectionsItem[]; + onShowDetail: (data: IConnectionsItem) => void; +} + +export const ConnectionTable = (props: Props) => { + const { connections, onShowDetail } = props; + + const [columnVisible, setColumnVisible] = useState< + Partial> + >({}); + + const columns: GridColDef[] = [ + { field: "host", headerName: "Host", flex: 220, minWidth: 220 }, + { + field: "download", + headerName: "Download", + width: 88, + align: "right", + headerAlign: "right", + }, + { + field: "upload", + headerName: "Upload", + width: 88, + align: "right", + headerAlign: "right", + }, + { + field: "dlSpeed", + headerName: "DL Speed", + width: 88, + align: "right", + headerAlign: "right", + }, + { + field: "ulSpeed", + headerName: "UL Speed", + width: 88, + align: "right", + headerAlign: "right", + }, + { field: "chains", headerName: "Chains", flex: 360, minWidth: 360 }, + { field: "rule", headerName: "Rule", flex: 300, minWidth: 250 }, + { field: "process", headerName: "Process", flex: 480, minWidth: 480 }, + { + field: "time", + headerName: "Time", + flex: 120, + minWidth: 100, + align: "right", + headerAlign: "right", + }, + { field: "source", headerName: "Source", flex: 200, minWidth: 130 }, + { + field: "destinationIP", + headerName: "Destination IP", + flex: 200, + minWidth: 130, + }, + { field: "type", headerName: "Type", flex: 160, minWidth: 100 }, + ]; + + const connRows = useMemo(() => { + return connections.map((each) => { + const { metadata, rulePayload } = each; + const chains = [...each.chains].reverse().join(" / "); + const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule; + + return { + id: each.id, + host: metadata.host + ? `${metadata.host}:${metadata.destinationPort}` + : `${metadata.destinationIP}:${metadata.destinationPort}`, + download: parseTraffic(each.download).join(" "), + upload: parseTraffic(each.upload).join(" "), + dlSpeed: parseTraffic(each.curDownload).join(" ") + "/s", + ulSpeed: parseTraffic(each.curUpload).join(" ") + "/s", + chains, + rule, + process: truncateStr(metadata.process || metadata.processPath), + time: dayjs(each.start).fromNow(), + source: `${metadata.sourceIP}:${metadata.sourcePort}`, + destinationIP: metadata.destinationIP, + type: `${metadata.type}(${metadata.network})`, + + connectionData: each, + }; + }); + }, [connections]); + + return ( + onShowDetail(e.row.connectionData)} + density="compact" + sx={{ border: "none", "div:focus": { outline: "none !important" } }} + columnVisibilityModel={columnVisible} + onColumnVisibilityModelChange={(e) => setColumnVisible(e)} + /> + ); +}; diff --git a/src/components/layout/layout-control.tsx b/src/components/layout/layout-control.tsx new file mode 100644 index 0000000..6ec12b9 --- /dev/null +++ b/src/components/layout/layout-control.tsx @@ -0,0 +1,39 @@ +import { Button } from "@mui/material"; +import { appWindow } from "@tauri-apps/api/window"; +import { + CloseRounded, + CropSquareRounded, + HorizontalRuleRounded, +} from "@mui/icons-material"; + +export const LayoutControl = () => { + const minWidth = 40; + + return ( + <> + + + + + + + ); +}; diff --git a/src/components/layout/layout-item.tsx b/src/components/layout/layout-item.tsx new file mode 100644 index 0000000..5a33da2 --- /dev/null +++ b/src/components/layout/layout-item.tsx @@ -0,0 +1,42 @@ +import { alpha, ListItem, ListItemButton, ListItemText } from "@mui/material"; +import { useMatch, useResolvedPath, useNavigate } from "react-router-dom"; +import type { LinkProps } from "react-router-dom"; + +export const LayoutItem = (props: LinkProps) => { + const { to, children } = props; + + const resolved = useResolvedPath(to); + const match = useMatch({ path: resolved.pathname, end: true }); + const navigate = useNavigate(); + + return ( + + { + const bgcolor = + mode === "light" + ? alpha(primary.main, 0.15) + : alpha(primary.main, 0.35); + const color = mode === "light" ? primary.main : primary.light; + + return { + "&.Mui-selected": { bgcolor }, + "&.Mui-selected:hover": { bgcolor }, + "&.Mui-selected .MuiListItemText-primary": { color }, + }; + }, + ]} + onClick={() => navigate(to)} + > + + + + ); +}; diff --git a/src/components/layout/layout-traffic.tsx b/src/components/layout/layout-traffic.tsx new file mode 100644 index 0000000..4e65a87 --- /dev/null +++ b/src/components/layout/layout-traffic.tsx @@ -0,0 +1,136 @@ +import { useEffect, useRef, useState } from "react"; +import { Box, Typography } from "@mui/material"; +import { + ArrowDownward, + ArrowUpward, + MemoryOutlined, +} from "@mui/icons-material"; +import { useClashInfo } from "@/hooks/use-clash"; +import { useVerge } from "@/hooks/use-verge"; +import { TrafficGraph, type TrafficRef } from "./traffic-graph"; +import { useLogSetup } from "./use-log-setup"; +import { useVisibility } from "@/hooks/use-visibility"; +import { useWebsocket } from "@/hooks/use-websocket"; +import parseTraffic from "@/utils/parse-traffic"; + +// setup the traffic +export const LayoutTraffic = () => { + const { clashInfo } = useClashInfo(); + const { verge } = useVerge(); + + // whether hide traffic graph + const trafficGraph = verge?.traffic_graph ?? true; + + const trafficRef = useRef(null); + const [traffic, setTraffic] = useState({ up: 0, down: 0 }); + const [memory, setMemory] = useState({ inuse: 0 }); + const pageVisible = useVisibility(); + + // setup log ws during layout + useLogSetup(); + + const { connect, disconnect } = useWebsocket((event) => { + const data = JSON.parse(event.data) as ITrafficItem; + trafficRef.current?.appendData(data); + setTraffic(data); + }); + + useEffect(() => { + if (!clashInfo || !pageVisible) return; + + const { server = "", secret = "" } = clashInfo; + connect(`ws://${server}/traffic?token=${encodeURIComponent(secret)}`); + + return () => { + disconnect(); + }; + }, [clashInfo, pageVisible]); + + /* --------- meta memory information --------- */ + const isMetaCore = verge?.clash_core === "clash-meta"; + const displayMemory = isMetaCore && (verge?.enable_memory_usage ?? true); + + const memoryWs = useWebsocket( + (event) => { + setMemory(JSON.parse(event.data)); + }, + { onError: () => setMemory({ inuse: 0 }) } + ); + + useEffect(() => { + if (!clashInfo || !pageVisible || !displayMemory) return; + const { server = "", secret = "" } = clashInfo; + memoryWs.connect( + `ws://${server}/memory?token=${encodeURIComponent(secret)}` + ); + return () => memoryWs.disconnect(); + }, [clashInfo, pageVisible, displayMemory]); + + const [up, upUnit] = parseTraffic(traffic.up); + const [down, downUnit] = parseTraffic(traffic.down); + const [inuse, inuseUnit] = parseTraffic(memory.inuse); + + const iconStyle: any = { + sx: { mr: "8px", fontSize: 16 }, + }; + const valStyle: any = { + component: "span", + color: "primary", + textAlign: "center", + sx: { flex: "1 1 56px", userSelect: "none" }, + }; + const unitStyle: any = { + component: "span", + color: "grey.500", + fontSize: "12px", + textAlign: "right", + sx: { flex: "0 1 27px", userSelect: "none" }, + }; + + return ( + + {trafficGraph && pageVisible && ( +
+ +
+ )} + + + + 0 ? "primary" : "disabled"} + /> + {up} + {upUnit}/s + + + + 0 ? "primary" : "disabled"} + /> + {down} + {downUnit}/s + + + {displayMemory && ( + + + {inuse} + {inuseUnit} + + )} + +
+ ); +}; diff --git a/src/components/layout/traffic-graph.tsx b/src/components/layout/traffic-graph.tsx new file mode 100644 index 0000000..5c1a6b7 --- /dev/null +++ b/src/components/layout/traffic-graph.tsx @@ -0,0 +1,195 @@ +import { forwardRef, useEffect, useImperativeHandle, useRef } from "react"; +import { useTheme } from "@mui/material"; + +const maxPoint = 30; + +const refLineAlpha = 1; +const refLineWidth = 2; + +const upLineAlpha = 0.6; +const upLineWidth = 4; + +const downLineAlpha = 1; +const downLineWidth = 4; + +const defaultList = Array(maxPoint + 2).fill({ up: 0, down: 0 }); + +type TrafficData = { up: number; down: number }; + +export interface TrafficRef { + appendData: (data: TrafficData) => void; + toggleStyle: () => void; +} + +/** + * draw the traffic graph + */ +export const TrafficGraph = forwardRef((props, ref) => { + const countRef = useRef(0); + const styleRef = useRef(true); + const listRef = useRef(defaultList); + const canvasRef = useRef(null!); + + const cacheRef = useRef(null); + + const { palette } = useTheme(); + + useImperativeHandle(ref, () => ({ + appendData: (data: TrafficData) => { + cacheRef.current = data; + }, + toggleStyle: () => { + styleRef.current = !styleRef.current; + }, + })); + + useEffect(() => { + let timer: any; + const zero = { up: 0, down: 0 }; + + const handleData = () => { + const data = cacheRef.current ? cacheRef.current : zero; + cacheRef.current = null; + + const list = listRef.current; + if (list.length > maxPoint + 2) list.shift(); + list.push(data); + countRef.current = 0; + + timer = setTimeout(handleData, 1000); + }; + + handleData(); + + return () => { + if (timer) clearTimeout(timer); + }; + }, []); + + useEffect(() => { + let raf = 0; + const canvas = canvasRef.current!; + + if (!canvas) return; + + const context = canvas.getContext("2d")!; + + if (!context) return; + + const { primary, secondary, divider } = palette; + const refLineColor = divider || "rgba(0, 0, 0, 0.12)"; + const upLineColor = secondary.main || "#9c27b0"; + const downLineColor = primary.main || "#5b5c9d"; + + const width = canvas.width; + const height = canvas.height; + const dx = width / maxPoint; + const dy = height / 7; + const l1 = dy; + const l2 = dy * 4; + + const countY = (v: number) => { + const h = height; + + if (v == 0) return h - 1; + if (v <= 10) return h - (v / 10) * dy; + if (v <= 100) return h - (v / 100 + 1) * dy; + if (v <= 1024) return h - (v / 1024 + 2) * dy; + if (v <= 10240) return h - (v / 10240 + 3) * dy; + if (v <= 102400) return h - (v / 102400 + 4) * dy; + if (v <= 1048576) return h - (v / 1048576 + 5) * dy; + if (v <= 10485760) return h - (v / 10485760 + 6) * dy; + return 1; + }; + + const drawBezier = (list: number[], offset: number) => { + const points = list.map((y, i) => [ + (dx * (i - 1) - offset + 3) | 0, + countY(y), + ]); + + let x = points[0][0]; + let y = points[0][1]; + + context.moveTo(x, y); + + for (let i = 1; i < points.length; i++) { + const p1 = points[i]; + const p2 = points[i + 1] || p1; + + const x1 = (p1[0] + p2[0]) / 2; + const y1 = (p1[1] + p2[1]) / 2; + + context.quadraticCurveTo(p1[0], p1[1], x1, y1); + x = x1; + y = y1; + } + }; + + const drawLine = (list: number[], offset: number) => { + const points = list.map((y, i) => [ + (dx * (i - 1) - offset) | 0, + countY(y), + ]); + + context.moveTo(points[0][0], points[0][1]); + + for (let i = 1; i < points.length; i++) { + const p = points[i]; + context.lineTo(p[0], p[1]); + } + }; + + const drawGraph = (lastTime: number) => { + const listUp = listRef.current.map((v) => v.up); + const listDown = listRef.current.map((v) => v.down); + const lineStyle = styleRef.current; + + const now = Date.now(); + const diff = now - lastTime; + const temp = Math.min((diff / 1000) * dx + countRef.current, dx); + const offset = countRef.current === 0 ? 0 : temp; + countRef.current = temp; + + context.clearRect(0, 0, width, height); + + // Reference lines + context.beginPath(); + context.globalAlpha = refLineAlpha; + context.lineWidth = refLineWidth; + context.strokeStyle = refLineColor; + context.moveTo(0, l1); + context.lineTo(width, l1); + context.moveTo(0, l2); + context.lineTo(width, l2); + context.stroke(); + context.closePath(); + + context.beginPath(); + context.globalAlpha = upLineAlpha; + context.lineWidth = upLineWidth; + context.strokeStyle = upLineColor; + lineStyle ? drawBezier(listUp, offset) : drawLine(listUp, offset); + context.stroke(); + context.closePath(); + + context.beginPath(); + context.globalAlpha = downLineAlpha; + context.lineWidth = downLineWidth; + context.strokeStyle = downLineColor; + lineStyle ? drawBezier(listDown, offset) : drawLine(listDown, offset); + context.stroke(); + context.closePath(); + + raf = requestAnimationFrame(() => drawGraph(now)); + }; + + drawGraph(Date.now()); + + return () => { + cancelAnimationFrame(raf); + }; + }, [palette]); + + return ; +}); diff --git a/src/components/layout/update-button.tsx b/src/components/layout/update-button.tsx new file mode 100644 index 0000000..b71ea07 --- /dev/null +++ b/src/components/layout/update-button.tsx @@ -0,0 +1,40 @@ +import useSWR from "swr"; +import { useRef } from "react"; +import { Button } from "@mui/material"; +import { checkUpdate } from "@tauri-apps/api/updater"; +import { UpdateViewer } from "../setting/mods/update-viewer"; +import { DialogRef } from "../base"; + +interface Props { + className?: string; +} + +export const UpdateButton = (props: Props) => { + const { className } = props; + + const viewerRef = useRef(null); + + const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, { + errorRetryCount: 2, + revalidateIfStale: false, + focusThrottleInterval: 36e5, // 1 hour + }); + + if (!updateInfo?.shouldUpdate) return null; + + return ( + <> + + + + + ); +}; diff --git a/src/components/layout/use-custom-theme.ts b/src/components/layout/use-custom-theme.ts new file mode 100644 index 0000000..e029494 --- /dev/null +++ b/src/components/layout/use-custom-theme.ts @@ -0,0 +1,124 @@ +import { useEffect, useMemo } from "react"; +import { useRecoilState } from "recoil"; +import { createTheme, Theme } from "@mui/material"; +import { appWindow } from "@tauri-apps/api/window"; +import { atomThemeMode } from "@/services/states"; +import { defaultTheme, defaultDarkTheme } from "@/pages/_theme"; +import { useVerge } from "@/hooks/use-verge"; + +/** + * custom theme + */ +export const useCustomTheme = () => { + const { verge } = useVerge(); + const { theme_mode, theme_setting } = verge ?? {}; + const [mode, setMode] = useRecoilState(atomThemeMode); + + useEffect(() => { + const themeMode = ["light", "dark", "system"].includes(theme_mode!) + ? theme_mode! + : "light"; + + if (themeMode !== "system") { + setMode(themeMode); + return; + } + + appWindow.theme().then((m) => m && setMode(m)); + const unlisten = appWindow.onThemeChanged((e) => setMode(e.payload)); + + return () => { + unlisten.then((fn) => fn()); + }; + }, [theme_mode]); + + const theme = useMemo(() => { + const setting = theme_setting || {}; + const dt = mode === "light" ? defaultTheme : defaultDarkTheme; + + let theme: Theme; + + try { + theme = createTheme({ + breakpoints: { + values: { xs: 0, sm: 650, md: 900, lg: 1200, xl: 1536 }, + }, + palette: { + mode, + primary: { main: setting.primary_color || dt.primary_color }, + secondary: { main: setting.secondary_color || dt.secondary_color }, + info: { main: setting.info_color || dt.info_color }, + error: { main: setting.error_color || dt.error_color }, + warning: { main: setting.warning_color || dt.warning_color }, + success: { main: setting.success_color || dt.success_color }, + text: { + primary: setting.primary_text || dt.primary_text, + secondary: setting.secondary_text || dt.secondary_text, + }, + }, + typography: { + // todo + fontFamily: setting.font_family + ? `${setting.font_family}, ${dt.font_family}` + : dt.font_family, + }, + }); + } catch { + // fix #294 + theme = createTheme({ + breakpoints: { + values: { xs: 0, sm: 650, md: 900, lg: 1200, xl: 1536 }, + }, + palette: { + mode, + primary: { main: dt.primary_color }, + secondary: { main: dt.secondary_color }, + info: { main: dt.info_color }, + error: { main: dt.error_color }, + warning: { main: dt.warning_color }, + success: { main: dt.success_color }, + text: { primary: dt.primary_text, secondary: dt.secondary_text }, + }, + typography: { fontFamily: dt.font_family }, + }); + } + + // css + const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5"; + const scrollColor = mode === "light" ? "#90939980" : "#54545480"; + + const rootEle = document.documentElement; + rootEle.style.setProperty("--selection-color", selectColor); + rootEle.style.setProperty("--scroller-color", scrollColor); + rootEle.style.setProperty("--primary-main", theme.palette.primary.main); + + // inject css + let style = document.querySelector("style#verge-theme"); + if (!style) { + style = document.createElement("style"); + style.id = "verge-theme"; + document.head.appendChild(style!); + } + if (style) { + style.innerHTML = setting.css_injection || ""; + } + + // update svg icon + const { palette } = theme; + + setTimeout(() => { + const dom = document.querySelector("#Gradient2"); + if (dom) { + dom.innerHTML = ` + + + + `; + } + }, 0); + + return theme; + }, [mode, theme_setting]); + + return { theme }; +}; diff --git a/src/components/layout/use-log-setup.ts b/src/components/layout/use-log-setup.ts new file mode 100644 index 0000000..0c130ac --- /dev/null +++ b/src/components/layout/use-log-setup.ts @@ -0,0 +1,39 @@ +import dayjs from "dayjs"; +import { useEffect } from "react"; +import { useRecoilValue, useSetRecoilState } from "recoil"; +import { getClashLogs } from "@/services/cmds"; +import { useClashInfo } from "@/hooks/use-clash"; +import { atomEnableLog, atomLogData } from "@/services/states"; +import { useWebsocket } from "@/hooks/use-websocket"; + +const MAX_LOG_NUM = 1000; + +// setup the log websocket +export const useLogSetup = () => { + const { clashInfo } = useClashInfo(); + + const enableLog = useRecoilValue(atomEnableLog); + const setLogData = useSetRecoilState(atomLogData); + + const { connect, disconnect } = useWebsocket((event) => { + const data = JSON.parse(event.data) as ILogItem; + const time = dayjs().format("MM-DD HH:mm:ss"); + setLogData((l) => { + if (l.length >= MAX_LOG_NUM) l.shift(); + return [...l, { ...data, time }]; + }); + }); + + useEffect(() => { + if (!enableLog || !clashInfo) return; + + getClashLogs().then(setLogData); + + const { server = "", secret = "" } = clashInfo; + connect(`ws://${server}/logs?token=${encodeURIComponent(secret)}`); + + return () => { + disconnect(); + }; + }, [clashInfo, enableLog]); +}; diff --git a/src/components/log/log-item.tsx b/src/components/log/log-item.tsx new file mode 100644 index 0000000..1aa0804 --- /dev/null +++ b/src/components/log/log-item.tsx @@ -0,0 +1,58 @@ +import { styled, Box } from "@mui/material"; + +const Item = styled(Box)(({ theme: { palette, typography } }) => ({ + padding: "8px 0", + margin: "0 12px", + lineHeight: 1.35, + borderBottom: `1px solid ${palette.divider}`, + fontSize: "0.875rem", + fontFamily: typography.fontFamily, + userSelect: "text", + "& .time": { + color: palette.text.secondary, + }, + "& .type": { + display: "inline-block", + marginLeft: 8, + textAlign: "center", + borderRadius: 2, + textTransform: "uppercase", + fontWeight: "600", + }, + '& .type[data-type="error"], & .type[data-type="err"]': { + color: palette.error.main, + }, + '& .type[data-type="warning"], & .type[data-type="warn"]': { + color: palette.warning.main, + }, + '& .type[data-type="info"], & .type[data-type="inf"]': { + color: palette.info.main, + }, + "& .data": { + color: palette.text.primary, + }, +})); + +interface Props { + value: ILogItem; +} + +const LogItem = (props: Props) => { + const { value } = props; + + return ( + +
+ {value.time} + + {value.type} + +
+
+ {value.payload} +
+
+ ); +}; + +export default LogItem; diff --git a/src/components/profile/editor-viewer.tsx b/src/components/profile/editor-viewer.tsx new file mode 100644 index 0000000..28608e9 --- /dev/null +++ b/src/components/profile/editor-viewer.tsx @@ -0,0 +1,94 @@ +import { useEffect, useRef } from "react"; +import { useLockFn } from "ahooks"; +import { useRecoilValue } from "recoil"; +import { useTranslation } from "react-i18next"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from "@mui/material"; +import { atomThemeMode } from "@/services/states"; +import { readProfileFile, saveProfileFile } from "@/services/cmds"; +import { Notice } from "@/components/base"; + +import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js"; +import "monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js"; +import "monaco-editor/esm/vs/editor/contrib/folding/browser/folding.js"; +import { editor } from "monaco-editor/esm/vs/editor/editor.api"; + +interface Props { + uid: string; + open: boolean; + mode: "yaml" | "javascript"; + onClose: () => void; + onChange?: () => void; +} + +export const EditorViewer = (props: Props) => { + const { uid, open, mode, onClose, onChange } = props; + + const { t } = useTranslation(); + const editorRef = useRef(); + const instanceRef = useRef(null); + const themeMode = useRecoilValue(atomThemeMode); + + useEffect(() => { + if (!open) return; + + readProfileFile(uid).then((data) => { + const dom = editorRef.current; + + if (!dom) return; + if (instanceRef.current) instanceRef.current.dispose(); + + instanceRef.current = editor.create(editorRef.current, { + value: data, + language: mode, + theme: themeMode === "light" ? "vs" : "vs-dark", + minimap: { enabled: false }, + }); + }); + + return () => { + if (instanceRef.current) { + instanceRef.current.dispose(); + instanceRef.current = null; + } + }; + }, [open]); + + const onSave = useLockFn(async () => { + const value = instanceRef.current?.getValue(); + + if (value == null) return; + + try { + await saveProfileFile(uid, value); + onChange?.(); + onClose(); + } catch (err: any) { + Notice.error(err.message || err.toString()); + } + }); + + return ( + + {t("Edit File")} + + +
+ + + + + + +
+ ); +}; diff --git a/src/components/profile/file-input.tsx b/src/components/profile/file-input.tsx new file mode 100644 index 0000000..5357224 --- /dev/null +++ b/src/components/profile/file-input.tsx @@ -0,0 +1,61 @@ +import { useRef, useState } from "react"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { Box, Button, Typography } from "@mui/material"; + +interface Props { + onChange: (value: string) => void; +} + +export const FileInput = (props: Props) => { + const { onChange } = props; + + const { t } = useTranslation(); + // file input + const inputRef = useRef(); + const [loading, setLoading] = useState(false); + const [fileName, setFileName] = useState(""); + + const onFileInput = useLockFn(async (e: any) => { + const file = e.target.files?.[0] as File; + + if (!file) return; + + setFileName(file.name); + setLoading(true); + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (event) => { + resolve(null); + onChange(event.target?.result as string); + }; + reader.onerror = reject; + reader.readAsText(file); + }).finally(() => setLoading(false)); + }); + + return ( + + + + + + + {loading ? "Loading..." : fileName} + + + ); +}; diff --git a/src/components/profile/log-viewer.tsx b/src/components/profile/log-viewer.tsx new file mode 100644 index 0000000..4792468 --- /dev/null +++ b/src/components/profile/log-viewer.tsx @@ -0,0 +1,69 @@ +import { Fragment } from "react"; +import { useTranslation } from "react-i18next"; +import { + Button, + Chip, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Typography, +} from "@mui/material"; +import { BaseEmpty } from "@/components/base"; + +interface Props { + open: boolean; + logInfo: [string, string][]; + onClose: () => void; +} + +export const LogViewer = (props: Props) => { + const { open, logInfo, onClose } = props; + + const { t } = useTranslation(); + + return ( + + {t("Script Console")} + + + {logInfo.map(([level, log], index) => ( + + + + {log} + + + + ))} + + {logInfo.length === 0 && } + + + + + + + ); +}; diff --git a/src/components/profile/profile-box.tsx b/src/components/profile/profile-box.tsx new file mode 100644 index 0000000..6e15a87 --- /dev/null +++ b/src/components/profile/profile-box.tsx @@ -0,0 +1,44 @@ +import { alpha, Box, styled } from "@mui/material"; + +export const ProfileBox = styled(Box)( + ({ theme, "aria-selected": selected }) => { + const { mode, primary, text, grey, background } = theme.palette; + const key = `${mode}-${!!selected}`; + + const backgroundColor = { + "light-true": alpha(primary.main, 0.2), + "light-false": alpha(background.paper, 0.75), + "dark-true": alpha(primary.main, 0.45), + "dark-false": alpha(grey[700], 0.45), + }[key]!; + + const color = { + "light-true": text.secondary, + "light-false": text.secondary, + "dark-true": alpha(text.secondary, 0.85), + "dark-false": alpha(text.secondary, 0.65), + }[key]!; + + const h2color = { + "light-true": primary.main, + "light-false": text.primary, + "dark-true": primary.light, + "dark-false": text.primary, + }[key]!; + + return { + position: "relative", + width: "100%", + display: "block", + cursor: "pointer", + textAlign: "left", + borderRadius: theme.shape.borderRadius, + boxShadow: theme.shadows[2], + padding: "8px 16px", + boxSizing: "border-box", + backgroundColor, + color, + "& h2": { color: h2color }, + }; + } +); diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx new file mode 100644 index 0000000..72fda6f --- /dev/null +++ b/src/components/profile/profile-item.tsx @@ -0,0 +1,341 @@ +import dayjs from "dayjs"; +import { mutate } from "swr"; +import { useEffect, useState } from "react"; +import { useLockFn } from "ahooks"; +import { useRecoilState } from "recoil"; +import { useTranslation } from "react-i18next"; +import { + Box, + Typography, + LinearProgress, + IconButton, + keyframes, + MenuItem, + Menu, + CircularProgress, +} from "@mui/material"; +import { RefreshRounded } from "@mui/icons-material"; +import { atomLoadingCache } from "@/services/states"; +import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds"; +import { Notice } from "@/components/base"; +import { EditorViewer } from "./editor-viewer"; +import { ProfileBox } from "./profile-box"; +import parseTraffic from "@/utils/parse-traffic"; + +const round = keyframes` + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +`; + +interface Props { + selected: boolean; + activating: boolean; + itemData: IProfileItem; + onSelect: (force: boolean) => void; + onEdit: () => void; +} + +export const ProfileItem = (props: Props) => { + const { selected, activating, itemData, onSelect, onEdit } = props; + + const { t } = useTranslation(); + const [anchorEl, setAnchorEl] = useState(null); + const [position, setPosition] = useState({ left: 0, top: 0 }); + const [loadingCache, setLoadingCache] = useRecoilState(atomLoadingCache); + + const { uid, name = "Profile", extra, updated = 0 } = itemData; + + // local file mode + // remote file mode + const hasUrl = !!itemData.url; + const hasExtra = !!extra; // only subscription url has extra info + + const { upload = 0, download = 0, total = 0 } = extra ?? {}; + const from = parseUrl(itemData.url); + const expire = parseExpire(extra?.expire); + const progress = Math.round(((download + upload) * 100) / (total + 0.1)); + + const loading = loadingCache[itemData.uid] ?? false; + + // interval update fromNow field + const [, setRefresh] = useState({}); + useEffect(() => { + if (!hasUrl) return; + + let timer: any = null; + + const handler = () => { + const now = Date.now(); + const lastUpdate = updated * 1000; + // 大于一天的不管 + if (now - lastUpdate >= 24 * 36e5) return; + + const wait = now - lastUpdate >= 36e5 ? 30e5 : 5e4; + + timer = setTimeout(() => { + setRefresh({}); + handler(); + }, wait); + }; + + handler(); + + return () => { + if (timer) clearTimeout(timer); + }; + }, [hasUrl, updated]); + + const [fileOpen, setFileOpen] = useState(false); + + const onEditInfo = () => { + setAnchorEl(null); + onEdit(); + }; + + const onEditFile = () => { + setAnchorEl(null); + setFileOpen(true); + }; + + const onForceSelect = () => { + setAnchorEl(null); + onSelect(true); + }; + + const onOpenFile = useLockFn(async () => { + setAnchorEl(null); + try { + await viewProfile(itemData.uid); + } catch (err: any) { + Notice.error(err?.message || err.toString()); + } + }); + + /// 0 不使用任何代理 + /// 1 使用配置好的代理 + /// 2 至少使用一个代理,根据配置,如果没配置,默认使用系统代理 + const onUpdate = useLockFn(async (type: 0 | 1 | 2) => { + setAnchorEl(null); + setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true })); + + const option: Partial = {}; + + if (type === 0) { + option.with_proxy = false; + option.self_proxy = false; + } else if (type === 1) { + // nothing + } else if (type === 2) { + if (itemData.option?.self_proxy) { + option.with_proxy = false; + option.self_proxy = true; + } else { + option.with_proxy = true; + option.self_proxy = false; + } + } + + try { + await updateProfile(itemData.uid, option); + mutate("getProfiles"); + } catch (err: any) { + const errmsg = err?.message || err.toString(); + Notice.error( + errmsg.replace(/error sending request for url (\S+?): /, "") + ); + } finally { + setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false })); + } + }); + + const onDelete = useLockFn(async () => { + setAnchorEl(null); + try { + await deleteProfile(itemData.uid); + mutate("getProfiles"); + } catch (err: any) { + Notice.error(err?.message || err.toString()); + } + }); + + const urlModeMenu = [ + { label: "Select", handler: onForceSelect }, + { label: "Edit Info", handler: onEditInfo }, + { label: "Edit File", handler: onEditFile }, + { label: "Open File", handler: onOpenFile }, + { label: "Update", handler: () => onUpdate(0) }, + { label: "Update(Proxy)", handler: () => onUpdate(2) }, + { label: "Delete", handler: onDelete }, + ]; + const fileModeMenu = [ + { label: "Select", handler: onForceSelect }, + { label: "Edit Info", handler: onEditInfo }, + { label: "Edit File", handler: onEditFile }, + { label: "Open File", handler: onOpenFile }, + { label: "Delete", handler: onDelete }, + ]; + + const boxStyle = { + height: 26, + display: "flex", + alignItems: "center", + justifyContent: "space-between", + }; + + return ( + <> + onSelect(false)} + onContextMenu={(event) => { + const { clientX, clientY } = event; + setPosition({ top: clientY, left: clientX }); + setAnchorEl(event.currentTarget); + event.preventDefault(); + }} + > + {activating && ( + + + + )} + + + + {name} + + + {/* only if has url can it be updated */} + {hasUrl && ( + { + e.stopPropagation(); + onUpdate(1); + }} + > + + + )} + + + {/* the second line show url's info or description */} + + {hasUrl ? ( + <> + + {from} + + + + {updated > 0 ? dayjs(updated * 1000).fromNow() : ""} + + + ) : ( + + {itemData.desc} + + )} + + + {/* the third line show extra info or last updated time */} + {hasExtra ? ( + + + {parseTraffic(upload + download)} / {parseTraffic(total)} + + {expire} + + ) : ( + + {parseExpire(updated)} + + )} + + + + + setAnchorEl(null)} + anchorPosition={position} + anchorReference="anchorPosition" + transitionDuration={225} + MenuListProps={{ sx: { py: 0.5 } }} + onContextMenu={(e) => { + setAnchorEl(null); + e.preventDefault(); + }} + > + {(hasUrl ? urlModeMenu : fileModeMenu).map((item) => ( + + {t(item.label)} + + ))} + + + setFileOpen(false)} + /> + + ); +}; + +function parseUrl(url?: string) { + if (!url) return ""; + const regex = /https?:\/\/(.+?)\//; + const result = url.match(regex); + return result ? result[1] : "local file"; +} + +function parseExpire(expire?: number) { + if (!expire) return "-"; + return dayjs(expire * 1000).format("YYYY-MM-DD"); +} diff --git a/src/components/profile/profile-more.tsx b/src/components/profile/profile-more.tsx new file mode 100644 index 0000000..f979665 --- /dev/null +++ b/src/components/profile/profile-more.tsx @@ -0,0 +1,243 @@ +import dayjs from "dayjs"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useLockFn } from "ahooks"; +import { + Box, + Badge, + Chip, + Typography, + MenuItem, + Menu, + IconButton, +} from "@mui/material"; +import { FeaturedPlayListRounded } from "@mui/icons-material"; +import { viewProfile } from "@/services/cmds"; +import { Notice } from "@/components/base"; +import { EditorViewer } from "./editor-viewer"; +import { ProfileBox } from "./profile-box"; +import { LogViewer } from "./log-viewer"; + +interface Props { + selected: boolean; + itemData: IProfileItem; + enableNum: number; + logInfo?: [string, string][]; + onEnable: () => void; + onDisable: () => void; + onMoveTop: () => void; + onMoveEnd: () => void; + onDelete: () => void; + onEdit: () => void; +} + +// profile enhanced item +export const ProfileMore = (props: Props) => { + const { + selected, + itemData, + enableNum, + logInfo = [], + onEnable, + onDisable, + onMoveTop, + onMoveEnd, + onDelete, + onEdit, + } = props; + + const { uid, type } = itemData; + const { t, i18n } = useTranslation(); + const [anchorEl, setAnchorEl] = useState(null); + const [position, setPosition] = useState({ left: 0, top: 0 }); + const [fileOpen, setFileOpen] = useState(false); + const [logOpen, setLogOpen] = useState(false); + + const onEditInfo = () => { + setAnchorEl(null); + onEdit(); + }; + + const onEditFile = () => { + setAnchorEl(null); + setFileOpen(true); + }; + + const onOpenFile = useLockFn(async () => { + setAnchorEl(null); + try { + await viewProfile(itemData.uid); + } catch (err: any) { + Notice.error(err?.message || err.toString()); + } + }); + + const fnWrapper = (fn: () => void) => () => { + setAnchorEl(null); + return fn(); + }; + + const hasError = !!logInfo.find((e) => e[0] === "exception"); + const showMove = enableNum > 1 && !hasError; + + const enableMenu = [ + { label: "Disable", handler: fnWrapper(onDisable) }, + { label: "Edit Info", handler: onEditInfo }, + { label: "Edit File", handler: onEditFile }, + { label: "Open File", handler: onOpenFile }, + { label: "To Top", show: showMove, handler: fnWrapper(onMoveTop) }, + { label: "To End", show: showMove, handler: fnWrapper(onMoveEnd) }, + { label: "Delete", handler: fnWrapper(onDelete) }, + ]; + + const disableMenu = [ + { label: "Enable", handler: fnWrapper(onEnable) }, + { label: "Edit Info", handler: onEditInfo }, + { label: "Edit File", handler: onEditFile }, + { label: "Open File", handler: onOpenFile }, + { label: "Delete", handler: fnWrapper(onDelete) }, + ]; + + const boxStyle = { + height: 26, + display: "flex", + alignItems: "center", + justifyContent: "space-between", + lineHeight: 1, + }; + + return ( + <> + onSelect(false)} + onContextMenu={(event) => { + const { clientX, clientY } = event; + setPosition({ top: clientY, left: clientX }); + setAnchorEl(event.currentTarget); + event.preventDefault(); + }} + > + + + {itemData.name} + + + + + + + {selected && type === "script" ? ( + hasError ? ( + + setLogOpen(true)} + > + + + + ) : ( + setLogOpen(true)} + > + + + ) + ) : ( + + {itemData.desc} + + )} + + + {!!itemData.updated + ? dayjs(itemData.updated! * 1000).fromNow() + : ""} + + + + + setAnchorEl(null)} + anchorPosition={position} + anchorReference="anchorPosition" + transitionDuration={225} + MenuListProps={{ sx: { py: 0.5 } }} + onContextMenu={(e) => { + setAnchorEl(null); + e.preventDefault(); + }} + > + {(selected ? enableMenu : disableMenu) + .filter((item: any) => item.show !== false) + .map((item) => ( + + {t(item.label)} + + ))} + + + setFileOpen(false)} + /> + + {selected && ( + setLogOpen(false)} + /> + )} + + ); +}; + +function parseExpire(expire?: number) { + if (!expire) return "-"; + return dayjs(expire * 1000).format("YYYY-MM-DD"); +} diff --git a/src/components/profile/profile-viewer.tsx b/src/components/profile/profile-viewer.tsx new file mode 100644 index 0000000..f928053 --- /dev/null +++ b/src/components/profile/profile-viewer.tsx @@ -0,0 +1,279 @@ +import { + forwardRef, + useEffect, + useImperativeHandle, + useRef, + useState, +} from "react"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { useForm, Controller } from "react-hook-form"; +import { + Box, + FormControl, + InputAdornment, + InputLabel, + MenuItem, + Select, + Switch, + styled, + TextField, +} from "@mui/material"; +import { createProfile, patchProfile } from "@/services/cmds"; +import { BaseDialog, Notice } from "@/components/base"; +import { version } from "@root/package.json"; +import { FileInput } from "./file-input"; + +interface Props { + onChange: () => void; +} + +export interface ProfileViewerRef { + create: () => void; + edit: (item: IProfileItem) => void; +} + +// create or edit the profile +// remote / local / merge / script +export const ProfileViewer = forwardRef( + (props, ref) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const [openType, setOpenType] = useState<"new" | "edit">("new"); + + // file input + const fileDataRef = useRef(null); + + const { control, watch, register, ...formIns } = useForm({ + defaultValues: { + type: "remote", + name: "Remote File", + desc: "", + url: "", + option: { + // user_agent: "", + with_proxy: false, + self_proxy: false, + }, + }, + }); + + useImperativeHandle(ref, () => ({ + create: () => { + setOpenType("new"); + setOpen(true); + }, + edit: (item) => { + if (item) { + Object.entries(item).forEach(([key, value]) => { + formIns.setValue(key as any, value); + }); + } + setOpenType("edit"); + setOpen(true); + }, + })); + + const selfProxy = watch("option.self_proxy"); + const withProxy = watch("option.with_proxy"); + + useEffect(() => { + if (selfProxy) formIns.setValue("option.with_proxy", false); + }, [selfProxy]); + + useEffect(() => { + if (withProxy) formIns.setValue("option.self_proxy", false); + }, [withProxy]); + + const handleOk = useLockFn( + formIns.handleSubmit(async (form) => { + try { + if (!form.type) throw new Error("`Type` should not be null"); + if (form.type === "remote" && !form.url) { + throw new Error("The URL should not be null"); + } + if (form.type !== "remote" && form.type !== "local") { + delete form.option; + } + if (form.option?.update_interval) { + form.option.update_interval = +form.option.update_interval; + } + const name = form.name || `${form.type} file`; + const item = { ...form, name }; + + // 创建 + if (openType === "new") { + await createProfile(item, fileDataRef.current); + } + // 编辑 + else { + if (!form.uid) throw new Error("UID not found"); + await patchProfile(form.uid, item); + } + setOpen(false); + setTimeout(() => formIns.reset(), 500); + fileDataRef.current = null; + props.onChange(); + } catch (err: any) { + Notice.error(err.message || err.toString()); + } + }) + ); + + const handleClose = () => { + setOpen(false); + fileDataRef.current = null; + setTimeout(() => formIns.reset(), 500); + }; + + const text = { + fullWidth: true, + size: "small", + margin: "normal", + variant: "outlined", + autoComplete: "off", + autoCorrect: "off", + } as const; + + const formType = watch("type"); + const isRemote = formType === "remote"; + const isLocal = formType === "local"; + + return ( + + ( + + {t("Type")} + + + )} + /> + + ( + + )} + /> + + ( + + )} + /> + + {isRemote && ( + <> + ( + + )} + /> + + ( + + )} + /> + + )} + + {(isRemote || isLocal) && ( + ( + { + e.target.value = e.target.value + ?.replace(/\D/, "") + .slice(0, 10); + field.onChange(e); + }} + label={t("Update Interval")} + InputProps={{ + endAdornment: ( + mins + ), + }} + /> + )} + /> + )} + + {isLocal && openType === "new" && ( + (fileDataRef.current = val)} /> + )} + + {isRemote && ( + <> + ( + + {t("Use System Proxy")} + + + )} + /> + + ( + + {t("Use Clash Proxy")} + + + )} + /> + + )} + + ); + } +); + +const StyledBox = styled(Box)(() => ({ + margin: "8px 0 8px 8px", + display: "flex", + alignItems: "center", + justifyContent: "space-between", +})); diff --git a/src/components/proxy/provider-button.tsx b/src/components/proxy/provider-button.tsx new file mode 100644 index 0000000..5a63072 --- /dev/null +++ b/src/components/proxy/provider-button.tsx @@ -0,0 +1,86 @@ +import dayjs from "dayjs"; +import useSWR, { mutate } from "swr"; +import { useState } from "react"; +import { + Button, + IconButton, + List, + ListItem, + ListItemText, +} from "@mui/material"; +import { RefreshRounded } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; +import { useLockFn } from "ahooks"; +import { getProviders, providerUpdate } from "@/services/api"; +import { BaseDialog } from "../base"; + +export const ProviderButton = () => { + const { t } = useTranslation(); + const { data } = useSWR("getProviders", getProviders); + + const [open, setOpen] = useState(false); + + const hasProvider = Object.keys(data || {}).length > 0; + + const handleUpdate = useLockFn(async (key: string) => { + await providerUpdate(key); + await mutate("getProxies"); + await mutate("getProviders"); + }); + + if (!hasProvider) return null; + + return ( + <> + + + setOpen(false)} + onCancel={() => setOpen(false)} + > + + {Object.entries(data || {}).map(([key, item]) => { + const time = dayjs(item.updatedAt); + return ( + + + + Type: {item.vehicleType} + + + Updated: {time.fromNow()} + + + } + /> + handleUpdate(key)} + > + + + + ); + })} + + + + ); +}; diff --git a/src/components/proxy/proxy-groups.tsx b/src/components/proxy/proxy-groups.tsx new file mode 100644 index 0000000..4637cb4 --- /dev/null +++ b/src/components/proxy/proxy-groups.tsx @@ -0,0 +1,135 @@ +import { useRef } from "react"; +import { useLockFn } from "ahooks"; +import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; +import { + getConnections, + providerHealthCheck, + updateProxy, + deleteConnection, +} from "@/services/api"; +import { useProfiles } from "@/hooks/use-profiles"; +import { useVerge } from "@/hooks/use-verge"; +import { BaseEmpty } from "../base"; +import { useRenderList } from "./use-render-list"; +import { ProxyRender } from "./proxy-render"; +import delayManager from "@/services/delay"; + +interface Props { + mode: string; +} + +export const ProxyGroups = (props: Props) => { + const { mode } = props; + + const { renderList, onProxies, onHeadState } = useRenderList(mode); + + const { verge } = useVerge(); + const { current, patchCurrent } = useProfiles(); + + const virtuosoRef = useRef(null); + + // 切换分组的节点代理 + const handleChangeProxy = useLockFn( + async (group: IProxyGroupItem, proxy: IProxyItem) => { + if (group.type !== "Selector" && group.type !== "Fallback") return; + + const { name, now } = group; + await updateProxy(name, proxy.name); + onProxies(); + + // 断开连接 + if (verge?.auto_close_connection) { + getConnections().then(({ connections }) => { + connections.forEach((conn) => { + if (conn.chains.includes(now!)) { + deleteConnection(conn.id); + } + }); + }); + } + + // 保存到selected中 + if (!current) return; + if (!current.selected) current.selected = []; + + const index = current.selected.findIndex( + (item) => item.name === group.name + ); + + if (index < 0) { + current.selected.push({ name, now: proxy.name }); + } else { + current.selected[index] = { name, now: proxy.name }; + } + await patchCurrent({ selected: current.selected }); + } + ); + + // 测全部延迟 + const handleCheckAll = useLockFn(async (groupName: string) => { + const proxies = renderList + .filter( + (e) => e.group?.name === groupName && (e.type === 2 || e.type === 4) + ) + .flatMap((e) => e.proxyCol || e.proxy!) + .filter(Boolean); + + const providers = new Set(proxies.map((p) => p!.provider!).filter(Boolean)); + + if (providers.size) { + Promise.allSettled( + [...providers].map((p) => providerHealthCheck(p)) + ).then(() => onProxies()); + } + + const names = proxies.filter((p) => !p!.provider).map((p) => p!.name); + await delayManager.checkListDelay(names, groupName); + + onProxies(); + }); + + // 滚到对应的节点 + const handleLocation = (group: IProxyGroupItem) => { + if (!group) return; + const { name, now } = group; + + const index = renderList.findIndex( + (e) => + e.group?.name === name && + ((e.type === 2 && e.proxy?.name === now) || + (e.type === 4 && e.proxyCol?.some((p) => p.name === now))) + ); + + if (index >= 0) { + virtuosoRef.current?.scrollToIndex?.({ + index, + align: "center", + behavior: "smooth", + }); + } + }; + + if (mode === "direct") { + return ; + } + + return ( + ( + + )} + /> + ); +}; diff --git a/src/components/proxy/proxy-head.tsx b/src/components/proxy/proxy-head.tsx new file mode 100644 index 0000000..fe29cb8 --- /dev/null +++ b/src/components/proxy/proxy-head.tsx @@ -0,0 +1,162 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Box, IconButton, TextField, SxProps } from "@mui/material"; +import { + AccessTimeRounded, + MyLocationRounded, + NetworkCheckRounded, + FilterAltRounded, + FilterAltOffRounded, + VisibilityRounded, + VisibilityOffRounded, + WifiTetheringRounded, + WifiTetheringOffRounded, + SortByAlphaRounded, + SortRounded, +} from "@mui/icons-material"; +import { useVerge } from "@/hooks/use-verge"; +import type { HeadState } from "./use-head-state"; +import type { ProxySortType } from "./use-filter-sort"; +import delayManager from "@/services/delay"; + +interface Props { + sx?: SxProps; + groupName: string; + headState: HeadState; + onLocation: () => void; + onCheckDelay: () => void; + onHeadState: (val: Partial) => void; +} + +export const ProxyHead = (props: Props) => { + const { sx = {}, groupName, headState, onHeadState } = props; + + const { showType, sortType, filterText, textState, testUrl } = headState; + + const { t } = useTranslation(); + const [autoFocus, setAutoFocus] = useState(false); + + useEffect(() => { + // fix the focus conflict + const timer = setTimeout(() => setAutoFocus(true), 100); + return () => clearTimeout(timer); + }, []); + + const { verge } = useVerge(); + + useEffect(() => { + delayManager.setUrl(groupName, testUrl || verge?.default_latency_test!); + }, [groupName, testUrl, verge?.default_latency_test]); + + return ( + + + + + + { + // Remind the user that it is custom test url + if (testUrl?.trim() && textState !== "filter") { + onHeadState({ textState: "url" }); + } + props.onCheckDelay(); + }} + > + + + + + onHeadState({ sortType: ((sortType + 1) % 3) as ProxySortType }) + } + > + {sortType !== 1 && sortType !== 2 && } + {sortType === 1 && } + {sortType === 2 && } + + + + onHeadState({ textState: textState === "url" ? null : "url" }) + } + > + {textState === "url" ? ( + + ) : ( + + )} + + + onHeadState({ showType: !showType })} + > + {showType ? : } + + + + onHeadState({ textState: textState === "filter" ? null : "filter" }) + } + > + {textState === "filter" ? ( + + ) : ( + + )} + + + {textState === "filter" && ( + onHeadState({ filterText: e.target.value })} + sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }} + /> + )} + + {textState === "url" && ( + onHeadState({ testUrl: e.target.value })} + sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }} + /> + )} + + ); +}; diff --git a/src/components/proxy/proxy-item-mini.tsx b/src/components/proxy/proxy-item-mini.tsx new file mode 100644 index 0000000..c7e98bc --- /dev/null +++ b/src/components/proxy/proxy-item-mini.tsx @@ -0,0 +1,181 @@ +import { useEffect, useState } from "react"; +import { useLockFn } from "ahooks"; +import { CheckCircleOutlineRounded } from "@mui/icons-material"; +import { alpha, Box, ListItemButton, styled, Typography } from "@mui/material"; +import { BaseLoading } from "@/components/base"; +import delayManager from "@/services/delay"; + +interface Props { + groupName: string; + proxy: IProxyItem; + selected: boolean; + showType?: boolean; + onClick?: (name: string) => void; +} + +// 多列布局 +export const ProxyItemMini = (props: Props) => { + const { groupName, proxy, selected, showType = true, onClick } = props; + + // -1/<=0 为 不显示 + // -2 为 loading + const [delay, setDelay] = useState(-1); + + useEffect(() => { + delayManager.setListener(proxy.name, groupName, setDelay); + + return () => { + delayManager.removeListener(proxy.name, groupName); + }; + }, [proxy.name, groupName]); + + useEffect(() => { + if (!proxy) return; + setDelay(delayManager.getDelayFix(proxy, groupName)); + }, [proxy]); + + const onDelay = useLockFn(async () => { + setDelay(-2); + setDelay(await delayManager.checkDelay(proxy.name, groupName)); + }); + + return ( + onClick?.(proxy.name)} + sx={[ + { + height: 56, + borderRadius: 1, + pl: 1.5, + pr: 1, + justifyContent: "space-between", + alignItems: "center", + }, + ({ palette: { mode, primary } }) => { + const bgcolor = + mode === "light" + ? alpha(primary.main, 0.15) + : alpha(primary.main, 0.35); + const color = mode === "light" ? primary.main : primary.light; + const showDelay = delay > 0; + + const shadowColor = + mode === "light" ? "rgba(0,0,0,0.04)" : "rgba(255,255,255,0.08)"; + + return { + "&:hover .the-check": { display: !showDelay ? "block" : "none" }, + "&:hover .the-delay": { display: showDelay ? "block" : "none" }, + "&:hover .the-icon": { display: "none" }, + "&.Mui-selected": { bgcolor, boxShadow: `0 0 0 1px ${bgcolor}` }, + "&.Mui-selected .MuiListItemText-secondary": { color }, + boxShadow: `0 0 0 1px ${shadowColor}`, + }; + }, + ]} + > + + + {proxy.name} + + + {showType && ( + + {!!proxy.provider && ( + {proxy.provider} + )} + {proxy.type} + {proxy.udp && UDP} + + )} + + + + {delay === -2 && ( + + + + )} + + {!proxy.provider && delay !== -2 && ( + // provider的节点不支持检测 + { + e.preventDefault(); + e.stopPropagation(); + onDelay(); + }} + sx={({ palette }) => ({ + display: "none", // hover才显示 + ":hover": { bgcolor: alpha(palette.primary.main, 0.15) }, + })} + > + Check + + )} + + {delay > 0 && ( + // 显示延迟 + { + if (proxy.provider) return; + e.preventDefault(); + e.stopPropagation(); + onDelay(); + }} + color={delayManager.formatDelayColor(delay)} + sx={({ palette }) => + !proxy.provider + ? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } } + : {} + } + > + {delayManager.formatDelay(delay)} + + )} + + {delay !== -2 && delay <= 0 && selected && ( + // 展示已选择的icon + + )} + + + ); +}; + +const Widget = styled(Box)(({ theme: { typography } }) => ({ + padding: "3px 6px", + fontSize: 14, + fontFamily: typography.fontFamily, + borderRadius: "4px", +})); + +const TypeBox = styled(Box)(({ theme: { palette, typography } }) => ({ + display: "inline-block", + border: "1px solid #ccc", + borderColor: alpha(palette.text.secondary, 0.36), + color: alpha(palette.text.secondary, 0.42), + borderRadius: 4, + fontSize: 10, + fontFamily: typography.fontFamily, + marginRight: "4px", + padding: "0 2px", + lineHeight: 1.25, +})); diff --git a/src/components/proxy/proxy-item.tsx b/src/components/proxy/proxy-item.tsx new file mode 100644 index 0000000..d4c63c4 --- /dev/null +++ b/src/components/proxy/proxy-item.tsx @@ -0,0 +1,170 @@ +import { useEffect, useState } from "react"; +import { useLockFn } from "ahooks"; +import { CheckCircleOutlineRounded } from "@mui/icons-material"; +import { + alpha, + Box, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + styled, + SxProps, + Theme, +} from "@mui/material"; +import { BaseLoading } from "@/components/base"; +import delayManager from "@/services/delay"; + +interface Props { + groupName: string; + proxy: IProxyItem; + selected: boolean; + showType?: boolean; + sx?: SxProps; + onClick?: (name: string) => void; +} + +const Widget = styled(Box)(() => ({ + padding: "3px 6px", + fontSize: 14, + borderRadius: "4px", +})); + +const TypeBox = styled(Box)(({ theme }) => ({ + display: "inline-block", + border: "1px solid #ccc", + borderColor: alpha(theme.palette.text.secondary, 0.36), + color: alpha(theme.palette.text.secondary, 0.42), + borderRadius: 4, + fontSize: 10, + marginRight: "4px", + padding: "0 2px", + lineHeight: 1.25, +})); + +export const ProxyItem = (props: Props) => { + const { groupName, proxy, selected, showType = true, sx, onClick } = props; + + // -1/<=0 为 不显示 + // -2 为 loading + const [delay, setDelay] = useState(-1); + + useEffect(() => { + delayManager.setListener(proxy.name, groupName, setDelay); + + return () => { + delayManager.removeListener(proxy.name, groupName); + }; + }, [proxy.name, groupName]); + + useEffect(() => { + if (!proxy) return; + setDelay(delayManager.getDelayFix(proxy, groupName)); + }, [proxy]); + + const onDelay = useLockFn(async () => { + setDelay(-2); + setDelay(await delayManager.checkDelay(proxy.name, groupName)); + }); + + return ( + + onClick?.(proxy.name)} + sx={[ + { borderRadius: 1 }, + ({ palette: { mode, primary } }) => { + const bgcolor = + mode === "light" + ? alpha(primary.main, 0.15) + : alpha(primary.main, 0.35); + const color = mode === "light" ? primary.main : primary.light; + const showDelay = delay > 0; + + return { + "&:hover .the-check": { display: !showDelay ? "block" : "none" }, + "&:hover .the-delay": { display: showDelay ? "block" : "none" }, + "&:hover .the-icon": { display: "none" }, + "&.Mui-selected": { bgcolor }, + "&.Mui-selected .MuiListItemText-secondary": { color }, + }; + }, + ]} + > + + {proxy.name} + + {showType && !!proxy.provider && ( + {proxy.provider} + )} + {showType && {proxy.type}} + {showType && proxy.udp && UDP} + + } + /> + + + {delay === -2 && ( + + + + )} + + {!proxy.provider && delay !== -2 && ( + // provider的节点不支持检测 + { + e.preventDefault(); + e.stopPropagation(); + onDelay(); + }} + sx={({ palette }) => ({ + display: "none", // hover才显示 + ":hover": { bgcolor: alpha(palette.primary.main, 0.15) }, + })} + > + Check + + )} + + {delay > 0 && ( + // 显示延迟 + { + if (proxy.provider) return; + e.preventDefault(); + e.stopPropagation(); + onDelay(); + }} + color={delayManager.formatDelayColor(delay)} + sx={({ palette }) => + !proxy.provider + ? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } } + : {} + } + > + {delayManager.formatDelay(delay)} + + )} + + {delay !== -2 && delay <= 0 && selected && ( + // 展示已选择的icon + + )} + + + + ); +}; diff --git a/src/components/proxy/proxy-render.tsx b/src/components/proxy/proxy-render.tsx new file mode 100644 index 0000000..012064a --- /dev/null +++ b/src/components/proxy/proxy-render.tsx @@ -0,0 +1,159 @@ +import { + alpha, + Box, + ListItemText, + ListItemButton, + Typography, + styled, +} from "@mui/material"; +import { + ExpandLessRounded, + ExpandMoreRounded, + InboxRounded, +} from "@mui/icons-material"; +import { HeadState } from "./use-head-state"; +import { ProxyHead } from "./proxy-head"; +import { ProxyItem } from "./proxy-item"; +import { ProxyItemMini } from "./proxy-item-mini"; +import type { IRenderItem } from "./use-render-list"; + +interface RenderProps { + item: IRenderItem; + indent: boolean; + onLocation: (group: IProxyGroupItem) => void; + onCheckAll: (groupName: string) => void; + onHeadState: (groupName: string, patch: Partial) => void; + onChangeProxy: (group: IProxyGroupItem, proxy: IProxyItem) => void; +} + +export const ProxyRender = (props: RenderProps) => { + const { indent, item, onLocation, onCheckAll, onHeadState, onChangeProxy } = + props; + const { type, group, headState, proxy, proxyCol } = item; + + if (type === 0) { + return ( + onHeadState(group.name, { open: !headState?.open })} + > + + {group.type} + {group.now} + + } + secondaryTypographyProps={{ + sx: { display: "flex", alignItems: "center" }, + }} + /> + {headState?.open ? : } + + ); + } + + if (type === 1) { + return ( + onLocation(group)} + onCheckDelay={() => onCheckAll(group.name)} + onHeadState={(p) => onHeadState(group.name, p)} + /> + ); + } + + if (type === 2) { + return ( + onChangeProxy(group, proxy!)} + /> + ); + } + + if (type === 3) { + return ( + + + No Proxies + + ); + } + + if (type === 4) { + return ( + + {proxyCol?.map((proxy) => ( + onChangeProxy(group, proxy!)} + /> + ))} + + ); + } + + return null; +}; + +const StyledSubtitle = styled("span")` + font-size: 0.8rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const ListItemTextChild = styled("span")` + display: block; +`; + +const StyledTypeBox = styled(ListItemTextChild)(({ theme }) => ({ + display: "inline-block", + border: "1px solid #ccc", + borderColor: alpha(theme.palette.primary.main, 0.5), + color: alpha(theme.palette.primary.main, 0.8), + borderRadius: 4, + fontSize: 10, + padding: "0 2px", + lineHeight: 1.25, + marginRight: "4px", +})); diff --git a/src/components/proxy/use-filter-sort.ts b/src/components/proxy/use-filter-sort.ts new file mode 100644 index 0000000..3d7b7fd --- /dev/null +++ b/src/components/proxy/use-filter-sort.ts @@ -0,0 +1,125 @@ +import { useEffect, useMemo, useState } from "react"; +import delayManager from "@/services/delay"; + +// default | delay | alphabet +export type ProxySortType = 0 | 1 | 2; + +export default function useFilterSort( + proxies: IProxyItem[], + groupName: string, + filterText: string, + sortType: ProxySortType +) { + const [refresh, setRefresh] = useState({}); + + useEffect(() => { + let last = 0; + + delayManager.setGroupListener(groupName, () => { + // 简单节流 + const now = Date.now(); + if (now - last > 666) { + last = now; + setRefresh({}); + } + }); + + return () => { + delayManager.removeGroupListener(groupName); + }; + }, [groupName]); + + return useMemo(() => { + const fp = filterProxies(proxies, groupName, filterText); + const sp = sortProxies(fp, groupName, sortType); + return sp; + }, [proxies, groupName, filterText, sortType, refresh]); +} + +export function filterSort( + proxies: IProxyItem[], + groupName: string, + filterText: string, + sortType: ProxySortType +) { + const fp = filterProxies(proxies, groupName, filterText); + const sp = sortProxies(fp, groupName, sortType); + return sp; +} + +/** + * 可以通过延迟数/节点类型 过滤 + */ +const regex1 = /delay([=<>])(\d+|timeout|error)/i; +const regex2 = /type=(.*)/i; + +/** + * filter the proxy + * according to the regular conditions + */ +function filterProxies( + proxies: IProxyItem[], + groupName: string, + filterText: string +) { + if (!filterText) return proxies; + + const res1 = regex1.exec(filterText); + if (res1) { + const symbol = res1[1]; + const symbol2 = res1[2].toLowerCase(); + const value = + symbol2 === "error" ? 1e5 : symbol2 === "timeout" ? 3000 : +symbol2; + + return proxies.filter((p) => { + const delay = delayManager.getDelayFix(p, groupName); + + if (delay < 0) return false; + if (symbol === "=" && symbol2 === "error") return delay >= 1e5; + if (symbol === "=" && symbol2 === "timeout") + return delay < 1e5 && delay >= 3000; + if (symbol === "=") return delay == value; + if (symbol === "<") return delay <= value; + if (symbol === ">") return delay >= value; + return false; + }); + } + + const res2 = regex2.exec(filterText); + if (res2) { + const type = res2[1].toLowerCase(); + return proxies.filter((p) => p.type.toLowerCase().includes(type)); + } + + return proxies.filter((p) => p.name.includes(filterText.trim())); +} + +/** + * sort the proxy + */ +function sortProxies( + proxies: IProxyItem[], + groupName: string, + sortType: ProxySortType +) { + if (!proxies) return []; + if (sortType === 0) return proxies; + + const list = proxies.slice(); + + if (sortType === 1) { + list.sort((a, b) => { + const ad = delayManager.getDelayFix(a, groupName); + const bd = delayManager.getDelayFix(b, groupName); + + if (ad === -1 || ad === -2) return 1; + if (bd === -1 || bd === -2) return -1; + + return ad - bd; + }); + } else { + list.sort((a, b) => a.name.localeCompare(b.name)); + } + + return list; +} diff --git a/src/components/proxy/use-head-state.ts b/src/components/proxy/use-head-state.ts new file mode 100644 index 0000000..d1bce2f --- /dev/null +++ b/src/components/proxy/use-head-state.ts @@ -0,0 +1,81 @@ +import { useCallback, useEffect, useState } from "react"; +import { ProxySortType } from "./use-filter-sort"; +import { useProfiles } from "@/hooks/use-profiles"; + +export interface HeadState { + open?: boolean; + showType: boolean; + sortType: ProxySortType; + filterText: string; + textState: "url" | "filter" | null; + testUrl: string; +} + +type HeadStateStorage = Record>; + +const HEAD_STATE_KEY = "proxy-head-state"; +export const DEFAULT_STATE: HeadState = { + open: false, + showType: false, + sortType: 0, + filterText: "", + textState: null, + testUrl: "", +}; + +export function useHeadStateNew() { + const { profiles } = useProfiles(); + const current = profiles?.current || ""; + + const [state, setState] = useState>({}); + + useEffect(() => { + if (!current) { + setState({}); + return; + } + + try { + const data = JSON.parse( + localStorage.getItem(HEAD_STATE_KEY)! + ) as HeadStateStorage; + + const value = data[current] || {}; + + if (value && typeof value === "object") { + setState(value); + } else { + setState({}); + } + } catch {} + }, [current]); + + const setHeadState = useCallback( + (groupName: string, obj: Partial) => { + setState((old) => { + const state = old[groupName] || DEFAULT_STATE; + const ret = { ...old, [groupName]: { ...state, ...obj } }; + + // 保存到存储中 + setTimeout(() => { + try { + const item = localStorage.getItem(HEAD_STATE_KEY); + + let data = (item ? JSON.parse(item) : {}) as HeadStateStorage; + + if (!data || typeof data !== "object") data = {}; + + data[current] = ret; + + localStorage.setItem(HEAD_STATE_KEY, JSON.stringify(data)); + } catch {} + }); + + return ret; + }); + }, + [current] + ); + + return [state, setHeadState] as const; +} diff --git a/src/components/proxy/use-render-list.ts b/src/components/proxy/use-render-list.ts new file mode 100644 index 0000000..9f8bc0a --- /dev/null +++ b/src/components/proxy/use-render-list.ts @@ -0,0 +1,141 @@ +import useSWR from "swr"; +import { useEffect, useMemo } from "react"; +import { getProxies } from "@/services/api"; +import { useVerge } from "@/hooks/use-verge"; +import { filterSort } from "./use-filter-sort"; +import { useWindowWidth } from "./use-window-width"; +import { + useHeadStateNew, + DEFAULT_STATE, + type HeadState, +} from "./use-head-state"; + +export interface IRenderItem { + // 组 | head | item | empty | item col + type: 0 | 1 | 2 | 3 | 4; + key: string; + group: IProxyGroupItem; + proxy?: IProxyItem; + col?: number; + proxyCol?: IProxyItem[]; + headState?: HeadState; +} + +export const useRenderList = (mode: string) => { + const { data: proxiesData, mutate: mutateProxies } = useSWR( + "getProxies", + getProxies, + { refreshInterval: 45000 } + ); + + const { verge } = useVerge(); + const { width } = useWindowWidth(); + + let col = Math.floor(verge?.proxy_layout_column || 6); + + // 自适应 + if (col >= 6 || col <= 0) { + if (width > 1450) col = 5; + else if (width > 1024) col = 4; + else if (width > 900) col = 3; + else if (width >= 600) col = 2; + else col = 1; + } + + const [headStates, setHeadState] = useHeadStateNew(); + + // make sure that fetch the proxies successfully + useEffect(() => { + if (!proxiesData) return; + const { groups, proxies } = proxiesData; + + if ( + (mode === "rule" && !groups.length) || + (mode === "global" && proxies.length < 2) + ) { + setTimeout(() => mutateProxies(), 500); + } + }, [proxiesData, mode]); + + const renderList: IRenderItem[] = useMemo(() => { + if (!proxiesData) return []; + + // global 和 direct 使用展开的样式 + const useRule = mode === "rule" || mode === "script"; + const renderGroups = + (useRule && proxiesData.groups.length + ? proxiesData.groups + : [proxiesData.global!]) || []; + + const retList = renderGroups.flatMap((group) => { + const headState = headStates[group.name] || DEFAULT_STATE; + const ret: IRenderItem[] = [ + { type: 0, key: group.name, group, headState }, + ]; + + if (headState?.open || !useRule) { + const proxies = filterSort( + group.all, + group.name, + headState.filterText, + headState.sortType + ); + + ret.push({ type: 1, key: `head-${group.name}`, group, headState }); + + if (!proxies.length) { + ret.push({ type: 3, key: `empty-${group.name}`, group, headState }); + } + + // 支持多列布局 + if (col > 1) { + return ret.concat( + groupList(proxies, col).map((proxyCol) => ({ + type: 4, + key: `col-${group.name}-${proxyCol[0].name}`, + group, + headState, + col, + proxyCol, + })) + ); + } + + return ret.concat( + proxies.map((proxy) => ({ + type: 2, + key: `${group.name}-${proxy!.name}`, + group, + proxy, + headState, + })) + ); + } + return ret; + }); + + if (!useRule) return retList.slice(1); + return retList; + }, [headStates, proxiesData, mode, col]); + + return { + renderList, + onProxies: mutateProxies, + onHeadState: setHeadState, + }; +}; + +function groupList(list: T[], size: number): T[][] { + return list.reduce((p, n) => { + if (!p.length) return [[n]]; + + const i = p.length - 1; + if (p[i].length < size) { + p[i].push(n); + return p; + } + + p.push([n]); + return p; + }, [] as T[][]); +} diff --git a/src/components/proxy/use-window-width.ts b/src/components/proxy/use-window-width.ts new file mode 100644 index 0000000..d1de3c3 --- /dev/null +++ b/src/components/proxy/use-window-width.ts @@ -0,0 +1,16 @@ +import { useEffect, useState } from "react"; + +export const useWindowWidth = () => { + const [width, setWidth] = useState(() => document.body.clientWidth); + + useEffect(() => { + const handleResize = () => setWidth(document.body.clientWidth); + + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + + return { width }; +}; diff --git a/src/components/rule/rule-item.tsx b/src/components/rule/rule-item.tsx new file mode 100644 index 0000000..3bf2291 --- /dev/null +++ b/src/components/rule/rule-item.tsx @@ -0,0 +1,72 @@ +import { styled, Box, Typography } from "@mui/material"; + +const Item = styled(Box)(({ theme }) => ({ + display: "flex", + padding: "4px 16px", + color: theme.palette.text.primary, +})); + +const COLOR = [ + "primary", + "secondary", + "info.main", + "warning.main", + "success.main", +]; + +interface Props { + index: number; + value: IRuleItem; +} + +const parseColor = (text: string) => { + if (text === "REJECT") return "error.main"; + if (text === "DIRECT") return "text.primary"; + + let sum = 0; + for (let i = 0; i < text.length; i++) { + sum += text.charCodeAt(i); + } + return COLOR[sum % COLOR.length]; +}; + +const RuleItem = (props: Props) => { + const { index, value } = props; + + return ( + + + {index} + + + + + {value.payload || "-"} + + + + {value.type} + + + + {value.proxy} + + + + ); +}; + +export default RuleItem; diff --git a/src/components/setting/mods/clash-core-viewer.tsx b/src/components/setting/mods/clash-core-viewer.tsx new file mode 100644 index 0000000..f043684 --- /dev/null +++ b/src/components/setting/mods/clash-core-viewer.tsx @@ -0,0 +1,132 @@ +import { mutate } from "swr"; +import { forwardRef, useImperativeHandle, useState } from "react"; +import { BaseDialog, DialogRef, Notice } from "@/components/base"; +import { useTranslation } from "react-i18next"; +import { useVerge } from "@/hooks/use-verge"; +import { useLockFn } from "ahooks"; +import { Lock } from "@mui/icons-material"; +import { + Box, + Button, + IconButton, + List, + ListItemButton, + ListItemText, +} from "@mui/material"; +import { changeClashCore, restartSidecar } from "@/services/cmds"; +import { closeAllConnections } from "@/services/api"; +import { grantPermission } from "@/services/cmds"; +import getSystem from "@/utils/get-system"; + +const VALID_CORE = [ + { name: "Clash", core: "clash" }, + { name: "Clash Meta", core: "clash-meta" }, +]; + +const OS = getSystem(); + +export const ClashCoreViewer = forwardRef((props, ref) => { + const { t } = useTranslation(); + + const { verge, mutateVerge } = useVerge(); + + const [open, setOpen] = useState(false); + + useImperativeHandle(ref, () => ({ + open: () => setOpen(true), + close: () => setOpen(false), + })); + + const { clash_core = "clash" } = verge ?? {}; + + const onCoreChange = useLockFn(async (core: string) => { + if (core === clash_core) return; + + try { + closeAllConnections(); + await changeClashCore(core); + mutateVerge(); + setTimeout(() => { + mutate("getClashConfig"); + mutate("getVersion"); + }, 100); + Notice.success(`Successfully switch to ${core}`, 1000); + } catch (err: any) { + Notice.error(err?.message || err.toString()); + } + }); + + const onGrant = useLockFn(async (core: string) => { + try { + await grantPermission(core); + // 自动重启 + if (core === clash_core) await restartSidecar(); + Notice.success(`Successfully grant permission to ${core}`, 1000); + } catch (err: any) { + Notice.error(err?.message || err.toString()); + } + }); + + const onRestart = useLockFn(async () => { + try { + await restartSidecar(); + Notice.success(`Successfully restart core`, 1000); + } catch (err: any) { + Notice.error(err?.message || err.toString()); + } + }); + + return ( + + {t("Clash Core")} + + + + } + contentSx={{ + pb: 0, + width: 320, + height: 200, + overflowY: "auto", + userSelect: "text", + marginTop: "-8px", + }} + disableOk + cancelBtn={t("Back")} + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + > + + {VALID_CORE.map((each) => ( + onCoreChange(each.core)} + > + + + {(OS === "macos" || OS === "linux") && ( + { + e.preventDefault(); + e.stopPropagation(); + onGrant(each.core); + }} + > + + + )} + + ))} + + + ); +}); diff --git a/src/components/setting/mods/clash-field-viewer.tsx b/src/components/setting/mods/clash-field-viewer.tsx new file mode 100644 index 0000000..8185cba --- /dev/null +++ b/src/components/setting/mods/clash-field-viewer.tsx @@ -0,0 +1,123 @@ +import useSWR from "swr"; +import { forwardRef, useImperativeHandle, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Checkbox, Divider, Stack, Tooltip, Typography } from "@mui/material"; +import { InfoRounded } from "@mui/icons-material"; +import { getRuntimeExists } from "@/services/cmds"; +import { + HANDLE_FIELDS, + DEFAULT_FIELDS, + OTHERS_FIELDS, +} from "@/utils/clash-fields"; +import { BaseDialog, DialogRef } from "@/components/base"; +import { useProfiles } from "@/hooks/use-profiles"; +import { Notice } from "@/components/base"; + +const otherFields = [...OTHERS_FIELDS]; +const handleFields = [...HANDLE_FIELDS, ...DEFAULT_FIELDS]; + +export const ClashFieldViewer = forwardRef((props, ref) => { + const { t } = useTranslation(); + + const { profiles = {}, patchProfiles } = useProfiles(); + const { data: existsKeys = [], mutate: mutateExists } = useSWR( + "getRuntimeExists", + getRuntimeExists + ); + + const [open, setOpen] = useState(false); + const [selected, setSelected] = useState([]); + + useImperativeHandle(ref, () => ({ + open: () => { + mutateExists(); + setSelected(profiles.valid || []); + setOpen(true); + }, + close: () => setOpen(false), + })); + + const handleChange = (item: string) => { + if (!item) return; + + setSelected((old) => + old.includes(item) ? old.filter((e) => e !== item) : [...old, item] + ); + }; + + const handleSave = async () => { + setOpen(false); + + const oldSet = new Set(profiles.valid || []); + const curSet = new Set(selected); + const joinSet = new Set(selected.concat([...oldSet])); + + if (curSet.size === oldSet.size && curSet.size === joinSet.size) return; + + try { + await patchProfiles({ valid: [...curSet] }); + // Notice.success("Refresh clash config", 1000); + } catch (err: any) { + Notice.error(err?.message || err.toString()); + } + }; + + return ( + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={handleSave} + > + {otherFields.map((item) => { + const inSelect = selected.includes(item); + const inConfig = existsKeys.includes(item); + + return ( + + handleChange(item)} + /> + {item} + + {!inSelect && inConfig && } + + ); + })} + + + + Clash Verge Control Fields + + + + {handleFields.map((item) => ( + + + {item} + + ))} + + ); +}); + +function WarnIcon() { + return ( + + + + ); +} diff --git a/src/components/setting/mods/clash-port-viewer.tsx b/src/components/setting/mods/clash-port-viewer.tsx new file mode 100644 index 0000000..2079740 --- /dev/null +++ b/src/components/setting/mods/clash-port-viewer.tsx @@ -0,0 +1,65 @@ +import { forwardRef, useImperativeHandle, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useLockFn } from "ahooks"; +import { List, ListItem, ListItemText, TextField } from "@mui/material"; +import { useClashInfo } from "@/hooks/use-clash"; +import { BaseDialog, DialogRef, Notice } from "@/components/base"; + +export const ClashPortViewer = forwardRef((props, ref) => { + const { t } = useTranslation(); + + const { clashInfo, patchInfo } = useClashInfo(); + + const [open, setOpen] = useState(false); + const [port, setPort] = useState(clashInfo?.port ?? 7890); + + useImperativeHandle(ref, () => ({ + open: () => { + if (clashInfo?.port) setPort(clashInfo?.port); + setOpen(true); + }, + close: () => setOpen(false), + })); + + const onSave = useLockFn(async () => { + if (port === clashInfo?.port) { + setOpen(false); + return; + } + try { + await patchInfo({ "mixed-port": port }); + setOpen(false); + Notice.success("Change Clash port successfully!", 1000); + } catch (err: any) { + Notice.error(err.message || err.toString(), 4000); + } + }); + + return ( + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + + + + + setPort(+e.target.value?.replace(/\D+/, "").slice(0, 5)) + } + /> + + + + ); +}); diff --git a/src/components/setting/mods/config-viewer.tsx b/src/components/setting/mods/config-viewer.tsx new file mode 100644 index 0000000..22c169c --- /dev/null +++ b/src/components/setting/mods/config-viewer.tsx @@ -0,0 +1,76 @@ +import { + forwardRef, + useEffect, + useImperativeHandle, + useRef, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; +import { useRecoilValue } from "recoil"; +import { Chip } from "@mui/material"; +import { atomThemeMode } from "@/services/states"; +import { getRuntimeYaml } from "@/services/cmds"; +import { BaseDialog, DialogRef } from "@/components/base"; +import { editor } from "monaco-editor/esm/vs/editor/editor.api"; + +import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js"; +import "monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js"; +import "monaco-editor/esm/vs/editor/contrib/folding/browser/folding.js"; + +export const ConfigViewer = forwardRef((props, ref) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + + const editorRef = useRef(); + const instanceRef = useRef(null); + const themeMode = useRecoilValue(atomThemeMode); + + useEffect(() => { + return () => { + if (instanceRef.current) { + instanceRef.current.dispose(); + instanceRef.current = null; + } + }; + }, []); + + useImperativeHandle(ref, () => ({ + open: () => { + setOpen(true); + + getRuntimeYaml().then((data) => { + const dom = editorRef.current; + + if (!dom) return; + if (instanceRef.current) instanceRef.current.dispose(); + + instanceRef.current = editor.create(editorRef.current, { + value: data ?? "# Error\n", + language: "yaml", + theme: themeMode === "light" ? "vs" : "vs-dark", + minimap: { enabled: false }, + readOnly: true, + }); + }); + }, + close: () => setOpen(false), + })); + + return ( + + {t("Runtime Config")} + + } + contentSx={{ width: 520, pb: 1, userSelect: "text" }} + cancelBtn={t("Back")} + disableOk + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + > +
+ + ); +}); diff --git a/src/components/setting/mods/controller-viewer.tsx b/src/components/setting/mods/controller-viewer.tsx new file mode 100644 index 0000000..17205d9 --- /dev/null +++ b/src/components/setting/mods/controller-viewer.tsx @@ -0,0 +1,74 @@ +import { forwardRef, useImperativeHandle, useState } from "react"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { List, ListItem, ListItemText, TextField } from "@mui/material"; +import { useClashInfo } from "@/hooks/use-clash"; +import { BaseDialog, DialogRef, Notice } from "@/components/base"; + +export const ControllerViewer = forwardRef((props, ref) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + + const { clashInfo, patchInfo } = useClashInfo(); + + const [controller, setController] = useState(clashInfo?.server || ""); + const [secret, setSecret] = useState(clashInfo?.secret || ""); + + useImperativeHandle(ref, () => ({ + open: () => { + setOpen(true); + setController(clashInfo?.server || ""); + setSecret(clashInfo?.secret || ""); + }, + close: () => setOpen(false), + })); + + const onSave = useLockFn(async () => { + try { + await patchInfo({ "external-controller": controller, secret }); + Notice.success("Change Clash Config successfully!", 1000); + setOpen(false); + } catch (err: any) { + Notice.error(err.message || err.toString(), 4000); + } + }); + + return ( + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + + + + setController(e.target.value)} + /> + + + + + setSecret(e.target.value)} + /> + + + + ); +}); diff --git a/src/components/setting/mods/guard-state.tsx b/src/components/setting/mods/guard-state.tsx new file mode 100644 index 0000000..5ab8e99 --- /dev/null +++ b/src/components/setting/mods/guard-state.tsx @@ -0,0 +1,85 @@ +import { cloneElement, isValidElement, ReactNode, useRef } from "react"; +import noop from "@/utils/noop"; + +interface Props { + value?: Value; + valueProps?: string; + onChangeProps?: string; + waitTime?: number; + onChange?: (value: Value) => void; + onFormat?: (...args: any[]) => Value; + onGuard?: (value: Value, oldValue: Value) => Promise; + onCatch?: (error: Error) => void; + children: ReactNode; +} + +export function GuardState(props: Props) { + const { + value, + children, + valueProps = "value", + onChangeProps = "onChange", + waitTime = 0, // debounce wait time default 0 + onGuard = noop, + onCatch = noop, + onChange = noop, + onFormat = (v: T) => v, + } = props; + + const lockRef = useRef(false); + const saveRef = useRef(value); + const lastRef = useRef(0); + const timeRef = useRef(); + + if (!isValidElement(children)) { + return children as any; + } + + const childProps = { ...children.props }; + + childProps[valueProps] = value; + childProps[onChangeProps] = async (...args: any[]) => { + // 多次操作无效 + if (lockRef.current) return; + + lockRef.current = true; + + try { + const newValue = (onFormat as any)(...args); + // 先在ui上响应操作 + onChange(newValue); + + const now = Date.now(); + + // save the old value + if (waitTime <= 0 || now - lastRef.current >= waitTime) { + saveRef.current = value; + } + + lastRef.current = now; + + if (waitTime <= 0) { + await onGuard(newValue, value!); + } else { + // debounce guard + clearTimeout(timeRef.current); + + timeRef.current = setTimeout(async () => { + try { + await onGuard(newValue, saveRef.current!); + } catch (err: any) { + // 状态回退 + onChange(saveRef.current!); + onCatch(err); + } + }, waitTime); + } + } catch (err: any) { + // 状态回退 + onChange(saveRef.current!); + onCatch(err); + } + lockRef.current = false; + }; + return cloneElement(children, childProps); +} diff --git a/src/components/setting/mods/hotkey-input.tsx b/src/components/setting/mods/hotkey-input.tsx new file mode 100644 index 0000000..a14658a --- /dev/null +++ b/src/components/setting/mods/hotkey-input.tsx @@ -0,0 +1,107 @@ +import { useRef, useState } from "react"; +import { alpha, Box, IconButton, styled } from "@mui/material"; +import { DeleteRounded } from "@mui/icons-material"; +import { parseHotkey } from "@/utils/parse-hotkey"; + +const KeyWrapper = styled("div")(({ theme }) => ({ + position: "relative", + width: 165, + minHeight: 36, + + "> input": { + position: "absolute", + top: 0, + left: 0, + width: "100%", + height: "100%", + zIndex: 1, + opacity: 0, + }, + "> input:focus + .list": { + borderColor: alpha(theme.palette.primary.main, 0.75), + }, + ".list": { + display: "flex", + alignItems: "center", + flexWrap: "wrap", + width: "100%", + height: "100%", + minHeight: 36, + boxSizing: "border-box", + padding: "3px 4px", + border: "1px solid", + borderRadius: 4, + borderColor: alpha(theme.palette.text.secondary, 0.15), + "&:last-child": { + marginRight: 0, + }, + }, + ".item": { + color: theme.palette.text.primary, + border: "1px solid", + borderColor: alpha(theme.palette.text.secondary, 0.2), + borderRadius: "2px", + padding: "1px 1px", + margin: "2px 0", + marginRight: 8, + }, +})); + +interface Props { + value: string[]; + onChange: (value: string[]) => void; +} + +export const HotkeyInput = (props: Props) => { + const { value, onChange } = props; + + const changeRef = useRef([]); + const [keys, setKeys] = useState(value); + + return ( + + + { + const ret = changeRef.current.slice(); + if (ret.length) { + onChange(ret); + changeRef.current = []; + } + }} + onKeyDown={(e) => { + const evt = e.nativeEvent; + e.preventDefault(); + e.stopPropagation(); + + const key = parseHotkey(evt.key); + if (key === "UNIDENTIFIED") return; + + changeRef.current = [...new Set([...changeRef.current, key])]; + setKeys(changeRef.current); + }} + /> + +
+ {keys.map((key) => ( +
+ {key} +
+ ))} +
+
+ + { + onChange([]); + setKeys([]); + }} + > + + +
+ ); +}; diff --git a/src/components/setting/mods/hotkey-viewer.tsx b/src/components/setting/mods/hotkey-viewer.tsx new file mode 100644 index 0000000..a47d30e --- /dev/null +++ b/src/components/setting/mods/hotkey-viewer.tsx @@ -0,0 +1,106 @@ +import { forwardRef, useImperativeHandle, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useLockFn } from "ahooks"; +import { styled, Typography } from "@mui/material"; +import { useVerge } from "@/hooks/use-verge"; +import { BaseDialog, DialogRef, Notice } from "@/components/base"; +import { HotkeyInput } from "./hotkey-input"; + +const ItemWrapper = styled("div")` + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +`; + +const HOTKEY_FUNC = [ + "open_dashboard", + "clash_mode_rule", + "clash_mode_global", + "clash_mode_direct", + "clash_mode_script", + "toggle_system_proxy", + "enable_system_proxy", + "disable_system_proxy", + "toggle_tun_mode", + "enable_tun_mode", + "disable_tun_mode", +]; + +export const HotkeyViewer = forwardRef((props, ref) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + + const { verge, patchVerge } = useVerge(); + + const [hotkeyMap, setHotkeyMap] = useState>({}); + + useImperativeHandle(ref, () => ({ + open: () => { + setOpen(true); + + const map = {} as typeof hotkeyMap; + + verge?.hotkeys?.forEach((text) => { + const [func, key] = text.split(",").map((e) => e.trim()); + + if (!func || !key) return; + + map[func] = key + .split("+") + .map((e) => e.trim()) + .map((k) => (k === "PLUS" ? "+" : k)); + }); + + setHotkeyMap(map); + }, + close: () => setOpen(false), + })); + + const onSave = useLockFn(async () => { + const hotkeys = Object.entries(hotkeyMap) + .map(([func, keys]) => { + if (!func || !keys?.length) return ""; + + const key = keys + .map((k) => k.trim()) + .filter(Boolean) + .map((k) => (k === "+" ? "PLUS" : k)) + .join("+"); + + if (!key) return ""; + return `${func},${key}`; + }) + .filter(Boolean); + + try { + await patchVerge({ hotkeys }); + setOpen(false); + } catch (err: any) { + Notice.error(err.message || err.toString()); + } + }); + + return ( + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + {HOTKEY_FUNC.map((func) => ( + + {t(func)} + setHotkeyMap((m) => ({ ...m, [func]: v }))} + /> + + ))} + + ); +}); diff --git a/src/components/setting/mods/layout-viewer.tsx b/src/components/setting/mods/layout-viewer.tsx new file mode 100644 index 0000000..baeca95 --- /dev/null +++ b/src/components/setting/mods/layout-viewer.tsx @@ -0,0 +1,80 @@ +import { forwardRef, useImperativeHandle, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { List, Switch } from "@mui/material"; +import { useVerge } from "@/hooks/use-verge"; +import { BaseDialog, DialogRef, Notice } from "@/components/base"; +import { SettingItem } from "./setting-comp"; +import { GuardState } from "./guard-state"; + +export const LayoutViewer = forwardRef((props, ref) => { + const { t } = useTranslation(); + const { verge, patchVerge, mutateVerge } = useVerge(); + + const [open, setOpen] = useState(false); + + useImperativeHandle(ref, () => ({ + open: () => setOpen(true), + close: () => setOpen(false), + })); + + const onSwitchFormat = (_e: any, value: boolean) => value; + const onError = (err: any) => { + Notice.error(err.message || err.toString()); + }; + const onChangeData = (patch: Partial) => { + mutateVerge({ ...verge, ...patch }, false); + }; + + return ( + setOpen(false)} + onCancel={() => setOpen(false)} + > + + + onChangeData({ theme_blur: e })} + onGuard={(e) => patchVerge({ theme_blur: e })} + > + + + + + + onChangeData({ traffic_graph: e })} + onGuard={(e) => patchVerge({ traffic_graph: e })} + > + + + + + + onChangeData({ enable_memory_usage: e })} + onGuard={(e) => patchVerge({ enable_memory_usage: e })} + > + + + + + + ); +}); diff --git a/src/components/setting/mods/misc-viewer.tsx b/src/components/setting/mods/misc-viewer.tsx new file mode 100644 index 0000000..7d63264 --- /dev/null +++ b/src/components/setting/mods/misc-viewer.tsx @@ -0,0 +1,199 @@ +import { forwardRef, useImperativeHandle, useState } from "react"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { + List, + ListItem, + ListItemText, + MenuItem, + Select, + Switch, + TextField, +} from "@mui/material"; +import { useVerge } from "@/hooks/use-verge"; +import { BaseDialog, DialogRef, Notice } from "@/components/base"; + +export const MiscViewer = forwardRef((props, ref) => { + const { t } = useTranslation(); + const { verge, patchVerge } = useVerge(); + + const [open, setOpen] = useState(false); + const [values, setValues] = useState({ + appLogLevel: "info", + autoCloseConnection: false, + enableClashFields: true, + enableBuiltinEnhanced: true, + proxyLayoutColumn: 6, + defaultLatencyTest: "", + autoLogClean: 0, + }); + + useImperativeHandle(ref, () => ({ + open: () => { + setOpen(true); + setValues({ + appLogLevel: verge?.app_log_level ?? "info", + autoCloseConnection: verge?.auto_close_connection ?? false, + enableClashFields: verge?.enable_clash_fields ?? true, + enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true, + proxyLayoutColumn: verge?.proxy_layout_column || 6, + defaultLatencyTest: verge?.default_latency_test || "", + autoLogClean: verge?.auto_log_clean || 0, + }); + }, + close: () => setOpen(false), + })); + + const onSave = useLockFn(async () => { + try { + await patchVerge({ + app_log_level: values.appLogLevel, + auto_close_connection: values.autoCloseConnection, + enable_clash_fields: values.enableClashFields, + enable_builtin_enhanced: values.enableBuiltinEnhanced, + proxy_layout_column: values.proxyLayoutColumn, + default_latency_test: values.defaultLatencyTest, + auto_log_clean: values.autoLogClean as any, + }); + setOpen(false); + } catch (err: any) { + Notice.error(err.message || err.toString()); + } + }); + + return ( + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + + + + + + + + + + setValues((v) => ({ ...v, autoCloseConnection: c })) + } + /> + + + + + + setValues((v) => ({ ...v, enableClashFields: c })) + } + /> + + + + + + setValues((v) => ({ ...v, enableBuiltinEnhanced: c })) + } + /> + + + + + + + + + + + + + + + + setValues((v) => ({ ...v, defaultLatencyTest: e.target.value })) + } + /> + + + + ); +}); diff --git a/src/components/setting/mods/service-viewer.tsx b/src/components/setting/mods/service-viewer.tsx new file mode 100644 index 0000000..839c8c2 --- /dev/null +++ b/src/components/setting/mods/service-viewer.tsx @@ -0,0 +1,123 @@ +import useSWR from "swr"; +import { forwardRef, useImperativeHandle, useState } from "react"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { Button, Stack, Typography } from "@mui/material"; +import { + checkService, + installService, + uninstallService, + patchVergeConfig, +} from "@/services/cmds"; +import { BaseDialog, DialogRef, Notice } from "@/components/base"; + +interface Props { + enable: boolean; +} + +export const ServiceViewer = forwardRef((props, ref) => { + const { enable } = props; + + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + + const { data: status, mutate: mutateCheck } = useSWR( + "checkService", + checkService, + { + revalidateIfStale: false, + shouldRetryOnError: false, + focusThrottleInterval: 36e5, // 1 hour + } + ); + + useImperativeHandle(ref, () => ({ + open: () => setOpen(true), + close: () => setOpen(false), + })); + + const state = status != null ? status : "pending"; + + const onInstall = useLockFn(async () => { + try { + await installService(); + mutateCheck(); + setOpen(false); + Notice.success("Service installed successfully"); + } catch (err: any) { + mutateCheck(); + Notice.error(err.message || err.toString()); + } + }); + + const onUninstall = useLockFn(async () => { + try { + if (enable) { + await patchVergeConfig({ enable_service_mode: false }); + } + + await uninstallService(); + mutateCheck(); + setOpen(false); + Notice.success("Service uninstalled successfully"); + } catch (err: any) { + mutateCheck(); + Notice.error(err.message || err.toString()); + } + }); + + // fix unhandled error of the service mode + const onDisable = useLockFn(async () => { + try { + await patchVergeConfig({ enable_service_mode: false }); + mutateCheck(); + setOpen(false); + } catch (err: any) { + mutateCheck(); + Notice.error(err.message || err.toString()); + } + }); + + return ( + setOpen(false)} + > + Current State: {state} + + {(state === "unknown" || state === "uninstall") && ( + + Information: Please make sure that the Clash Verge Service is + installed and enabled + + )} + + + {state === "uninstall" && enable && ( + + )} + + {state === "uninstall" && ( + + )} + + {(state === "active" || state === "installed") && ( + + )} + + + ); +}); diff --git a/src/components/setting/mods/setting-comp.tsx b/src/components/setting/mods/setting-comp.tsx new file mode 100644 index 0000000..e0cd181 --- /dev/null +++ b/src/components/setting/mods/setting-comp.tsx @@ -0,0 +1,48 @@ +import React, { ReactNode } from "react"; +import { + Box, + List, + ListItem, + ListItemText, + ListSubheader, +} from "@mui/material"; + +interface ItemProps { + label: ReactNode; + extra?: ReactNode; + children?: ReactNode; + secondary?: ReactNode; +} + +export const SettingItem: React.FC = (props) => { + const { label, extra, children, secondary } = props; + + const primary = !extra ? ( + label + ) : ( + + {label} + {extra} + + ); + + return ( + + + {children} + + ); +}; + +export const SettingList: React.FC<{ + title: string; + children: ReactNode; +}> = (props) => ( + + + {props.title} + + + {props.children} + +); diff --git a/src/components/setting/mods/sysproxy-viewer.tsx b/src/components/setting/mods/sysproxy-viewer.tsx new file mode 100644 index 0000000..44682dc --- /dev/null +++ b/src/components/setting/mods/sysproxy-viewer.tsx @@ -0,0 +1,173 @@ +import { forwardRef, useImperativeHandle, useState } from "react"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { + Box, + InputAdornment, + List, + ListItem, + ListItemText, + styled, + Switch, + TextField, + Typography, +} from "@mui/material"; +import { useVerge } from "@/hooks/use-verge"; +import { getSystemProxy } from "@/services/cmds"; +import { BaseDialog, DialogRef, Notice } from "@/components/base"; + +export const SysproxyViewer = forwardRef((props, ref) => { + const { t } = useTranslation(); + + const [open, setOpen] = useState(false); + + const { verge, patchVerge } = useVerge(); + + type SysProxy = Awaited>; + const [sysproxy, setSysproxy] = useState(); + + const { + enable_system_proxy: enabled, + enable_proxy_guard, + system_proxy_bypass, + proxy_guard_duration, + } = verge ?? {}; + + const [value, setValue] = useState({ + guard: enable_proxy_guard, + bypass: system_proxy_bypass, + duration: proxy_guard_duration ?? 10, + }); + + useImperativeHandle(ref, () => ({ + open: () => { + setOpen(true); + setValue({ + guard: enable_proxy_guard, + bypass: system_proxy_bypass, + duration: proxy_guard_duration ?? 10, + }); + getSystemProxy().then((p) => setSysproxy(p)); + }, + close: () => setOpen(false), + })); + + const onSave = useLockFn(async () => { + if (value.duration < 1) { + Notice.error("Proxy guard duration at least 1 seconds"); + return; + } + + const patch: Partial = {}; + + if (value.guard !== enable_proxy_guard) { + patch.enable_proxy_guard = value.guard; + } + if (value.duration !== proxy_guard_duration) { + patch.proxy_guard_duration = value.duration; + } + if (value.bypass !== system_proxy_bypass) { + patch.system_proxy_bypass = value.bypass; + } + + try { + await patchVerge(patch); + setOpen(false); + } catch (err: any) { + Notice.error(err.message || err.toString()); + } + }); + + return ( + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + + + + setValue((v) => ({ ...v, guard: e }))} + /> + + + + + s, + }} + onChange={(e) => { + setValue((v) => ({ + ...v, + duration: +e.target.value.replace(/\D/, ""), + })); + }} + /> + + + + + + setValue((v) => ({ ...v, bypass: e.target.value })) + } + /> + + + + + + {t("Current System Proxy")} + + + + Enable: + + {(!!sysproxy?.enable).toString()} + + + + + Server: + {sysproxy?.server || "-"} + + + + Bypass: + {sysproxy?.bypass || "-"} + + + + ); +}); + +const FlexBox = styled("div")` + display: flex; + margin-top: 4px; + + .label { + flex: none; + width: 80px; + } +`; diff --git a/src/components/setting/mods/theme-mode-switch.tsx b/src/components/setting/mods/theme-mode-switch.tsx new file mode 100644 index 0000000..29ae9ef --- /dev/null +++ b/src/components/setting/mods/theme-mode-switch.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from "react-i18next"; +import { Button, ButtonGroup } from "@mui/material"; + +type ThemeValue = IVergeConfig["theme_mode"]; + +interface Props { + value?: ThemeValue; + onChange?: (value: ThemeValue) => void; +} + +export const ThemeModeSwitch = (props: Props) => { + const { value, onChange } = props; + const { t } = useTranslation(); + + const modes = ["light", "dark", "system"] as const; + + return ( + + {modes.map((mode) => ( + + ))} + + ); +}; diff --git a/src/components/setting/mods/theme-viewer.tsx b/src/components/setting/mods/theme-viewer.tsx new file mode 100644 index 0000000..c68ada2 --- /dev/null +++ b/src/components/setting/mods/theme-viewer.tsx @@ -0,0 +1,136 @@ +import { forwardRef, useImperativeHandle, useState } from "react"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { + List, + ListItem, + ListItemText, + styled, + TextField, + useTheme, +} from "@mui/material"; +import { useVerge } from "@/hooks/use-verge"; +import { defaultTheme, defaultDarkTheme } from "@/pages/_theme"; +import { BaseDialog, DialogRef, Notice } from "@/components/base"; + +export const ThemeViewer = forwardRef((props, ref) => { + const { t } = useTranslation(); + + const [open, setOpen] = useState(false); + const { verge, patchVerge } = useVerge(); + const { theme_setting } = verge ?? {}; + const [theme, setTheme] = useState(theme_setting || {}); + + useImperativeHandle(ref, () => ({ + open: () => { + setOpen(true); + setTheme({ ...theme_setting } || {}); + }, + close: () => setOpen(false), + })); + + const textProps = { + size: "small", + autoComplete: "off", + sx: { width: 135 }, + } as const; + + const handleChange = (field: keyof typeof theme) => (e: any) => { + setTheme((t) => ({ ...t, [field]: e.target.value })); + }; + + const onSave = useLockFn(async () => { + try { + await patchVerge({ theme_setting: theme }); + setOpen(false); + } catch (err: any) { + Notice.error(err.message || err.toString()); + } + }); + + // default theme + const { palette } = useTheme(); + + const dt = palette.mode === "light" ? defaultTheme : defaultDarkTheme; + + type ThemeKey = keyof typeof theme & keyof typeof defaultTheme; + + const renderItem = (label: string, key: ThemeKey) => { + return ( + + + + e.key === "Enter" && onSave()} + /> + + ); + }; + + return ( + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onSave} + > + + {renderItem("Primary Color", "primary_color")} + + {renderItem("Secondary Color", "secondary_color")} + + {renderItem("Primary Text", "primary_text")} + + {renderItem("Secondary Text", "secondary_text")} + + {renderItem("Info Color", "info_color")} + + {renderItem("Error Color", "error_color")} + + {renderItem("Warning Color", "warning_color")} + + {renderItem("Success Color", "success_color")} + + + + e.key === "Enter" && onSave()} + /> + + + + + e.key === "Enter" && onSave()} + /> + + + + ); +}); + +const Item = styled(ListItem)(() => ({ + padding: "5px 2px", +})); + +const Round = styled("div")(() => ({ + width: "24px", + height: "24px", + borderRadius: "18px", + display: "inline-block", + marginRight: "8px", +})); diff --git a/src/components/setting/mods/update-viewer.tsx b/src/components/setting/mods/update-viewer.tsx new file mode 100644 index 0000000..8b4e68a --- /dev/null +++ b/src/components/setting/mods/update-viewer.tsx @@ -0,0 +1,70 @@ +import useSWR from "swr"; +import snarkdown from "snarkdown"; +import { forwardRef, useImperativeHandle, useState, useMemo } from "react"; +import { useLockFn } from "ahooks"; +import { Box, styled } from "@mui/material"; +import { useRecoilState } from "recoil"; +import { useTranslation } from "react-i18next"; +import { relaunch } from "@tauri-apps/api/process"; +import { checkUpdate, installUpdate } from "@tauri-apps/api/updater"; +import { BaseDialog, DialogRef, Notice } from "@/components/base"; +import { atomUpdateState } from "@/services/states"; + +const UpdateLog = styled(Box)(() => ({ + "h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" }, +})); + +export const UpdateViewer = forwardRef((props, ref) => { + const { t } = useTranslation(); + + const [open, setOpen] = useState(false); + const [updateState, setUpdateState] = useRecoilState(atomUpdateState); + + const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, { + errorRetryCount: 2, + revalidateIfStale: false, + focusThrottleInterval: 36e5, // 1 hour + }); + + useImperativeHandle(ref, () => ({ + open: () => setOpen(true), + close: () => setOpen(false), + })); + + // markdown parser + const parseContent = useMemo(() => { + if (!updateInfo?.manifest?.body) { + return "New Version is available"; + } + return snarkdown(updateInfo?.manifest?.body); + }, [updateInfo]); + + const onUpdate = useLockFn(async () => { + if (updateState) return; + setUpdateState(true); + + try { + await installUpdate(); + await relaunch(); + } catch (err: any) { + Notice.error(err?.message || err.toString()); + } finally { + setUpdateState(false); + } + }); + + return ( + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={onUpdate} + > + + + ); +}); diff --git a/src/components/setting/mods/web-ui-item.tsx b/src/components/setting/mods/web-ui-item.tsx new file mode 100644 index 0000000..5d3d84d --- /dev/null +++ b/src/components/setting/mods/web-ui-item.tsx @@ -0,0 +1,130 @@ +import { useState } from "react"; +import { + Divider, + IconButton, + Stack, + TextField, + Typography, +} from "@mui/material"; +import { + CheckRounded, + CloseRounded, + DeleteRounded, + EditRounded, + OpenInNewRounded, +} from "@mui/icons-material"; + +interface Props { + value?: string; + onlyEdit?: boolean; + onChange: (value?: string) => void; + onOpenUrl?: (value?: string) => void; + onDelete?: () => void; + onCancel?: () => void; +} + +export const WebUIItem = (props: Props) => { + const { + value, + onlyEdit = false, + onChange, + onDelete, + onOpenUrl, + onCancel, + } = props; + + const [editing, setEditing] = useState(false); + const [editValue, setEditValue] = useState(value); + + if (editing || onlyEdit) { + return ( + <> + + setEditValue(e.target.value)} + placeholder={`Support %host %port %secret`} + autoComplete="off" + /> + { + onChange(editValue); + setEditing(false); + }} + > + + + { + onCancel?.(); + setEditing(false); + }} + > + + + + + + ); + } + + const html = value + ?.replace("%host", "%host") + .replace("%port", "%port") + .replace("%secret", "%secret"); + + return ( + <> + + ({ + "> span": { + color: palette.primary.main, + }, + })} + dangerouslySetInnerHTML={{ __html: html || "NULL" }} + /> + onOpenUrl?.(value)} + > + + + { + setEditing(true); + setEditValue(value); + }} + > + + + + + + + + + ); +}; diff --git a/src/components/setting/mods/web-ui-viewer.tsx b/src/components/setting/mods/web-ui-viewer.tsx new file mode 100644 index 0000000..8588d3d --- /dev/null +++ b/src/components/setting/mods/web-ui-viewer.tsx @@ -0,0 +1,137 @@ +import { forwardRef, useImperativeHandle, useState } from "react"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { Button, Box, Typography } from "@mui/material"; +import { useVerge } from "@/hooks/use-verge"; +import { openWebUrl } from "@/services/cmds"; +import { BaseDialog, BaseEmpty, DialogRef, Notice } from "@/components/base"; +import { useClashInfo } from "@/hooks/use-clash"; +import { WebUIItem } from "./web-ui-item"; + +export const WebUIViewer = forwardRef((props, ref) => { + const { t } = useTranslation(); + + const { clashInfo } = useClashInfo(); + const { verge, patchVerge, mutateVerge } = useVerge(); + + const [open, setOpen] = useState(false); + const [editing, setEditing] = useState(false); + + useImperativeHandle(ref, () => ({ + open: () => setOpen(true), + close: () => setOpen(false), + })); + + const webUIList = verge?.web_ui_list || []; + + const handleAdd = useLockFn(async (value: string) => { + const newList = [value, ...webUIList]; + mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old), false); + await patchVerge({ web_ui_list: newList }); + }); + + const handleChange = useLockFn(async (index: number, value?: string) => { + const newList = [...webUIList]; + newList[index] = value ?? ""; + mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old), false); + await patchVerge({ web_ui_list: newList }); + }); + + const handleDelete = useLockFn(async (index: number) => { + const newList = [...webUIList]; + newList.splice(index, 1); + mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old), false); + await patchVerge({ web_ui_list: newList }); + }); + + const handleOpenUrl = useLockFn(async (value?: string) => { + if (!value) return; + try { + let url = value.trim().replaceAll("%host", "127.0.0.1"); + + if (url.includes("%port") || url.includes("%secret")) { + if (!clashInfo) throw new Error("failed to get clash info"); + if (!clashInfo.server?.includes(":")) { + throw new Error(`failed to parse the server "${clashInfo.server}"`); + } + + const port = clashInfo.server + .slice(clashInfo.server.indexOf(":") + 1) + .trim(); + + url = url.replaceAll("%port", port || "9090"); + url = url.replaceAll( + "%secret", + encodeURIComponent(clashInfo.secret || "") + ); + } + + await openWebUrl(url); + } catch (e: any) { + Notice.error(e.message || e.toString()); + } + }); + + return ( + + {t("Web UI")} + + + } + contentSx={{ + width: 450, + height: 300, + pb: 1, + overflowY: "auto", + userSelect: "text", + }} + cancelBtn={t("Back")} + disableOk + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + > + {editing && ( + { + setEditing(false); + handleAdd(v || ""); + }} + onCancel={() => setEditing(false)} + /> + )} + + {!editing && webUIList.length === 0 && ( + + Replace host, port, secret with "%host" "%port" "%secret" + + } + /> + )} + + {webUIList.map((item, index) => ( + handleChange(index, v)} + onDelete={() => handleDelete(index)} + onOpenUrl={handleOpenUrl} + /> + ))} + + ); +}); diff --git a/src/components/setting/setting-clash.tsx b/src/components/setting/setting-clash.tsx new file mode 100644 index 0000000..19f77ca --- /dev/null +++ b/src/components/setting/setting-clash.tsx @@ -0,0 +1,169 @@ +import { useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { + TextField, + Switch, + Select, + MenuItem, + Typography, + IconButton, +} from "@mui/material"; +import { ArrowForward, Settings } from "@mui/icons-material"; +import { DialogRef } from "@/components/base"; +import { useClash } from "@/hooks/use-clash"; +import { GuardState } from "./mods/guard-state"; +import { WebUIViewer } from "./mods/web-ui-viewer"; +import { ClashFieldViewer } from "./mods/clash-field-viewer"; +import { ClashPortViewer } from "./mods/clash-port-viewer"; +import { ControllerViewer } from "./mods/controller-viewer"; +import { SettingList, SettingItem } from "./mods/setting-comp"; +import { ClashCoreViewer } from "./mods/clash-core-viewer"; + +interface Props { + onError: (err: Error) => void; +} + +const SettingClash = ({ onError }: Props) => { + const { t } = useTranslation(); + + const { clash, version, mutateClash, patchClash } = useClash(); + + const { + ipv6, + "allow-lan": allowLan, + "log-level": logLevel, + "mixed-port": mixedPort, + } = clash ?? {}; + + const webRef = useRef(null); + const fieldRef = useRef(null); + const portRef = useRef(null); + const ctrlRef = useRef(null); + const coreRef = useRef(null); + + const onSwitchFormat = (_e: any, value: boolean) => value; + const onChangeData = (patch: Partial) => { + mutateClash((old) => ({ ...(old! || {}), ...patch }), false); + }; + + return ( + + + + + + + + + onChangeData({ "allow-lan": e })} + onGuard={(e) => patchClash({ "allow-lan": e })} + > + + + + + + onChangeData({ ipv6: e })} + onGuard={(e) => patchClash({ ipv6: e })} + > + + + + + + e.target.value} + onChange={(e) => onChangeData({ "log-level": e })} + onGuard={(e) => patchClash({ "log-level": e })} + > + + + + + + { + portRef.current?.open(); + (e.target as any).blur(); + }} + /> + + + + ctrlRef.current?.open()} + > + + + + + + webRef.current?.open()} + > + + + + + + fieldRef.current?.open()} + > + + + + + coreRef.current?.open()} + > + + + } + > + {version} + + + ); +}; + +export default SettingClash; diff --git a/src/components/setting/setting-system.tsx b/src/components/setting/setting-system.tsx new file mode 100644 index 0000000..7fdeb8f --- /dev/null +++ b/src/components/setting/setting-system.tsx @@ -0,0 +1,163 @@ +import useSWR from "swr"; +import { useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { IconButton, Switch } from "@mui/material"; +import { ArrowForward, PrivacyTipRounded, Settings } from "@mui/icons-material"; +import { checkService } from "@/services/cmds"; +import { useVerge } from "@/hooks/use-verge"; +import { DialogRef } from "@/components/base"; +import { SettingList, SettingItem } from "./mods/setting-comp"; +import { GuardState } from "./mods/guard-state"; +import { ServiceViewer } from "./mods/service-viewer"; +import { SysproxyViewer } from "./mods/sysproxy-viewer"; +import getSystem from "@/utils/get-system"; + +interface Props { + onError?: (err: Error) => void; +} + +const isWIN = getSystem() === "windows"; + +const SettingSystem = ({ onError }: Props) => { + const { t } = useTranslation(); + + const { verge, mutateVerge, patchVerge } = useVerge(); + + // service mode + const { data: serviceStatus } = useSWR( + isWIN ? "checkService" : null, + checkService, + { + revalidateIfStale: false, + shouldRetryOnError: false, + focusThrottleInterval: 36e5, // 1 hour + } + ); + + const serviceRef = useRef(null); + const sysproxyRef = useRef(null); + + const { + enable_tun_mode, + enable_auto_launch, + enable_service_mode, + enable_silent_start, + enable_system_proxy, + } = verge ?? {}; + + const onSwitchFormat = (_e: any, value: boolean) => value; + const onChangeData = (patch: Partial) => { + mutateVerge({ ...verge, ...patch }, false); + }; + + return ( + + + {isWIN && ( + + )} + + + onChangeData({ enable_tun_mode: e })} + onGuard={(e) => patchVerge({ enable_tun_mode: e })} + > + + + + + {isWIN && ( + serviceRef.current?.open()} + > + + + } + > + onChangeData({ enable_service_mode: e })} + onGuard={(e) => patchVerge({ enable_service_mode: e })} + > + + + + )} + + sysproxyRef.current?.open()} + > + + + } + > + onChangeData({ enable_system_proxy: e })} + onGuard={(e) => patchVerge({ enable_system_proxy: e })} + > + + + + + + onChangeData({ enable_auto_launch: e })} + onGuard={(e) => patchVerge({ enable_auto_launch: e })} + > + + + + + + onChangeData({ enable_silent_start: e })} + onGuard={(e) => patchVerge({ enable_silent_start: e })} + > + + + + + ); +}; + +export default SettingSystem; diff --git a/src/components/setting/setting-verge.tsx b/src/components/setting/setting-verge.tsx new file mode 100644 index 0000000..d54094d --- /dev/null +++ b/src/components/setting/setting-verge.tsx @@ -0,0 +1,202 @@ +import { useRef } from "react"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { IconButton, MenuItem, Select, Typography } from "@mui/material"; +import { openAppDir, openCoreDir, openLogsDir } from "@/services/cmds"; +import { ArrowForward } from "@mui/icons-material"; +import { checkUpdate } from "@tauri-apps/api/updater"; +import { useVerge } from "@/hooks/use-verge"; +import { version } from "@root/package.json"; +import { DialogRef, Notice } from "@/components/base"; +import { SettingList, SettingItem } from "./mods/setting-comp"; +import { ThemeModeSwitch } from "./mods/theme-mode-switch"; +import { ConfigViewer } from "./mods/config-viewer"; +import { HotkeyViewer } from "./mods/hotkey-viewer"; +import { MiscViewer } from "./mods/misc-viewer"; +import { ThemeViewer } from "./mods/theme-viewer"; +import { GuardState } from "./mods/guard-state"; +import { LayoutViewer } from "./mods/layout-viewer"; +import { UpdateViewer } from "./mods/update-viewer"; +import getSystem from "@/utils/get-system"; + +interface Props { + onError?: (err: Error) => void; +} + +const OS = getSystem(); + +const SettingVerge = ({ onError }: Props) => { + const { t } = useTranslation(); + + const { verge, patchVerge, mutateVerge } = useVerge(); + const { theme_mode, language } = verge ?? {}; + + const configRef = useRef(null); + const hotkeyRef = useRef(null); + const miscRef = useRef(null); + const themeRef = useRef(null); + const layoutRef = useRef(null); + const updateRef = useRef(null); + + const onChangeData = (patch: Partial) => { + mutateVerge({ ...verge, ...patch }, false); + }; + + const onCheckUpdate = useLockFn(async () => { + try { + const info = await checkUpdate(); + if (!info?.shouldUpdate) { + Notice.success("No Updates Available"); + } else { + updateRef.current?.open(); + } + } catch (err: any) { + Notice.error(err.message || err.toString()); + } + }); + + return ( + + + + + + + + + + e.target.value} + onChange={(e) => onChangeData({ language: e })} + onGuard={(e) => patchVerge({ language: e })} + > + + + + + + onChangeData({ theme_mode: e })} + onGuard={(e) => patchVerge({ theme_mode: e })} + > + + + + + + themeRef.current?.open()} + > + + + + + + layoutRef.current?.open()} + > + + + + + + miscRef.current?.open()} + > + + + + + + hotkeyRef.current?.open()} + > + + + + + + configRef.current?.open()} + > + + + + + + + + + + + + + + + + + + + + + + + {!(OS === "windows" && WIN_PORTABLE) && ( + + + + + + )} + + + v{version} + + + ); +}; + +export default SettingVerge; diff --git a/src/hooks/use-clash.ts b/src/hooks/use-clash.ts new file mode 100644 index 0000000..9804c5f --- /dev/null +++ b/src/hooks/use-clash.ts @@ -0,0 +1,83 @@ +import useSWR, { mutate } from "swr"; +import { useLockFn } from "ahooks"; +import { + getAxios, + getClashConfig, + getVersion, + updateConfigs, +} from "@/services/api"; +import { getClashInfo, patchClashConfig } from "@/services/cmds"; + +export const useClash = () => { + const { data: clash, mutate: mutateClash } = useSWR( + "getClashConfig", + getClashConfig + ); + + const { data: versionData, mutate: mutateVersion } = useSWR( + "getVersion", + getVersion + ); + + const patchClash = useLockFn(async (patch: Partial) => { + await updateConfigs(patch); + await patchClashConfig(patch); + mutateClash(); + }); + + const version = versionData?.premium + ? `${versionData.version} Premium` + : versionData?.meta + ? `${versionData.version} Meta` + : versionData?.version || "-"; + + return { + clash, + version, + mutateClash, + mutateVersion, + patchClash, + }; +}; + +export const useClashInfo = () => { + const { data: clashInfo, mutate: mutateInfo } = useSWR( + "getClashInfo", + getClashInfo + ); + + const patchInfo = async ( + patch: Partial< + Pick + > + ) => { + const hasInfo = + patch["mixed-port"] != null || + patch["external-controller"] != null || + patch.secret != null; + + if (!hasInfo) return; + + if (patch["mixed-port"]) { + const port = patch["mixed-port"]; + if (port < 1000) { + throw new Error("The port should not < 1000"); + } + if (port > 65536) { + throw new Error("The port should not > 65536"); + } + } + + await patchClashConfig(patch); + mutateInfo(); + mutate("getClashConfig"); + // 刷新接口 + getAxios(true); + }; + + return { + clashInfo, + mutateInfo, + patchInfo, + }; +}; diff --git a/src/hooks/use-profiles.ts b/src/hooks/use-profiles.ts new file mode 100644 index 0000000..5faa3ca --- /dev/null +++ b/src/hooks/use-profiles.ts @@ -0,0 +1,74 @@ +import useSWR, { mutate } from "swr"; +import { + getProfiles, + patchProfile, + patchProfilesConfig, +} from "@/services/cmds"; +import { getProxies, updateProxy } from "@/services/api"; + +export const useProfiles = () => { + const { data: profiles, mutate: mutateProfiles } = useSWR( + "getProfiles", + getProfiles + ); + + const patchProfiles = async (value: Partial) => { + await patchProfilesConfig(value); + mutateProfiles(); + }; + + const patchCurrent = async (value: Partial) => { + if (profiles?.current) { + await patchProfile(profiles.current, value); + mutateProfiles(); + } + }; + + // 根据selected的节点选择 + const activateSelected = async () => { + const proxiesData = await getProxies(); + const profileData = await getProfiles(); + + if (!profileData || !proxiesData) return; + + const current = profileData.items?.find( + (e) => e && e.uid === profileData.current + ); + + if (!current) return; + + // init selected array + const { selected = [] } = current; + const selectedMap = Object.fromEntries( + selected.map((each) => [each.name!, each.now!]) + ); + + let hasChange = false; + + const newSelected: typeof selected = []; + const { global, groups } = proxiesData; + + [global, ...groups].forEach(({ type, name, now }) => { + if (!now || type !== "Selector") return; + if (selectedMap[name] != null && selectedMap[name] !== now) { + hasChange = true; + updateProxy(name, selectedMap[name]); + } + newSelected.push({ name, now: selectedMap[name] }); + }); + + if (hasChange) { + patchProfile(profileData.current!, { selected: newSelected }); + mutate("getProxies", getProxies()); + } + }; + + return { + profiles, + current: profiles?.items?.find((p) => p && p.uid === profiles.current), + activateSelected, + patchProfiles, + patchCurrent, + mutateProfiles, + }; +}; diff --git a/src/hooks/use-verge.ts b/src/hooks/use-verge.ts new file mode 100644 index 0000000..6eeebe8 --- /dev/null +++ b/src/hooks/use-verge.ts @@ -0,0 +1,20 @@ +import useSWR from "swr"; +import { getVergeConfig, patchVergeConfig } from "@/services/cmds"; + +export const useVerge = () => { + const { data: verge, mutate: mutateVerge } = useSWR( + "getVergeConfig", + getVergeConfig + ); + + const patchVerge = async (value: Partial) => { + await patchVergeConfig(value); + mutateVerge(); + }; + + return { + verge, + mutateVerge, + patchVerge, + }; +}; diff --git a/src/hooks/use-visibility.ts b/src/hooks/use-visibility.ts new file mode 100644 index 0000000..31d6d3d --- /dev/null +++ b/src/hooks/use-visibility.ts @@ -0,0 +1,27 @@ +import { useEffect, useState } from "react"; + +export const useVisibility = () => { + const [visible, setVisible] = useState(true); + + useEffect(() => { + const handleVisibilityChange = () => { + setVisible(document.visibilityState === "visible"); + }; + + const handleFocus = () => setVisible(true); + const handleClick = () => setVisible(true); + + handleVisibilityChange(); + document.addEventListener("focus", handleFocus); + document.addEventListener("pointerdown", handleClick); + document.addEventListener("visibilitychange", handleVisibilityChange); + + return () => { + document.removeEventListener("focus", handleFocus); + document.removeEventListener("pointerdown", handleClick); + document.removeEventListener("visibilitychange", handleVisibilityChange); + }; + }, []); + + return visible; +}; diff --git a/src/hooks/use-websocket.ts b/src/hooks/use-websocket.ts new file mode 100644 index 0000000..810c7b1 --- /dev/null +++ b/src/hooks/use-websocket.ts @@ -0,0 +1,53 @@ +import { useRef } from "react"; + +export type WsMsgFn = (event: MessageEvent) => void; + +export interface WsOptions { + errorCount?: number; // default is 5 + retryInterval?: number; // default is 2500 + onError?: () => void; +} + +export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => { + const wsRef = useRef(null); + const timerRef = useRef(null); + + const disconnect = () => { + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + + const connect = (url: string) => { + let errorCount = options?.errorCount ?? 5; + + if (!url) return; + + const connectHelper = () => { + disconnect(); + + const ws = new WebSocket(url); + wsRef.current = ws; + + ws.addEventListener("message", onMessage); + ws.addEventListener("error", () => { + errorCount -= 1; + + if (errorCount >= 0) { + timerRef.current = setTimeout(connectHelper, 2500); + } else { + disconnect(); + options?.onError?.(); + } + }); + }; + + connectHelper(); + }; + + return { connect, disconnect }; +}; diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..90966cb --- /dev/null +++ b/src/index.html @@ -0,0 +1,38 @@ + + + + + + + Clash Verge + + + +
+ + + diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..10c92ef --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,135 @@ +{ + "Label-Proxies": "Proxies", + "Label-Profiles": "Profiles", + "Label-Connections": "Connections", + "Label-Logs": "Logs", + "Label-Rules": "Rules", + "Label-Settings": "Settings", + + "Connections": "Connections", + "Logs": "Logs", + "Clear": "Clear", + "Proxies": "Proxies", + "Proxy Groups": "Proxy Groups", + "rule": "rule", + "global": "global", + "direct": "direct", + "script": "script", + + "Profiles": "Profiles", + "Profile URL": "Profile URL", + "Import": "Import", + "New": "New", + "Create Profile": "Create Profile", + "Choose File": "Choose File", + "Close All": "Close All", + "Select": "Select", + "Edit Info": "Edit Info", + "Edit File": "Edit File", + "Open File": "Open File", + "Update": "Update", + "Update(Proxy)": "Update(Proxy)", + "Delete": "Delete", + "Enable": "Enable", + "Disable": "Disable", + "Refresh": "Refresh", + "To Top": "To Top", + "To End": "To End", + "Update All Profiles": "Update All Profiles", + "View Runtime Config": "View Runtime Config", + "Reactivate Profiles": "Reactivate Profiles", + + "Location": "Location", + "Delay check": "Delay check", + "Sort by default": "Sort by default", + "Sort by delay": "Sort by delay", + "Sort by name": "Sort by name", + "Delay check URL": "Delay check URL", + "Proxy detail": "Proxy detail", + "Filter": "Filter", + "Filter conditions": "Filter conditions", + "Refresh profiles": "Refresh profiles", + + "Type": "Type", + "Name": "Name", + "Descriptions": "Descriptions", + "Subscription URL": "Subscription URL", + "Update Interval": "Update Interval", + "Use System Proxy": "Use System Proxy", + "Use Clash Proxy": "Use Clash Proxy", + + "Settings": "Settings", + "Clash Setting": "Clash Setting", + "System Setting": "System Setting", + "Verge Setting": "Verge Setting", + "Allow Lan": "Allow Lan", + "IPv6": "IPv6", + "Log Level": "Log Level", + "Mixed Port": "Mixed Port", + "External": "External", + "Clash Core": "Clash Core", + "Tun Mode": "Tun Mode", + "Service Mode": "Service Mode", + "Auto Launch": "Auto Launch", + "Silent Start": "Silent Start", + "System Proxy": "System Proxy", + "System Proxy Setting": "System Proxy Setting", + "Proxy Guard": "Proxy Guard", + "Guard Duration": "Guard Duration", + "Proxy Bypass": "Proxy Bypass", + "Current System Proxy": "Current System Proxy", + "Theme Mode": "Theme Mode", + "Theme Blur": "Theme Blur", + "Theme Setting": "Theme Setting", + "Layout Setting": "Layout Setting", + "Miscellaneous": "Miscellaneous", + "Hotkey Setting": "Hotkey Setting", + "Traffic Graph": "Traffic Graph", + "Memory Usage": "Memory Usage", + "Language": "Language", + "Open App Dir": "Open App Dir", + "Open Core Dir": "Open Core Dir", + "Open Logs Dir": "Open Logs Dir", + "Check for Updates": "Check for Updates", + "Verge Version": "Verge Version", + "theme.light": "Light", + "theme.dark": "Dark", + "theme.system": "System", + "Clash Field": "Clash Field", + "Runtime Config": "Runtime Config", + "ReadOnly": "ReadOnly", + "Restart": "Restart", + + "Back": "Back", + "Save": "Save", + "Cancel": "Cancel", + + "Default": "Default", + "Download Speed": "Download Speed", + "Upload Speed": "Upload Speed", + + "open_dashboard": "Open Dashboard", + "clash_mode_rule": "Rule Mode", + "clash_mode_global": "Global Mode", + "clash_mode_direct": "Direct Mode", + "clash_mode_script": "Script Mode", + "toggle_system_proxy": "Toggle System Proxy", + "enable_system_proxy": "Enable System Proxy", + "disable_system_proxy": "Disable System Proxy", + "toggle_tun_mode": "Toggle Tun Mode", + "enable_tun_mode": "Enable Tun Mode", + "disable_tun_mode": "Disable Tun Mode", + + "App Log Level": "App Log Level", + "Auto Close Connections": "Auto Close Connections", + "Enable Clash Fields Filter": "Enable Clash Fields Filter", + "Enable Builtin Enhanced": "Enable Builtin Enhanced", + "Proxy Layout Column": "Proxy Layout Column", + "Default Latency Test": "Default Latency Test", + + "Auto Log Clean": "Auto Log Clean", + "Never Clean": "Never Clean", + "Retain 7 Days": "Retain 7 Days", + "Retain 30 Days": "Retain 30 Days", + "Retain 90 Days": "Retain 90 Days" +} diff --git a/src/locales/ru.json b/src/locales/ru.json new file mode 100644 index 0000000..f8ea10b --- /dev/null +++ b/src/locales/ru.json @@ -0,0 +1,111 @@ +{ + "Label-Proxies": "Прокси", + "Label-Profiles": "Профили", + "Label-Connections": "Соединения", + "Label-Logs": "Логи", + "Label-Rules": "Правила", + "Label-Settings": "Настройки", + + "Connections": "Соединения", + "Logs": "Логи", + "Clear": "Очистить", + "Proxies": "Прокси", + "Proxy Groups": "Группы прокси", + "rule": "правила", + "global": "глобальный", + "direct": "прямой", + "script": "скриптовый", + + "Profiles": "Профили", + "Profile URL": "URL профиля", + "Import": "Импорт", + "New": "Новый", + "Create Profile": "Создать профиль", + "Choose File": "Выбрать файл", + "Close All": "Закрыть всё", + "Select": "Выбрать", + "Edit Info": "Изменить информацию", + "Edit File": "Изменить файл", + "Open File": "Открыть файл", + "Update": "Обновить", + "Update(Proxy)": "Обновить (прокси)", + "Delete": "Удалить", + "Enable": "Включить", + "Disable": "Отключить", + "Refresh": "Обновить", + "To Top": "Наверх", + "To End": "Вниз", + "Update All Profiles": "Обновить все профили", + "View Runtime Config": "Просмотреть используемый конфиг", + "Reactivate Profiles": "Реактивировать профили", + + "Location": "Местоположение", + "Delay check": "Проверка задержки", + "Sort by default": "Сортировать по умолчанию", + "Sort by delay": "Сортировать по задержке", + "Sort by name": "Сортировать по названию", + "Delay check URL": "URL проверки задержки", + "Proxy detail": "Подробности о прокси", + "Filter": "Фильтр", + "Filter conditions": "Условия фильтрации", + "Refresh profiles": "Обновить профили", + + "Type": "Тип", + "Name": "Название", + "Descriptions": "Описания", + "Subscription URL": "URL подписки", + "Update Interval": "Интервал обновления", + + "Settings": "Настройки", + "Clash Setting": "Настройки Clash", + "System Setting": "Настройки системы", + "Verge Setting": "Настройки Verge", + "Allow Lan": "Разрешить локальную сеть", + "IPv6": "IPv6", + "Log Level": "Уровень логов", + "Mixed Port": "Смешанный порт", + "Clash Core": "Ядро Clash", + "Tun Mode": "Режим туннеля", + "Service Mode": "Режим сервиса", + "Auto Launch": "Автозапуск", + "Silent Start": "Тихий запуск", + "System Proxy": "Системный прокси", + "System Proxy Setting": "Настройка системного прокси", + "Proxy Guard": "Защита прокси", + "Guard Duration": "Период защиты", + "Proxy Bypass": "Игнорирование прокси", + "Current System Proxy": "Текущий системный прокси", + "Theme Mode": "Режим темы", + "Theme Blur": "Размытие темы", + "Theme Setting": "Настройка темы", + "Hotkey Setting": "Настройка клавиатурных сокращений", + "Traffic Graph": "График трафика", + "Language": "Язык", + "Open App Dir": "Открыть папку приложения", + "Open Core Dir": "Открыть папку ядра", + "Open Logs Dir": "Открыть папку логов", + "Verge Version": "Версия Verge", + "theme.light": "Светлая", + "theme.dark": "Тёмная", + "theme.system": "Системная", + "Clash Field": "Используемые настройки Clash", + "Runtime Config": "Используемый конфиг", + "ReadOnly": "Только для чтения", + "Restart": "Перезапуск", + + "Back": "Назад", + "Save": "Сохранить", + "Cancel": "Отмена", + + "open_dashboard": "Open Dashboard", + "clash_mode_rule": "Режим правил", + "clash_mode_global": "Глобальный режим", + "clash_mode_direct": "Прямой режим", + "clash_mode_script": "Скриптовый режим", + "toggle_system_proxy": "Переключить режим системного прокси", + "enable_system_proxy": "Включить системный прокси", + "disable_system_proxy": "Отключить системный прокси", + "toggle_tun_mode": "Переключить режим туннеля", + "enable_tun_mode": "Включить режим туннеля", + "disable_tun_mode": "Отключить режим туннеля" +} diff --git a/src/locales/zh.json b/src/locales/zh.json new file mode 100644 index 0000000..27b358c --- /dev/null +++ b/src/locales/zh.json @@ -0,0 +1,135 @@ +{ + "Label-Proxies": "代 理", + "Label-Profiles": "配 置", + "Label-Connections": "连 接", + "Label-Logs": "日 志", + "Label-Rules": "规 则", + "Label-Settings": "设 置", + + "Connections": "连接", + "Logs": "日志", + "Clear": "清除", + "Proxies": "代理", + "Proxy Groups": "代理组", + "rule": "规则", + "global": "全局", + "direct": "直连", + "script": "脚本", + + "Profiles": "配置", + "Profile URL": "配置文件链接", + "Import": "导入", + "New": "新建", + "Create Profile": "新建配置", + "Choose File": "选择文件", + "Close All": "关闭全部", + "Select": "使用", + "Edit Info": "编辑信息", + "Edit File": "编辑文件", + "Open File": "打开文件", + "Update": "更新", + "Update(Proxy)": "更新(代理)", + "Delete": "删除", + "Enable": "启用", + "Disable": "禁用", + "Refresh": "刷新", + "To Top": "移到最前", + "To End": "移到末尾", + "Update All Profiles": "更新所有配置", + "View Runtime Config": "查看运行时配置", + "Reactivate Profiles": "重新激活配置", + + "Location": "当前节点", + "Delay check": "延迟测试", + "Sort by default": "默认排序", + "Sort by delay": "按延迟排序", + "Sort by name": "按名称排序", + "Delay check URL": "延迟测试链接", + "Proxy detail": "展示节点细节", + "Filter": "过滤节点", + "Filter conditions": "过滤条件", + "Refresh profiles": "刷新配置", + + "Type": "类型", + "Name": "名称", + "Descriptions": "描述", + "Subscription URL": "订阅链接", + "Update Interval": "更新间隔", + "Use System Proxy": "使用系统代理更新", + "Use Clash Proxy": "使用Clash代理更新", + + "Settings": "设置", + "Clash Setting": "Clash 设置", + "System Setting": "系统设置", + "Verge Setting": "Verge 设置", + "Allow Lan": "局域网连接", + "IPv6": "IPv6", + "Log Level": "日志等级", + "Mixed Port": "端口设置", + "External": "外部控制", + "Clash Core": "Clash 内核", + "Tun Mode": "Tun 模式", + "Service Mode": "服务模式", + "Auto Launch": "开机自启", + "Silent Start": "静默启动", + "System Proxy": "系统代理", + "System Proxy Setting": "系统代理设置", + "Proxy Guard": "系统代理守卫", + "Guard Duration": "代理守卫间隔", + "Proxy Bypass": "Proxy Bypass", + "Current System Proxy": "当前系统代理", + "Theme Mode": "主题模式", + "Theme Blur": "背景模糊", + "Theme Setting": "主题设置", + "Layout Setting": "界面设置", + "Miscellaneous": "杂项设置", + "Hotkey Setting": "热键设置", + "Traffic Graph": "流量图显", + "Memory Usage": "内存使用", + "Language": "语言设置", + "Open App Dir": "应用目录", + "Open Core Dir": "内核目录", + "Open Logs Dir": "日志目录", + "Check for Updates": "检查更新", + "Verge Version": "应用版本", + "theme.light": "浅色", + "theme.dark": "深色", + "theme.system": "系统", + "Clash Field": "Clash 字段", + "Runtime Config": "运行配置", + "ReadOnly": "只读", + "Restart": "重启内核", + + "Back": "返回", + "Save": "保存", + "Cancel": "取消", + + "Default": "默认", + "Download Speed": "下载速度", + "Upload Speed": "上传速度", + + "open_dashboard": "打开面板", + "clash_mode_rule": "规则模式", + "clash_mode_global": "全局模式", + "clash_mode_direct": "直连模式", + "clash_mode_script": "脚本模式", + "toggle_system_proxy": "切换系统代理", + "enable_system_proxy": "开启系统代理", + "disable_system_proxy": "关闭系统代理", + "toggle_tun_mode": "切换Tun模式", + "enable_tun_mode": "开启Tun模式", + "disable_tun_mode": "关闭Tun模式", + + "App Log Level": "App日志等级", + "Auto Close Connections": "自动关闭连接", + "Enable Clash Fields Filter": "开启Clash字段过滤", + "Enable Builtin Enhanced": "开启内建增强功能", + "Proxy Layout Column": "代理页布局列数", + "Default Latency Test": "默认测试链接", + + "Auto Log Clean": "自动清理日志", + "Never Clean": "不清理", + "Retain 7 Days": "保留7天", + "Retain 30 Days": "保留30天", + "Retain 90 Days": "保留90天" +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..8588844 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,37 @@ +/// +/// +import "./assets/styles/index.scss"; + +import { ResizeObserver } from "@juggle/resize-observer"; +if (!window.ResizeObserver) { + window.ResizeObserver = ResizeObserver; +} + +import React from "react"; +import { createRoot } from "react-dom/client"; +import { RecoilRoot } from "recoil"; +import { BrowserRouter } from "react-router-dom"; +import { BaseErrorBoundary } from "./components/base"; +import Layout from "./pages/_layout"; +import "./services/i18n"; + +const mainElementId = "root"; +const container = document.getElementById(mainElementId); + +if (!container) { + throw new Error( + `No container '${mainElementId}' found to render application` + ); +} + +createRoot(container).render( + + + + + + + + + +); diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx new file mode 100644 index 0000000..27ef9b0 --- /dev/null +++ b/src/pages/_layout.tsx @@ -0,0 +1,161 @@ +import dayjs from "dayjs"; +import i18next from "i18next"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { SWRConfig, mutate } from "swr"; +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { Route, Routes } from "react-router-dom"; +import { alpha, List, Paper, ThemeProvider } from "@mui/material"; +import { listen } from "@tauri-apps/api/event"; +import { appWindow } from "@tauri-apps/api/window"; +import { routers } from "./_routers"; +import { getAxios } from "@/services/api"; +import { useVerge } from "@/hooks/use-verge"; +import { ReactComponent as LogoSvg } from "@/assets/image/logo.svg"; +import { BaseErrorBoundary, Notice } from "@/components/base"; +import { LayoutItem } from "@/components/layout/layout-item"; +import { LayoutControl } from "@/components/layout/layout-control"; +import { LayoutTraffic } from "@/components/layout/layout-traffic"; +import { UpdateButton } from "@/components/layout/update-button"; +import { useCustomTheme } from "@/components/layout/use-custom-theme"; +import getSystem from "@/utils/get-system"; +import "dayjs/locale/ru"; +import "dayjs/locale/zh-cn"; + +dayjs.extend(relativeTime); + +const OS = getSystem(); + +const Layout = () => { + const { t } = useTranslation(); + + const { theme } = useCustomTheme(); + + const { verge } = useVerge(); + const { theme_blur, language } = verge || {}; + + useEffect(() => { + window.addEventListener("keydown", (e) => { + // macOS有cmd+w + if (e.key === "Escape" && OS !== "macos") { + appWindow.close(); + } + }); + + listen("verge://refresh-clash-config", async () => { + // the clash info may be updated + await getAxios(true); + mutate("getProxies"); + mutate("getVersion"); + mutate("getClashConfig"); + mutate("getProviders"); + }); + + // update the verge config + listen("verge://refresh-verge-config", () => mutate("getVergeConfig")); + + // 设置提示监听 + listen("verge://notice-message", ({ payload }) => { + const [status, msg] = payload as [string, string]; + switch (status) { + case "set_config::ok": + Notice.success("Refresh clash config"); + break; + case "set_config::error": + Notice.error(msg); + break; + default: + break; + } + }); + }, []); + + useEffect(() => { + if (language) { + dayjs.locale(language === "zh" ? "zh-cn" : language); + i18next.changeLanguage(language); + } + }, [language]); + + return ( + + + { + if (e.target?.dataset?.windrag) appWindow.startDragging(); + }} + onContextMenu={(e) => { + // only prevent it on Windows + const validList = ["input", "textarea"]; + const target = e.currentTarget; + if ( + OS === "windows" && + !( + validList.includes(target.tagName.toLowerCase()) || + target.isContentEditable + ) + ) { + e.preventDefault(); + } + }} + sx={[ + ({ palette }) => ({ + bgcolor: alpha(palette.background.paper, theme_blur ? 0.8 : 1), + }), + ]} + > +
+
+ + + {!(OS === "windows" && WIN_PORTABLE) && ( + + )} +
+ + + {routers.map((router) => ( + + {t(router.label)} + + ))} + + +
+ +
+
+ +
+ {OS === "windows" && ( +
+ +
+ )} + +
+ + {routers.map(({ label, link, ele: Ele }) => ( + + + + } + /> + ))} + +
+
+
+
+
+ ); +}; + +export default Layout; diff --git a/src/pages/_routers.tsx b/src/pages/_routers.tsx new file mode 100644 index 0000000..f53ed48 --- /dev/null +++ b/src/pages/_routers.tsx @@ -0,0 +1,39 @@ +import LogsPage from "./logs"; +import ProxiesPage from "./proxies"; +import ProfilesPage from "./profiles"; +import SettingsPage from "./settings"; +import ConnectionsPage from "./connections"; +import RulesPage from "./rules"; + +export const routers = [ + { + label: "Label-Proxies", + link: "/", + ele: ProxiesPage, + }, + { + label: "Label-Profiles", + link: "/profile", + ele: ProfilesPage, + }, + { + label: "Label-Connections", + link: "/connections", + ele: ConnectionsPage, + }, + { + label: "Label-Rules", + link: "/rules", + ele: RulesPage, + }, + { + label: "Label-Logs", + link: "/logs", + ele: LogsPage, + }, + { + label: "Label-Settings", + link: "/settings", + ele: SettingsPage, + }, +]; diff --git a/src/pages/_theme.tsx b/src/pages/_theme.tsx new file mode 100644 index 0000000..37ce63e --- /dev/null +++ b/src/pages/_theme.tsx @@ -0,0 +1,19 @@ +// default theme setting +export const defaultTheme = { + primary_color: "#5b5c9d", + secondary_color: "#9c27b0", + primary_text: "#637381", + secondary_text: "#909399", + info_color: "#0288d1", + error_color: "#d32f2f", + warning_color: "#ed6c02", + success_color: "#2e7d32", + font_family: `"Roboto", "Helvetica", "Arial", sans-serif`, +}; + +// dark mode +export const defaultDarkTheme = { + ...defaultTheme, + primary_text: "#757575", + secondary_text: "#637381", +}; diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx new file mode 100644 index 0000000..7c53f67 --- /dev/null +++ b/src/pages/connections.tsx @@ -0,0 +1,218 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { useLockFn } from "ahooks"; +import { + Box, + Button, + IconButton, + MenuItem, + Paper, + Select, + TextField, +} from "@mui/material"; +import { useRecoilState } from "recoil"; +import { Virtuoso } from "react-virtuoso"; +import { useTranslation } from "react-i18next"; +import { TableChartRounded, TableRowsRounded } from "@mui/icons-material"; +import { closeAllConnections } from "@/services/api"; +import { atomConnectionSetting } from "@/services/states"; +import { useClashInfo } from "@/hooks/use-clash"; +import { BaseEmpty, BasePage } from "@/components/base"; +import { useWebsocket } from "@/hooks/use-websocket"; +import { ConnectionItem } from "@/components/connection/connection-item"; +import { ConnectionTable } from "@/components/connection/connection-table"; +import { + ConnectionDetail, + ConnectionDetailRef, +} from "@/components/connection/connection-detail"; + +const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] }; + +type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]; + +const ConnectionsPage = () => { + const { t, i18n } = useTranslation(); + const { clashInfo } = useClashInfo(); + + const [filterText, setFilterText] = useState(""); + const [curOrderOpt, setOrderOpt] = useState("Default"); + const [connData, setConnData] = useState(initConn); + + const [setting, setSetting] = useRecoilState(atomConnectionSetting); + + const isTableLayout = setting.layout === "table"; + + const orderOpts: Record = { + Default: (list) => list, + "Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!), + "Download Speed": (list) => + list.sort((a, b) => b.curDownload! - a.curDownload!), + }; + + const filterConn = useMemo(() => { + const orderFunc = orderOpts[curOrderOpt]; + const connections = connData.connections.filter((conn) => + (conn.metadata.host || conn.metadata.destinationIP)?.includes(filterText) + ); + + if (orderFunc) return orderFunc(connections); + return connections; + }, [connData, filterText, curOrderOpt]); + + const { connect, disconnect } = useWebsocket( + (event) => { + // meta v1.15.0 出现data.connections为null的情况 + const data = JSON.parse(event.data) as IConnections; + // 尽量与前一次connections的展示顺序保持一致 + setConnData((old) => { + const oldConn = old.connections; + const maxLen = data.connections?.length; + + const connections: typeof oldConn = []; + + const rest = (data.connections || []).filter((each) => { + const index = oldConn.findIndex((o) => o.id === each.id); + + if (index >= 0 && index < maxLen) { + const old = oldConn[index]; + each.curUpload = each.upload - old.upload; + each.curDownload = each.download - old.download; + + connections[index] = each; + return false; + } + return true; + }); + + for (let i = 0; i < maxLen; ++i) { + if (!connections[i] && rest.length > 0) { + connections[i] = rest.shift()!; + connections[i].curUpload = 0; + connections[i].curDownload = 0; + } + } + + return { ...data, connections }; + }); + }, + { errorCount: 3, retryInterval: 1000 } + ); + + useEffect(() => { + if (!clashInfo) return; + + const { server = "", secret = "" } = clashInfo; + connect(`ws://${server}/connections?token=${encodeURIComponent(secret)}`); + + return () => { + disconnect(); + }; + }, [clashInfo]); + + const onCloseAll = useLockFn(closeAllConnections); + + const detailRef = useRef(null!); + + return ( + + + setSetting((o) => + o.layout === "list" + ? { ...o, layout: "table" } + : { ...o, layout: "list" } + ) + } + > + {isTableLayout ? ( + + ) : ( + + )} + + + + + } + > + + + {!isTableLayout && ( + + )} + + setFilterText(e.target.value)} + sx={{ input: { py: 0.65, px: 1.25 } }} + /> + + + + {filterConn.length === 0 ? ( + + ) : isTableLayout ? ( + detailRef.current?.open(detail)} + /> + ) : ( + ( + detailRef.current?.open(item)} + /> + )} + /> + )} + + + + + + ); +}; + +export default ConnectionsPage; diff --git a/src/pages/logs.tsx b/src/pages/logs.tsx new file mode 100644 index 0000000..a26b7eb --- /dev/null +++ b/src/pages/logs.tsx @@ -0,0 +1,129 @@ +import { useMemo, useState } from "react"; +import { useRecoilState } from "recoil"; +import { + Box, + Button, + IconButton, + MenuItem, + Paper, + Select, + TextField, +} from "@mui/material"; +import { Virtuoso } from "react-virtuoso"; +import { useTranslation } from "react-i18next"; +import { + PlayCircleOutlineRounded, + PauseCircleOutlineRounded, +} from "@mui/icons-material"; +import { atomEnableLog, atomLogData } from "@/services/states"; +import { BaseEmpty, BasePage } from "@/components/base"; +import LogItem from "@/components/log/log-item"; + +const LogPage = () => { + const { t } = useTranslation(); + const [logData, setLogData] = useRecoilState(atomLogData); + const [enableLog, setEnableLog] = useRecoilState(atomEnableLog); + + const [logState, setLogState] = useState("all"); + const [filterText, setFilterText] = useState(""); + + const filterLogs = useMemo(() => { + return logData.filter((data) => { + return ( + data.payload.includes(filterText) && + (logState === "all" ? true : data.type.includes(logState)) + ); + }); + }, [logData, logState, filterText]); + + return ( + + setEnableLog((e) => !e)} + > + {enableLog ? ( + + ) : ( + + )} + + + + + } + > + + + + + setFilterText(e.target.value)} + sx={{ input: { py: 0.65, px: 1.25 } }} + /> + + + + {filterLogs.length > 0 ? ( + } + followOutput={"smooth"} + /> + ) : ( + + )} + + + + ); +}; + +export default LogPage; diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx new file mode 100644 index 0000000..95a4fc8 --- /dev/null +++ b/src/pages/profiles.tsx @@ -0,0 +1,334 @@ +import useSWR, { mutate } from "swr"; +import { useMemo, useRef, useState } from "react"; +import { useLockFn } from "ahooks"; +import { useSetRecoilState } from "recoil"; +import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material"; +import { + ClearRounded, + ContentCopyRounded, + LocalFireDepartmentRounded, + RefreshRounded, + TextSnippetOutlined, +} from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; +import { + getProfiles, + importProfile, + enhanceProfiles, + getRuntimeLogs, + deleteProfile, + updateProfile, +} from "@/services/cmds"; +import { atomLoadingCache } from "@/services/states"; +import { closeAllConnections } from "@/services/api"; +import { BasePage, DialogRef, Notice } from "@/components/base"; +import { + ProfileViewer, + ProfileViewerRef, +} from "@/components/profile/profile-viewer"; +import { ProfileItem } from "@/components/profile/profile-item"; +import { ProfileMore } from "@/components/profile/profile-more"; +import { useProfiles } from "@/hooks/use-profiles"; +import { ConfigViewer } from "@/components/setting/mods/config-viewer"; +import { throttle } from "lodash-es"; + +const ProfilePage = () => { + const { t } = useTranslation(); + + const [url, setUrl] = useState(""); + const [disabled, setDisabled] = useState(false); + const [activating, setActivating] = useState(""); + + const { + profiles = {}, + activateSelected, + patchProfiles, + mutateProfiles, + } = useProfiles(); + + const { data: chainLogs = {}, mutate: mutateLogs } = useSWR( + "getRuntimeLogs", + getRuntimeLogs + ); + + const chain = profiles.chain || []; + const viewerRef = useRef(null); + const configRef = useRef(null); + + // distinguish type + const { regularItems, enhanceItems } = useMemo(() => { + const items = profiles.items || []; + const chain = profiles.chain || []; + + const type1 = ["local", "remote"]; + const type2 = ["merge", "script"]; + + const regularItems = items.filter((i) => i && type1.includes(i.type!)); + const restItems = items.filter((i) => i && type2.includes(i.type!)); + const restMap = Object.fromEntries(restItems.map((i) => [i.uid, i])); + const enhanceItems = chain + .map((i) => restMap[i]!) + .filter(Boolean) + .concat(restItems.filter((i) => !chain.includes(i.uid))); + + return { regularItems, enhanceItems }; + }, [profiles]); + + const onImport = async () => { + if (!url) return; + setUrl(""); + setDisabled(true); + + try { + await importProfile(url); + Notice.success("Successfully import profile."); + + getProfiles().then((newProfiles) => { + mutate("getProfiles", newProfiles); + + const remoteItem = newProfiles.items?.find((e) => e.type === "remote"); + if (!newProfiles.current && remoteItem) { + const current = remoteItem.uid; + patchProfiles({ current }); + mutateLogs(); + setTimeout(() => activateSelected(), 2000); + } + }); + } catch (err: any) { + Notice.error(err.message || err.toString()); + } finally { + setDisabled(false); + } + }; + + const onSelect = useLockFn(async (current: string, force: boolean) => { + if (!force && current === profiles.current) return; + // 避免大多数情况下loading态闪烁 + const reset = setTimeout(() => setActivating(current), 100); + try { + await patchProfiles({ current }); + mutateLogs(); + closeAllConnections(); + setTimeout(() => activateSelected(), 2000); + Notice.success("Refresh clash config", 1000); + } catch (err: any) { + Notice.error(err?.message || err.toString(), 4000); + } finally { + clearTimeout(reset); + setActivating(""); + } + }); + + const onEnhance = useLockFn(async () => { + try { + await enhanceProfiles(); + mutateLogs(); + Notice.success("Refresh clash config", 1000); + } catch (err: any) { + Notice.error(err.message || err.toString(), 3000); + } + }); + + const onEnable = useLockFn(async (uid: string) => { + if (chain.includes(uid)) return; + const newChain = [...chain, uid]; + await patchProfiles({ chain: newChain }); + mutateLogs(); + }); + + const onDisable = useLockFn(async (uid: string) => { + if (!chain.includes(uid)) return; + const newChain = chain.filter((i) => i !== uid); + await patchProfiles({ chain: newChain }); + mutateLogs(); + }); + + const onDelete = useLockFn(async (uid: string) => { + try { + await onDisable(uid); + await deleteProfile(uid); + mutateProfiles(); + mutateLogs(); + } catch (err: any) { + Notice.error(err?.message || err.toString()); + } + }); + + const onMoveTop = useLockFn(async (uid: string) => { + if (!chain.includes(uid)) return; + const newChain = [uid].concat(chain.filter((i) => i !== uid)); + await patchProfiles({ chain: newChain }); + mutateLogs(); + }); + + const onMoveEnd = useLockFn(async (uid: string) => { + if (!chain.includes(uid)) return; + const newChain = chain.filter((i) => i !== uid).concat([uid]); + await patchProfiles({ chain: newChain }); + mutateLogs(); + }); + + // 更新所有配置 + const setLoadingCache = useSetRecoilState(atomLoadingCache); + const onUpdateAll = useLockFn(async () => { + const throttleMutate = throttle(mutateProfiles, 2000, { + trailing: true, + }); + const updateOne = async (uid: string) => { + try { + await updateProfile(uid); + throttleMutate(); + } finally { + setLoadingCache((cache) => ({ ...cache, [uid]: false })); + } + }; + + return new Promise((resolve) => { + setLoadingCache((cache) => { + // 获取没有正在更新的配置 + const items = regularItems.filter( + (e) => e.type === "remote" && !cache[e.uid] + ); + const change = Object.fromEntries(items.map((e) => [e.uid, true])); + + Promise.allSettled(items.map((e) => updateOne(e.uid))).then(resolve); + return { ...cache, ...change }; + }); + }); + }); + + const onCopyLink = async () => { + const text = await navigator.clipboard.readText(); + if (text) setUrl(text); + }; + + return ( + + + + + + configRef.current?.open()} + > + + + + + + + + } + > + + setUrl(e.target.value)} + sx={{ input: { py: 0.65, px: 1.25 } }} + placeholder={t("Profile URL")} + InputProps={{ + sx: { pr: 1 }, + endAdornment: !url ? ( + + + + ) : ( + setUrl("")} + > + + + ), + }} + /> + + + + + + + {regularItems.map((item) => ( + + onSelect(item.uid, f)} + onEdit={() => viewerRef.current?.edit(item)} + /> + + ))} + + + + {enhanceItems.length > 0 && ( + + {enhanceItems.map((item) => ( + + onEnable(item.uid)} + onDisable={() => onDisable(item.uid)} + onDelete={() => onDelete(item.uid)} + onMoveTop={() => onMoveTop(item.uid)} + onMoveEnd={() => onMoveEnd(item.uid)} + onEdit={() => viewerRef.current?.edit(item)} + /> + + ))} + + )} + + mutateProfiles()} /> + + + ); +}; + +export default ProfilePage; diff --git a/src/pages/proxies.tsx b/src/pages/proxies.tsx new file mode 100644 index 0000000..39c777b --- /dev/null +++ b/src/pages/proxies.tsx @@ -0,0 +1,90 @@ +import useSWR from "swr"; +import { useEffect, useMemo } from "react"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { Box, Button, ButtonGroup, Paper } from "@mui/material"; +import { + closeAllConnections, + getClashConfig, + updateConfigs, +} from "@/services/api"; +import { patchClashConfig } from "@/services/cmds"; +import { useVerge } from "@/hooks/use-verge"; +import { BasePage } from "@/components/base"; +import { ProxyGroups } from "@/components/proxy/proxy-groups"; +import { ProviderButton } from "@/components/proxy/provider-button"; + +const ProxyPage = () => { + const { t } = useTranslation(); + + const { data: clashConfig, mutate: mutateClash } = useSWR( + "getClashConfig", + getClashConfig + ); + + const { verge } = useVerge(); + + const modeList = useMemo(() => { + if (verge?.clash_core === "clash-meta") { + return ["rule", "global", "direct"]; + } + return ["rule", "global", "direct", "script"]; + }, [verge?.clash_core]); + + const curMode = clashConfig?.mode?.toLowerCase(); + + const onChangeMode = useLockFn(async (mode: string) => { + // 断开连接 + if (mode !== curMode && verge?.auto_close_connection) { + closeAllConnections(); + } + await updateConfigs({ mode }); + await patchClashConfig({ mode }); + mutateClash(); + }); + + useEffect(() => { + if (curMode && !modeList.includes(curMode)) { + onChangeMode("rule"); + } + }, [curMode]); + + return ( + + + + + {modeList.map((mode) => ( + + ))} + + + } + > + + + + + ); +}; + +export default ProxyPage; diff --git a/src/pages/rules.tsx b/src/pages/rules.tsx new file mode 100644 index 0000000..86cbdfa --- /dev/null +++ b/src/pages/rules.tsx @@ -0,0 +1,65 @@ +import useSWR from "swr"; +import { useState, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Virtuoso } from "react-virtuoso"; +import { Box, Paper, TextField } from "@mui/material"; +import { getRules } from "@/services/api"; +import { BaseEmpty, BasePage } from "@/components/base"; +import RuleItem from "@/components/rule/rule-item"; + +const RulesPage = () => { + const { t } = useTranslation(); + const { data = [] } = useSWR("getRules", getRules); + + const [filterText, setFilterText] = useState(""); + + const rules = useMemo(() => { + return data.filter((each) => each.payload.includes(filterText)); + }, [data, filterText]); + + return ( + + + + setFilterText(e.target.value)} + sx={{ input: { py: 0.65, px: 1.25 } }} + /> + + + + {rules.length > 0 ? ( + ( + + )} + followOutput={"smooth"} + /> + ) : ( + + )} + + + + ); +}; + +export default RulesPage; diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx new file mode 100644 index 0000000..039513b --- /dev/null +++ b/src/pages/settings.tsx @@ -0,0 +1,51 @@ +import { IconButton, Paper } from "@mui/material"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { BasePage, Notice } from "@/components/base"; +import { GitHub } from "@mui/icons-material"; +import { openWebUrl } from "@/services/cmds"; +import SettingVerge from "@/components/setting/setting-verge"; +import SettingClash from "@/components/setting/setting-clash"; +import SettingSystem from "@/components/setting/setting-system"; + +const SettingPage = () => { + const { t } = useTranslation(); + + const onError = (err: any) => { + Notice.error(err?.message || err.toString()); + }; + + const toGithubRepo = useLockFn(() => { + return openWebUrl("https://github.com/zzzgydi/clash-verge"); + }); + + return ( + + + + } + > + + + + + + + + + + + + + ); +}; + +export default SettingPage; diff --git a/src/services/api.ts b/src/services/api.ts new file mode 100644 index 0000000..c9ded07 --- /dev/null +++ b/src/services/api.ts @@ -0,0 +1,193 @@ +import axios, { AxiosInstance } from "axios"; +import { getClashInfo } from "./cmds"; + +let axiosIns: AxiosInstance = null!; + +/// initialize some information +/// enable force update axiosIns +export const getAxios = async (force: boolean = false) => { + if (axiosIns && !force) return axiosIns; + + let server = ""; + let secret = ""; + + try { + const info = await getClashInfo(); + + if (info?.server) { + server = info.server; + + // compatible width `external-controller` + if (server.startsWith(":")) server = `127.0.0.1${server}`; + else if (/^\d+$/.test(server)) server = `127.0.0.1:${server}`; + } + if (info?.secret) secret = info?.secret; + } catch {} + + axiosIns = axios.create({ + baseURL: `http://${server}`, + headers: secret ? { Authorization: `Bearer ${secret}` } : {}, + timeout: 15000, + }); + axiosIns.interceptors.response.use((r) => r.data); + return axiosIns; +}; + +/// Get Version +export const getVersion = async () => { + const instance = await getAxios(); + return instance.get("/version") as Promise<{ + premium: boolean; + meta?: boolean; + version: string; + }>; +}; + +/// Get current base configs +export const getClashConfig = async () => { + const instance = await getAxios(); + return instance.get("/configs") as Promise; +}; + +/// Update current configs +export const updateConfigs = async (config: Partial) => { + const instance = await getAxios(); + return instance.patch("/configs", config); +}; + +/// Get current rules +export const getRules = async () => { + const instance = await getAxios(); + const response = await instance.get("/rules"); + return response?.rules as IRuleItem[]; +}; + +/// Get Proxy delay +export const getProxyDelay = async (name: string, url?: string) => { + const params = { + timeout: 10000, + url: url || "http://www.gstatic.com/generate_204", + }; + const instance = await getAxios(); + const result = await instance.get( + `/proxies/${encodeURIComponent(name)}/delay`, + { params } + ); + return result as any as { delay: number }; +}; + +/// Update the Proxy Choose +export const updateProxy = async (group: string, proxy: string) => { + const instance = await getAxios(); + return instance.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy }); +}; + +// get proxy +export const getProxiesInner = async () => { + const instance = await getAxios(); + const response = await instance.get("/proxies"); + return (response?.proxies || {}) as Record; +}; + +/// Get the Proxy information +export const getProxies = async () => { + const [proxyRecord, providerRecord] = await Promise.all([ + getProxiesInner(), + getProviders(), + ]); + + // provider name map + const providerMap = Object.fromEntries( + Object.entries(providerRecord).flatMap(([provider, item]) => + item.proxies.map((p) => [p.name, { ...p, provider }]) + ) + ); + + // compatible with proxy-providers + const generateItem = (name: string) => { + if (proxyRecord[name]) return proxyRecord[name]; + if (providerMap[name]) return providerMap[name]; + return { name, type: "unknown", udp: false, history: [] }; + }; + + const { GLOBAL: global, DIRECT: direct, REJECT: reject } = proxyRecord; + + let groups: IProxyGroupItem[] = []; + + if (global?.all) { + groups = global.all + .filter((name) => proxyRecord[name]?.all) + .map((name) => proxyRecord[name]) + .map((each) => ({ + ...each, + all: each.all!.map((item) => generateItem(item)), + })); + } else { + groups = Object.values(proxyRecord) + .filter((each) => each.name !== "GLOBAL" && each.all) + .map((each) => ({ + ...each, + all: each.all!.map((item) => generateItem(item)), + })) + .sort((a, b) => b.name.localeCompare(a.name)); + } + + const proxies = [direct, reject].concat( + Object.values(proxyRecord).filter( + (p) => !p.all?.length && p.name !== "DIRECT" && p.name !== "REJECT" + ) + ); + + const _global: IProxyGroupItem = { + ...global, + all: global?.all?.map((item) => generateItem(item)) || [], + }; + + return { global: _global, direct, groups, records: proxyRecord, proxies }; +}; + +// get proxy providers +export const getProviders = async () => { + const instance = await getAxios(); + const response = await instance.get("/providers/proxies"); + + const providers = (response.providers || {}) as Record; + + return Object.fromEntries( + Object.entries(providers).filter(([key, item]) => { + const type = item.vehicleType.toLowerCase(); + return type === "http" || type === "file"; + }) + ); +}; + +// proxy providers health check +export const providerHealthCheck = async (name: string) => { + const instance = await getAxios(); + return instance.get( + `/providers/proxies/${encodeURIComponent(name)}/healthcheck` + ); +}; + +export const providerUpdate = async (name: string) => { + const instance = await getAxios(); + return instance.put(`/providers/proxies/${encodeURIComponent(name)}`); +}; + +export const getConnections = async () => { + const instance = await getAxios(); + const result = await instance.get("/connections"); + return result as any as IConnections; +}; + +// Close specific connection +export const deleteConnection = async (id: string) => { + const instance = await getAxios(); + await instance.delete(`/connections/${encodeURIComponent(id)}`); +}; + +// Close all connections +export const closeAllConnections = async () => { + const instance = await getAxios(); + await instance.delete(`/connections`); +}; diff --git a/src/services/cmds.ts b/src/services/cmds.ts new file mode 100644 index 0000000..02ee72a --- /dev/null +++ b/src/services/cmds.ts @@ -0,0 +1,180 @@ +import dayjs from "dayjs"; +import { invoke } from "@tauri-apps/api/tauri"; +import { Notice } from "@/components/base"; + +export async function getClashLogs() { + const regex = /time="(.+?)"\s+level=(.+?)\s+msg="(.+?)"/; + const newRegex = /(.+?)\s+(.+?)\s+(.+)/; + const logs = await invoke("get_clash_logs"); + + return logs + .map((log) => { + const result = log.match(regex); + if (result) { + const [_, _time, type, payload] = result; + const time = dayjs(_time).format("MM-DD HH:mm:ss"); + return { time, type, payload }; + } + + const result2 = log.match(newRegex); + if (result2) { + const [_, time, type, payload] = result2; + return { time, type, payload }; + } + return null; + }) + .filter(Boolean) as ILogItem[]; +} + +export async function getProfiles() { + return invoke("get_profiles"); +} + +export async function enhanceProfiles() { + return invoke("enhance_profiles"); +} + +export async function patchProfilesConfig(profiles: IProfilesConfig) { + return invoke("patch_profiles_config", { profiles }); +} + +export async function createProfile( + item: Partial, + fileData?: string | null +) { + return invoke("create_profile", { item, fileData }); +} + +export async function viewProfile(index: string) { + return invoke("view_profile", { index }); +} + +export async function readProfileFile(index: string) { + return invoke("read_profile_file", { index }); +} + +export async function saveProfileFile(index: string, fileData: string) { + return invoke("save_profile_file", { index, fileData }); +} + +export async function importProfile(url: string) { + return invoke("import_profile", { + url, + option: { with_proxy: true }, + }); +} + +export async function updateProfile(index: string, option?: IProfileOption) { + return invoke("update_profile", { index, option }); +} + +export async function deleteProfile(index: string) { + return invoke("delete_profile", { index }); +} + +export async function patchProfile( + index: string, + profile: Partial +) { + return invoke("patch_profile", { index, profile }); +} + +export async function getClashInfo() { + return invoke("get_clash_info"); +} + +export async function getRuntimeConfig() { + return invoke("get_runtime_config"); +} + +export async function getRuntimeYaml() { + return invoke("get_runtime_yaml"); +} + +export async function getRuntimeExists() { + return invoke("get_runtime_exists"); +} + +export async function getRuntimeLogs() { + return invoke>("get_runtime_logs"); +} + +export async function patchClashConfig(payload: Partial) { + return invoke("patch_clash_config", { payload }); +} + +export async function getVergeConfig() { + return invoke("get_verge_config"); +} + +export async function patchVergeConfig(payload: IVergeConfig) { + return invoke("patch_verge_config", { payload }); +} + +export async function getSystemProxy() { + return invoke<{ + enable: boolean; + server: string; + bypass: string; + }>("get_sys_proxy"); +} + +export async function changeClashCore(clashCore: string) { + return invoke("change_clash_core", { clashCore }); +} + +export async function restartSidecar() { + return invoke("restart_sidecar"); +} + +export async function grantPermission(core: string) { + return invoke("grant_permission", { core }); +} + +export async function openAppDir() { + return invoke("open_app_dir").catch((err) => + Notice.error(err?.message || err.toString(), 1500) + ); +} + +export async function openCoreDir() { + return invoke("open_core_dir").catch((err) => + Notice.error(err?.message || err.toString(), 1500) + ); +} + +export async function openLogsDir() { + return invoke("open_logs_dir").catch((err) => + Notice.error(err?.message || err.toString(), 1500) + ); +} + +export async function openWebUrl(url: string) { + return invoke("open_web_url", { url }); +} + +export async function cmdGetProxyDelay(name: string, url?: string) { + name = encodeURIComponent(name); + return invoke<{ delay: number }>("clash_api_get_proxy_delay", { name, url }); +} + +/// service mode + +export async function checkService() { + try { + const result = await invoke("check_service"); + if (result?.code === 0) return "active"; + if (result?.code === 400) return "installed"; + return "unknown"; + } catch (err: any) { + return "uninstall"; + } +} + +export async function installService() { + return invoke("install_service"); +} + +export async function uninstallService() { + return invoke("uninstall_service"); +} diff --git a/src/services/delay.ts b/src/services/delay.ts new file mode 100644 index 0000000..9b18776 --- /dev/null +++ b/src/services/delay.ts @@ -0,0 +1,127 @@ +import { cmdGetProxyDelay } from "./cmds"; + +const hashKey = (name: string, group: string) => `${group ?? ""}::${name}`; + +class DelayManager { + private cache = new Map(); + private urlMap = new Map(); + + // 每个item的监听 + private listenerMap = new Map void>(); + + // 每个分组的监听 + private groupListenerMap = new Map void>(); + + setUrl(group: string, url: string) { + this.urlMap.set(group, url); + } + + getUrl(group: string) { + return this.urlMap.get(group); + } + + setListener(name: string, group: string, listener: (time: number) => void) { + const key = hashKey(name, group); + this.listenerMap.set(key, listener); + } + + removeListener(name: string, group: string) { + const key = hashKey(name, group); + this.listenerMap.delete(key); + } + + setGroupListener(group: string, listener: () => void) { + this.groupListenerMap.set(group, listener); + } + + removeGroupListener(group: string) { + this.groupListenerMap.delete(group); + } + + setDelay(name: string, group: string, delay: number) { + const key = hashKey(name, group); + this.cache.set(key, [Date.now(), delay]); + this.listenerMap.get(key)?.(delay); + this.groupListenerMap.get(group)?.(); + } + + getDelay(name: string, group: string) { + if (!name) return -1; + + const result = this.cache.get(hashKey(name, group)); + if (result && Date.now() - result[0] <= 18e5) { + return result[1]; + } + return -1; + } + + /// 暂时修复provider的节点延迟排序的问题 + getDelayFix(proxy: IProxyItem, group: string) { + if (!proxy.provider) { + const delay = this.getDelay(proxy.name, group); + if (delay >= 0 || delay === -2) return delay; + } + + if (proxy.history.length > 0) { + // 0ms以error显示 + return proxy.history[proxy.history.length - 1].delay || 1e6; + } + return -1; + } + + async checkDelay(name: string, group: string) { + let delay = -1; + + try { + const url = this.getUrl(group); + const result = await cmdGetProxyDelay(name, url); + delay = result.delay; + } catch { + delay = 1e6; // error + } + + this.setDelay(name, group, delay); + return delay; + } + + async checkListDelay(nameList: string[], group: string, concurrency = 36) { + const names = nameList.filter(Boolean); + // 设置正在延迟测试中 + names.forEach((name) => this.setDelay(name, group, -2)); + + let total = names.length; + let current = 0; + + return new Promise((resolve) => { + const help = async (): Promise => { + if (current >= concurrency) return; + const task = names.shift(); + if (!task) return; + current += 1; + await this.checkDelay(task, group); + current -= 1; + total -= 1; + if (total <= 0) resolve(null); + else return help(); + }; + for (let i = 0; i < concurrency; ++i) help(); + }); + } + + formatDelay(delay: number) { + if (delay < 0) return "-"; + if (delay > 1e5) return "Error"; + if (delay >= 10000) return "Timeout"; // 10s + return `${delay}`; + } + + formatDelayColor(delay: number) { + if (delay <= 0) return "text.secondary"; + if (delay >= 10000) return "error.main"; + if (delay > 500) return "warning.main"; + if (delay > 100) return "text.secondary"; + return "success.main"; + } +} + +export default new DelayManager(); diff --git a/src/services/i18n.ts b/src/services/i18n.ts new file mode 100644 index 0000000..8d4c14b --- /dev/null +++ b/src/services/i18n.ts @@ -0,0 +1,19 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import en from "@/locales/en.json"; +import ru from "@/locales/ru.json"; +import zh from "@/locales/zh.json"; + +const resources = { + en: { translation: en }, + ru: { translation: ru }, + zh: { translation: zh }, +}; + +i18n.use(initReactI18next).init({ + resources, + lng: "en", + interpolation: { + escapeValue: false, + }, +}); diff --git a/src/services/states.ts b/src/services/states.ts new file mode 100644 index 0000000..6ac6edc --- /dev/null +++ b/src/services/states.ts @@ -0,0 +1,73 @@ +import { atom } from "recoil"; + +export const atomThemeMode = atom<"light" | "dark">({ + key: "atomThemeMode", + default: "light", +}); + +export const atomLogData = atom({ + key: "atomLogData", + default: [], +}); + +export const atomEnableLog = atom({ + key: "atomEnableLog", + effects: [ + ({ setSelf, onSet }) => { + const key = "enable-log"; + + try { + setSelf(localStorage.getItem(key) !== "false"); + } catch {} + + onSet((newValue, _, isReset) => { + try { + if (isReset) { + localStorage.removeItem(key); + } else { + localStorage.setItem(key, newValue.toString()); + } + } catch {} + }); + }, + ], +}); + +interface IConnectionSetting { + layout: "table" | "list"; +} + +export const atomConnectionSetting = atom({ + key: "atomConnectionSetting", + effects: [ + ({ setSelf, onSet }) => { + const key = "connections-setting"; + + try { + const value = localStorage.getItem(key); + const data = value == null ? { layout: "table" } : JSON.parse(value); + setSelf(data); + } catch { + setSelf({ layout: "table" }); + } + + onSet((newValue) => { + try { + localStorage.setItem(key, JSON.stringify(newValue)); + } catch {} + }); + }, + ], +}); + +// save the state of each profile item loading +export const atomLoadingCache = atom>({ + key: "atomLoadingCache", + default: {}, +}); + +// save update state +export const atomUpdateState = atom({ + key: "atomUpdateState", + default: false, +}); diff --git a/src/services/types.d.ts b/src/services/types.d.ts new file mode 100644 index 0000000..3fe2175 --- /dev/null +++ b/src/services/types.d.ts @@ -0,0 +1,263 @@ +type Platform = + | "aix" + | "android" + | "darwin" + | "freebsd" + | "haiku" + | "linux" + | "openbsd" + | "sunos" + | "win32" + | "cygwin" + | "netbsd"; + +/** + * defines in `vite.config.ts` + */ +declare const WIN_PORTABLE: boolean; +declare const OS_PLATFORM: Platform; + +/** + * Some interface for clash api + */ +interface IConfigData { + port: number; + mode: string; + ipv6: boolean; + "socket-port": number; + "allow-lan": boolean; + "log-level": string; + "mixed-port": number; + "redir-port": number; + "socks-port": number; + "tproxy-port": number; + "external-controller": string; + secret: string; +} + +interface IRuleItem { + type: string; + payload: string; + proxy: string; +} + +interface IProxyItem { + name: string; + type: string; + udp: boolean; + history: { + time: string; + delay: number; + }[]; + all?: string[]; + now?: string; + provider?: string; // 记录是否来自provider +} + +type IProxyGroupItem = Omit & { + all: IProxyItem[]; +}; + +interface IProviderItem { + name: string; + type: string; + proxies: IProxyItem[]; + updatedAt: string; + vehicleType: string; +} + +interface ITrafficItem { + up: number; + down: number; +} + +interface ILogItem { + type: string; + time?: string; + payload: string; +} + +interface IConnectionsItem { + id: string; + metadata: { + network: string; + type: string; + host: string; + sourceIP: string; + sourcePort: string; + destinationPort: string; + destinationIP?: string; + process?: string; + processPath?: string; + }; + upload: number; + download: number; + start: string; + chains: string[]; + rule: string; + rulePayload: string; + curUpload?: number; // upload speed, calculate at runtime + curDownload?: number; // download speed, calculate at runtime +} + +interface IConnections { + downloadTotal: number; + uploadTotal: number; + connections: IConnectionsItem[]; +} + +/** + * Some interface for command + */ + +interface IClashInfo { + // status: string; + port?: number; // clash mixed port + server?: string; // external-controller + secret?: string; +} + +interface IProfileItem { + uid: string; + type?: "local" | "remote" | "merge" | "script"; + name?: string; + desc?: string; + file?: string; + url?: string; + updated?: number; + selected?: { + name?: string; + now?: string; + }[]; + extra?: { + upload: number; + download: number; + total: number; + expire: number; + }; + option?: IProfileOption; +} + +interface IProfileOption { + user_agent?: string; + with_proxy?: boolean; + self_proxy?: boolean; + update_interval?: number; +} + +interface IProfilesConfig { + current?: string; + chain?: string[]; + valid?: string[]; + items?: IProfileItem[]; +} + +interface IVergeConfig { + app_log_level?: "trace" | "debug" | "info" | "warn" | "error" | string; + language?: string; + clash_core?: string; + theme_mode?: "light" | "dark" | "system"; + theme_blur?: boolean; + traffic_graph?: boolean; + enable_memory_usage?: boolean; + enable_tun_mode?: boolean; + enable_auto_launch?: boolean; + enable_service_mode?: boolean; + enable_silent_start?: boolean; + enable_system_proxy?: boolean; + enable_proxy_guard?: boolean; + proxy_guard_duration?: number; + system_proxy_bypass?: string; + web_ui_list?: string[]; + hotkeys?: string[]; + theme_setting?: { + primary_color?: string; + secondary_color?: string; + primary_text?: string; + secondary_text?: string; + info_color?: string; + error_color?: string; + warning_color?: string; + success_color?: string; + font_family?: string; + css_injection?: string; + }; + auto_close_connection?: boolean; + default_latency_test?: string; + enable_clash_fields?: boolean; + enable_builtin_enhanced?: boolean; + auto_log_clean?: 0 | 1 | 2 | 3; + proxy_layout_column?: number; +} + +type IClashConfigValue = any; + +interface IProfileMerge { + // clash config fields (default supports) + rules?: IClashConfigValue; + proxies?: IClashConfigValue; + "proxy-groups"?: IClashConfigValue; + "proxy-providers"?: IClashConfigValue; + "rule-providers"?: IClashConfigValue; + // clash config fields (use flag) + tun?: IClashConfigValue; + dns?: IClashConfigValue; + hosts?: IClashConfigValue; + script?: IClashConfigValue; + profile?: IClashConfigValue; + payload?: IClashConfigValue; + "interface-name"?: IClashConfigValue; + "routing-mark"?: IClashConfigValue; + // functional fields + use?: string[]; + "prepend-rules"?: any[]; + "append-rules"?: any[]; + "prepend-proxies"?: any[]; + "append-proxies"?: any[]; + "prepend-proxy-groups"?: any[]; + "append-proxy-groups"?: any[]; + // fix + ebpf?: any; + experimental?: any; + iptables?: any; + sniffer?: any; + authentication?: any; + "bind-address"?: any; + "external-ui"?: any; + "auto-redir"?: any; + "socks-port"?: any; + "redir-port"?: any; + "tproxy-port"?: any; + "geodata-mode"?: any; + "tcp-concurrent"?: any; +} + +// partial of the clash config +type IProfileData = Partial<{ + rules: any[]; + proxies: any[]; + "proxy-groups": any[]; + "proxy-providers": any[]; + "rule-providers": any[]; + + [k: string]: any; +}>; + +interface IChainItem { + item: IProfileItem; + merge?: IProfileMerge; + script?: string; +} + +interface IEnhancedPayload { + chain: IChainItem[]; + valid: string[]; + current: IProfileData; + callback: string; +} + +interface IEnhancedResult { + data: IProfileData; + status: string; + error?: string; +} diff --git a/src/utils/clash-fields.ts b/src/utils/clash-fields.ts new file mode 100644 index 0000000..86e1b60 --- /dev/null +++ b/src/utils/clash-fields.ts @@ -0,0 +1,52 @@ +export const HANDLE_FIELDS = [ + "mode", + "port", + "socks-port", + "mixed-port", + "allow-lan", + "log-level", + "ipv6", + "secret", + "external-controller", +]; + +export const DEFAULT_FIELDS = [ + "proxies", + "proxy-groups", + "proxy-providers", + "rules", + "rule-providers", +] as const; + +export const OTHERS_FIELDS = [ + "dns", + "tun", + "ebpf", + "hosts", + "script", + "profile", + "payload", + "tunnels", + "auto-redir", + "experimental", + "interface-name", + "routing-mark", + "redir-port", + "tproxy-port", + "iptables", + "external-ui", + "bind-address", + "authentication", + "tls", // meta + "sniffer", // meta + "geox-url", // meta + "listeners", // meta + "sub-rules", // meta + "geodata-mode", // meta + "unified-delay", // meta + "tcp-concurrent", // meta + "enable-process", // meta + "find-process-mode", // meta + "external-controller-tls", // meta + "global-client-fingerprint", // meta +] as const; diff --git a/src/utils/get-system.ts b/src/utils/get-system.ts new file mode 100644 index 0000000..8917703 --- /dev/null +++ b/src/utils/get-system.ts @@ -0,0 +1,14 @@ +// get the system os +// according to UA +export default function getSystem() { + const ua = navigator.userAgent; + const platform = OS_PLATFORM; + + if (ua.includes("Mac OS X") || platform === "darwin") return "macos"; + + if (/win64|win32/i.test(ua) || platform === "win32") return "windows"; + + if (/linux/i.test(ua)) return "linux"; + + return "unknown"; +} diff --git a/src/utils/ignore-case.ts b/src/utils/ignore-case.ts new file mode 100644 index 0000000..f6533f6 --- /dev/null +++ b/src/utils/ignore-case.ts @@ -0,0 +1,14 @@ +// Deep copy and change all keys to lowercase +type TData = Record; + +export default function ignoreCase(data: TData): TData { + if (!data) return {}; + + const newData = {} as TData; + + Object.entries(data).forEach(([key, value]) => { + newData[key.toLowerCase()] = JSON.parse(JSON.stringify(value)); + }); + + return newData; +} diff --git a/src/utils/noop.ts b/src/utils/noop.ts new file mode 100644 index 0000000..ca6a744 --- /dev/null +++ b/src/utils/noop.ts @@ -0,0 +1 @@ +export default function noop() {} diff --git a/src/utils/parse-hotkey.ts b/src/utils/parse-hotkey.ts new file mode 100644 index 0000000..864ef4f --- /dev/null +++ b/src/utils/parse-hotkey.ts @@ -0,0 +1,48 @@ +const KEY_MAP: Record = { + '"': "'", + ":": ";", + "?": "/", + ">": ".", + "<": ",", + "{": "[", + "}": "]", + "|": "\\", + "!": "1", + "@": "2", + "#": "3", + $: "4", + "%": "5", + "^": "6", + "&": "7", + "*": "8", + "(": "9", + ")": "0", + "~": "`", +}; + +export const parseHotkey = (key: string) => { + let temp = key.toUpperCase(); + + if (temp.startsWith("ARROW")) { + temp = temp.slice(5); + } else if (temp.startsWith("DIGIT")) { + temp = temp.slice(5); + } else if (temp.startsWith("KEY")) { + temp = temp.slice(3); + } else if (temp.endsWith("LEFT")) { + temp = temp.slice(0, -4); + } else if (temp.endsWith("RIGHT")) { + temp = temp.slice(0, -5); + } + + switch (temp) { + case "CONTROL": + return "CTRL"; + case "META": + return "CMD"; + case " ": + return "SPACE"; + default: + return KEY_MAP[temp] || temp; + } +}; diff --git a/src/utils/parse-traffic.ts b/src/utils/parse-traffic.ts new file mode 100644 index 0000000..514d24f --- /dev/null +++ b/src/utils/parse-traffic.ts @@ -0,0 +1,14 @@ +const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + +const parseTraffic = (num?: number) => { + if (typeof num !== "number") return ["NaN", ""]; + if (num < 1000) return [`${Math.round(num)}`, "B"]; + const exp = Math.min(Math.floor(Math.log2(num) / 10), UNITS.length - 1); + const dat = num / Math.pow(1024, exp); + const ret = dat >= 1000 ? dat.toFixed(0) : dat.toPrecision(3); + const unit = UNITS[exp]; + + return [ret, unit]; +}; + +export default parseTraffic; diff --git a/src/utils/truncate-str.ts b/src/utils/truncate-str.ts new file mode 100644 index 0000000..491fa07 --- /dev/null +++ b/src/utils/truncate-str.ts @@ -0,0 +1,6 @@ +export const truncateStr = (str?: string, prefixLen = 16, maxLen = 56) => { + if (!str || str.length <= maxLen) return str; + return ( + str.slice(0, prefixLen) + " ... " + str.slice(-(maxLen - prefixLen - 5)) + ); +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a285c97 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "paths": { + "@/*": ["src/*"], + "@root/*": ["./*"] + } + }, + "include": ["./src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..0315d76 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from "vite"; +import path from "path"; +import svgr from "vite-plugin-svgr"; +import react from "@vitejs/plugin-react"; +import monaco from "vite-plugin-monaco-editor"; + +// https://vitejs.dev/config/ +export default defineConfig({ + root: "src", + server: { port: 3000 }, + plugins: [ + svgr(), + react(), + monaco({ languageWorkers: ["editorWorkerService", "typescript"] }), + ], + build: { + outDir: "../dist", + emptyOutDir: true, + }, + resolve: { + alias: { + "@": path.resolve("./src"), + "@root": path.resolve("."), + }, + }, + define: { + OS_PLATFORM: `"${process.platform}"`, + WIN_PORTABLE: !!process.env.VITE_WIN_PORTABLE, + }, +}); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..895c8bb --- /dev/null +++ b/yarn.lock @@ -0,0 +1,2348 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@actions/github@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.0.3.tgz#b305765d6173962d113451ea324ff675aa674f35" + integrity sha512-myjA/pdLQfhUGLtRZC/J4L1RXOG4o6aYdiEq+zr5wVVKljzbFld+xv10k1FX6IkIJtNxbAq44BdwSNpQ015P0A== + dependencies: + "@actions/http-client" "^2.0.1" + "@octokit/core" "^3.6.0" + "@octokit/plugin-paginate-rest" "^2.17.0" + "@octokit/plugin-rest-endpoint-methods" "^5.13.0" + +"@actions/http-client@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.0.1.tgz#873f4ca98fe32f6839462a6f046332677322f99c" + integrity sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw== + dependencies: + tunnel "^0.0.6" + +"@ampproject/remapping@^2.1.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34" + integrity sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg== + dependencies: + "@jridgewell/trace-mapping" "^0.3.0" + +"@babel/code-frame@^7.0.0": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + dependencies: + "@babel/highlight" "^7.16.7" + +"@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/compat-data@^7.18.8": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.13.tgz#6aff7b350a1e8c3e40b029e46cbe78e24a913483" + integrity sha512-5yUzC5LqyTFp2HLmDoxGQelcdYgSpP9xsnMWBphAscOdFrHSAVbLNzWiy32sVNDqJRDiJK6klfDnAgu6PAGSHw== + +"@babel/core@^7.18.10", "@babel/core@^7.18.5": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.13.tgz#9be8c44512751b05094a4d3ab05fc53a47ce00ac" + integrity sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.13" + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-module-transforms" "^7.18.9" + "@babel/helpers" "^7.18.9" + "@babel/parser" "^7.18.13" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.18.13" + "@babel/types" "^7.18.13" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/generator@^7.18.13": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.13.tgz#59550cbb9ae79b8def15587bdfbaa388c4abf212" + integrity sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ== + dependencies: + "@babel/types" "^7.18.13" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" + integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-compilation-targets@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz#69e64f57b524cde3e5ff6cc5a9f4a387ee5563bf" + integrity sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg== + dependencies: + "@babel/compat-data" "^7.18.8" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.20.2" + semver "^6.3.0" + +"@babel/helper-environment-visitor@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" + integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== + +"@babel/helper-function-name@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz#940e6084a55dee867d33b4e487da2676365e86b0" + integrity sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A== + dependencies: + "@babel/template" "^7.18.6" + "@babel/types" "^7.18.9" + +"@babel/helper-hoist-variables@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" + integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-imports@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" + integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-transforms@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz#5a1079c005135ed627442df31a42887e80fcb712" + integrity sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g== + 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.6" + "@babel/traverse" "^7.18.9" + "@babel/types" "^7.18.9" + +"@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f" + integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w== + +"@babel/helper-simple-access@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea" + integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-split-export-declaration@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" + integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-string-parser@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56" + integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw== + +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + +"@babel/helper-validator-identifier@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" + integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== + +"@babel/helper-validator-option@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" + integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== + +"@babel/helpers@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.9.tgz#4bef3b893f253a1eced04516824ede94dcfe7ff9" + integrity sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ== + dependencies: + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.9" + "@babel/types" "^7.18.9" + +"@babel/highlight@^7.16.7": + version "7.16.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" + integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.18.10", "@babel/parser@^7.18.13": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.13.tgz#5b2dd21cae4a2c5145f1fbd8ca103f9313d3b7e4" + integrity sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg== + +"@babel/plugin-syntax-jsx@^7.17.12", "@babel/plugin-syntax-jsx@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" + integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-react-jsx-development@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz#dbe5c972811e49c7405b630e4d0d2e1380c0ddc5" + integrity sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.18.6" + +"@babel/plugin-transform-react-jsx-self@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.18.6.tgz#3849401bab7ae8ffa1e3e5687c94a753fc75bda7" + integrity sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-react-jsx-source@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.18.6.tgz#06e9ae8a14d2bc19ce6e3c447d842032a50598fc" + integrity sha512-utZmlASneDfdaMh0m/WausbjUjEdGrQJz0vFK93d7wD3xf5wBtX219+q6IlCNZeguIcxS2f/CvLZrlLSvSHQXw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-react-jsx@^7.18.10", "@babel/plugin-transform-react-jsx@^7.18.6": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.10.tgz#ea47b2c4197102c196cbd10db9b3bb20daa820f1" + integrity sha512-gCy7Iikrpu3IZjYZolFE4M1Sm+nrh1/6za2Ewj77Z+XirT4TsbJcvOFOyF+fRPwU6AKKK136CZxx6L8AbSFG6A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-jsx" "^7.18.6" + "@babel/types" "^7.18.10" + +"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" + integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2" + integrity sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.19.0": + version "7.20.1" + resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9" + integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg== + dependencies: + regenerator-runtime "^0.13.10" + +"@babel/template@^7.18.10", "@babel/template@^7.18.6": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" + integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.18.10" + "@babel/types" "^7.18.10" + +"@babel/traverse@^7.18.13", "@babel/traverse@^7.18.9": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.13.tgz#5ab59ef51a997b3f10c4587d648b9696b6cb1a68" + integrity sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.13" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.18.13" + "@babel/types" "^7.18.13" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.16.7": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" + integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + +"@babel/types@^7.18.10", "@babel/types@^7.18.13", "@babel/types@^7.18.4", "@babel/types@^7.18.6", "@babel/types@^7.18.9": + version "7.18.13" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.13.tgz#30aeb9e514f4100f7c1cb6e5ba472b30e48f519a" + integrity sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ== + dependencies: + "@babel/helper-string-parser" "^7.18.10" + "@babel/helper-validator-identifier" "^7.18.6" + to-fast-properties "^2.0.0" + +"@emotion/babel-plugin@^11.10.5": + version "11.10.5" + resolved "https://registry.npmmirror.com/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz#65fa6e1790ddc9e23cc22658a4c5dea423c55c3c" + integrity sha512-xE7/hyLHJac7D2Ve9dKroBBZqBT7WuPQmWcq7HSGb84sUuP4mlOWoB8dvVfD9yk5DHkU1m6RW7xSoDtnQHNQeA== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/plugin-syntax-jsx" "^7.17.12" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.0" + "@emotion/memoize" "^0.8.0" + "@emotion/serialize" "^1.1.1" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.1.3" + +"@emotion/cache@^11.10.3": + version "11.10.3" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.3.tgz#c4f67904fad10c945fea5165c3a5a0583c164b87" + integrity sha512-Psmp/7ovAa8appWh3g51goxu/z3iVms7JXOreq136D8Bbn6dYraPnmL6mdM8GThEx9vwSn92Fz+mGSjBzN8UPQ== + 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" + +"@emotion/cache@^11.10.5": + version "11.10.5" + resolved "https://registry.npmmirror.com/@emotion/cache/-/cache-11.10.5.tgz#c142da9351f94e47527ed458f7bbbbe40bb13c12" + integrity sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA== + dependencies: + "@emotion/memoize" "^0.8.0" + "@emotion/sheet" "^1.2.1" + "@emotion/utils" "^1.2.0" + "@emotion/weak-memoize" "^0.3.0" + stylis "4.1.3" + +"@emotion/hash@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.0.tgz#c5153d50401ee3c027a57a177bc269b16d889cb7" + integrity sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ== + +"@emotion/is-prop-valid@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz#7f2d35c97891669f7e276eb71c83376a5dc44c83" + integrity sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg== + dependencies: + "@emotion/memoize" "^0.8.0" + +"@emotion/memoize@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f" + integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA== + +"@emotion/react@^11.10.5": + version "11.10.5" + resolved "https://registry.npmmirror.com/@emotion/react/-/react-11.10.5.tgz#95fff612a5de1efa9c0d535384d3cfa115fe175d" + integrity sha512-TZs6235tCJ/7iF6/rvTaOH4oxQg2gMAcdHemjwLKIjKz4rRuYe1HJ2TQJKnAcRAfOUDdU8XoDadCe1rl72iv8A== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.10.5" + "@emotion/cache" "^11.10.5" + "@emotion/serialize" "^1.1.1" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0" + "@emotion/utils" "^1.2.0" + "@emotion/weak-memoize" "^0.3.0" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.1.1": + version "1.1.1" + resolved "https://registry.npmmirror.com/@emotion/serialize/-/serialize-1.1.1.tgz#0595701b1902feded8a96d293b26be3f5c1a5cf0" + integrity sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA== + dependencies: + "@emotion/hash" "^0.9.0" + "@emotion/memoize" "^0.8.0" + "@emotion/unitless" "^0.8.0" + "@emotion/utils" "^1.2.0" + csstype "^3.0.2" + +"@emotion/sheet@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.0.tgz#771b1987855839e214fc1741bde43089397f7be5" + integrity sha512-OiTkRgpxescko+M51tZsMq7Puu/KP55wMT8BgpcXVG2hqXc0Vo0mfymJ/Uj24Hp0i083ji/o0aLddh08UEjq8w== + +"@emotion/sheet@^1.2.1": + version "1.2.1" + resolved "https://registry.npmmirror.com/@emotion/sheet/-/sheet-1.2.1.tgz#0767e0305230e894897cadb6c8df2c51e61a6c2c" + integrity sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA== + +"@emotion/styled@^11.10.5": + version "11.10.5" + resolved "https://registry.npmmirror.com/@emotion/styled/-/styled-11.10.5.tgz#1fe7bf941b0909802cb826457e362444e7e96a79" + integrity sha512-8EP6dD7dMkdku2foLoruPCNkRevzdcBaY6q0l0OsbyJK+x8D9HWjX27ARiSIKNF634hY9Zdoedh8bJCiva8yZw== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.10.5" + "@emotion/is-prop-valid" "^1.2.0" + "@emotion/serialize" "^1.1.1" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0" + "@emotion/utils" "^1.2.0" + +"@emotion/unitless@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.0.tgz#a4a36e9cbdc6903737cd20d38033241e1b8833db" + integrity sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw== + +"@emotion/use-insertion-effect-with-fallbacks@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz#ffadaec35dbb7885bd54de3fa267ab2f860294df" + integrity sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A== + +"@emotion/utils@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561" + integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw== + +"@emotion/weak-memoize@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb" + integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg== + +"@esbuild/android-arm@0.15.18": + version "0.15.18" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.18.tgz#266d40b8fdcf87962df8af05b76219bc786b4f80" + integrity sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw== + +"@esbuild/linux-loong64@0.15.18": + version "0.15.18" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz#128b76ecb9be48b60cf5cfc1c63a4f00691a3239" + integrity sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ== + +"@jridgewell/gen-mapping@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c" + integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.11" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" + integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== + +"@jridgewell/trace-mapping@^0.3.0": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3" + integrity sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.15" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" + integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@juggle/resize-observer@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" + integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== + +"@mui/base@5.0.0-alpha.105": + version "5.0.0-alpha.105" + resolved "https://registry.npmmirror.com/@mui/base/-/base-5.0.0-alpha.105.tgz#ddf92c86db3355e0fe6886a818be073e2ee9a9f9" + integrity sha512-4IPBcJQIgVVXQvN6DQMoCHed52GBtwSqYs0jD0dDcMR3o76AodQtpEeWFz3p7mJoc6f/IHBl9U6jEfL1r/kM4g== + dependencies: + "@babel/runtime" "^7.19.0" + "@emotion/is-prop-valid" "^1.2.0" + "@mui/types" "^7.2.0" + "@mui/utils" "^5.10.9" + "@popperjs/core" "^2.11.6" + clsx "^1.2.1" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@mui/core-downloads-tracker@^5.10.13": + version "5.10.13" + resolved "https://registry.npmmirror.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.13.tgz#34068ede2853392ca4fd82ad16d9c1ca664f69b3" + integrity sha512-zWkWPV/SaNdsIdxAWiuVGZ+Ue3BkfSIlU/BFIrJmuUcwiIa7gQsbI/DOpj1KzLvqZhdEe2wC1aG4nCHfzgc1Hg== + +"@mui/icons-material@^5.10.9": + version "5.10.9" + resolved "https://registry.npmmirror.com/@mui/icons-material/-/icons-material-5.10.9.tgz#f9522c49797caf30146acc576e37ecb4f95bbc38" + integrity sha512-sqClXdEM39WKQJOQ0ZCPTptaZgqwibhj2EFV9N0v7BU1PO8y4OcX/a2wIQHn4fNuDjIZktJIBrmU23h7aqlGgg== + dependencies: + "@babel/runtime" "^7.19.0" + +"@mui/material@^5.10.13": + version "5.10.13" + resolved "https://registry.npmmirror.com/@mui/material/-/material-5.10.13.tgz#49c505ed99bc97e573d0cc15bec074b080aacee1" + integrity sha512-TkkT1rNc0/hhL4/+zv4gYcA6egNWBH/1Tz+azoTnQIUdZ32fgwFI2pFX2KVJNTt30xnLznxDWtTv7ilmJQ52xw== + dependencies: + "@babel/runtime" "^7.19.0" + "@mui/base" "5.0.0-alpha.105" + "@mui/core-downloads-tracker" "^5.10.13" + "@mui/system" "^5.10.13" + "@mui/types" "^7.2.0" + "@mui/utils" "^5.10.9" + "@types/react-transition-group" "^4.4.5" + clsx "^1.2.1" + csstype "^3.1.1" + prop-types "^15.8.1" + react-is "^18.2.0" + react-transition-group "^4.4.5" + +"@mui/private-theming@^5.10.9": + version "5.10.9" + resolved "https://registry.npmmirror.com/@mui/private-theming/-/private-theming-5.10.9.tgz#c427bfa736455703975cdb108dbde6a174ba7971" + integrity sha512-BN7/CnsVPVyBaQpDTij4uV2xGYHHHhOgpdxeYLlIu+TqnsVM7wUeF+37kXvHovxM6xmL5qoaVUD98gDC0IZnHg== + dependencies: + "@babel/runtime" "^7.19.0" + "@mui/utils" "^5.10.9" + prop-types "^15.8.1" + +"@mui/styled-engine@^5.10.8": + version "5.10.8" + resolved "https://registry.npmmirror.com/@mui/styled-engine/-/styled-engine-5.10.8.tgz#2db411e4278f06f70ccb6b5cd56ace67109513f6" + integrity sha512-w+y8WI18EJV6zM/q41ug19cE70JTeO6sWFsQ7tgePQFpy6ToCVPh0YLrtqxUZXSoMStW5FMw0t9fHTFAqPbngw== + dependencies: + "@babel/runtime" "^7.19.0" + "@emotion/cache" "^11.10.3" + csstype "^3.1.1" + prop-types "^15.8.1" + +"@mui/system@^5.10.13": + version "5.10.13" + resolved "https://registry.npmmirror.com/@mui/system/-/system-5.10.13.tgz#b32a4441f9dd0760724cdbccf0a09728e63e3674" + integrity sha512-Xzx26Asu5fVlm0ucm+gnJmeX4Y1isrpVDvqxX4yJaOT7Fzmd8Lfq9ih3QMfZajns5LMtUiOuCQlVFRtUG5IY7A== + dependencies: + "@babel/runtime" "^7.19.0" + "@mui/private-theming" "^5.10.9" + "@mui/styled-engine" "^5.10.8" + "@mui/types" "^7.2.0" + "@mui/utils" "^5.10.9" + clsx "^1.2.1" + csstype "^3.1.1" + prop-types "^15.8.1" + +"@mui/types@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.0.tgz#91380c2d42420f51f404120f7a9270eadd6f5c23" + integrity sha512-lGXtFKe5lp3UxTBGqKI1l7G8sE2xBik8qCfrLHD5olwP/YU0/ReWoWT7Lp1//ri32dK39oPMrJN8TgbkCSbsNA== + +"@mui/utils@^5.10.3": + version "5.10.3" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.10.3.tgz#ce2a96f31de2a5e717f507b5383dbabbddbc4dfc" + integrity sha512-4jXMDPfx6bpMVuheLaOpKTjpzw39ogAZLeaLj5+RJec3E37/hAZMYjURfblLfTWMMoGoqkY03mNsZaEwNobBow== + dependencies: + "@babel/runtime" "^7.18.9" + "@types/prop-types" "^15.7.5" + "@types/react-is" "^16.7.1 || ^17.0.0" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@mui/utils@^5.10.9": + version "5.10.9" + resolved "https://registry.npmmirror.com/@mui/utils/-/utils-5.10.9.tgz#9dc455f9230f43eeb81d96a9a4bdb3855bb9ea39" + integrity sha512-2tdHWrq3+WCy+G6TIIaFx3cg7PorXZ71P375ExuX61od1NOAJP1mK90VxQ8N4aqnj2vmO3AQDkV4oV2Ktvt4bA== + dependencies: + "@babel/runtime" "^7.19.0" + "@types/prop-types" "^15.7.5" + "@types/react-is" "^16.7.1 || ^17.0.0" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@mui/x-data-grid@^5.17.11": + version "5.17.11" + resolved "https://registry.npmmirror.com/@mui/x-data-grid/-/x-data-grid-5.17.11.tgz#3a2a9889fb24030d8f11b03319638392d7df8752" + integrity sha512-9KaAsEHKTho/hXXSboxkewBI5HF9NwmgaHCjX7UCg/av3yP2wcWELui9mAWUjI6qm6+8hvKmKclf20ZZ+aPiNg== + dependencies: + "@babel/runtime" "^7.18.9" + "@mui/utils" "^5.10.3" + clsx "^1.2.1" + prop-types "^15.8.1" + reselect "^4.1.6" + +"@octokit/auth-token@^2.4.4": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36" + integrity sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g== + dependencies: + "@octokit/types" "^6.0.3" + +"@octokit/core@^3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.6.0.tgz#3376cb9f3008d9b3d110370d90e0a1fcd5fe6085" + integrity sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q== + dependencies: + "@octokit/auth-token" "^2.4.4" + "@octokit/graphql" "^4.5.8" + "@octokit/request" "^5.6.3" + "@octokit/request-error" "^2.0.5" + "@octokit/types" "^6.0.3" + before-after-hook "^2.2.0" + universal-user-agent "^6.0.0" + +"@octokit/endpoint@^6.0.1": + version "6.0.12" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658" + integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA== + dependencies: + "@octokit/types" "^6.0.3" + is-plain-object "^5.0.0" + universal-user-agent "^6.0.0" + +"@octokit/graphql@^4.5.8": + version "4.8.0" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.8.0.tgz#664d9b11c0e12112cbf78e10f49a05959aa22cc3" + integrity sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg== + dependencies: + "@octokit/request" "^5.6.0" + "@octokit/types" "^6.0.3" + universal-user-agent "^6.0.0" + +"@octokit/openapi-types@^11.2.0": + version "11.2.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-11.2.0.tgz#b38d7fc3736d52a1e96b230c1ccd4a58a2f400a6" + integrity sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA== + +"@octokit/plugin-paginate-rest@^2.17.0": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz#32e9c7cab2a374421d3d0de239102287d791bce7" + integrity sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw== + dependencies: + "@octokit/types" "^6.34.0" + +"@octokit/plugin-rest-endpoint-methods@^5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz#8c46109021a3412233f6f50d28786f8e552427ba" + integrity sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA== + dependencies: + "@octokit/types" "^6.34.0" + deprecation "^2.3.1" + +"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" + integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg== + dependencies: + "@octokit/types" "^6.0.3" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request@^5.6.0", "@octokit/request@^5.6.3": + version "5.6.3" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.3.tgz#19a022515a5bba965ac06c9d1334514eb50c48b0" + integrity sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A== + dependencies: + "@octokit/endpoint" "^6.0.1" + "@octokit/request-error" "^2.1.0" + "@octokit/types" "^6.16.1" + is-plain-object "^5.0.0" + node-fetch "^2.6.7" + universal-user-agent "^6.0.0" + +"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.34.0": + version "6.34.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.34.0.tgz#c6021333334d1ecfb5d370a8798162ddf1ae8218" + integrity sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw== + dependencies: + "@octokit/openapi-types" "^11.2.0" + +"@popperjs/core@^2.11.6": + version "2.11.6" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" + integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== + +"@remix-run/router@1.0.3": + version "1.0.3" + resolved "https://registry.npmmirror.com/@remix-run/router/-/router-1.0.3.tgz#953b88c20ea00d0eddaffdc1b115c08474aa295d" + integrity sha512-ceuyTSs7PZ/tQqi19YZNBc5X7kj1f8p+4DIyrcIYFY9h+hd1OKm4RqtiWldR9eGEvIiJfsqwM4BsuCtRIuEw6Q== + +"@rollup/pluginutils@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" + integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + +"@svgr/babel-plugin-add-jsx-attribute@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.3.1.tgz#b9a5d84902be75a05ede92e70b338d28ab63fa74" + integrity sha512-jDBKArXYO1u0B1dmd2Nf8Oy6aTF5vLDfLoO9Oon/GLkqZ/NiggYWZA+a2HpUMH4ITwNqS3z43k8LWApB8S583w== + +"@svgr/babel-plugin-remove-jsx-attribute@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.3.1.tgz#4877995452efc997b36777abe1fde9705ef78e8b" + integrity sha512-dQzyJ4prwjcFd929T43Z8vSYiTlTu8eafV40Z2gO7zy/SV5GT+ogxRJRBIKWomPBOiaVXFg3jY4S5hyEN3IBjQ== + +"@svgr/babel-plugin-remove-jsx-empty-expression@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-6.3.1.tgz#2d67a0e92904c9be149a5b22d3a3797ce4d7b514" + integrity sha512-HBOUc1XwSU67fU26V5Sfb8MQsT0HvUyxru7d0oBJ4rA2s4HW3PhyAPC7fV/mdsSGpAvOdd8Wpvkjsr0fWPUO7A== + +"@svgr/babel-plugin-replace-jsx-attribute-value@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.3.1.tgz#306f5247139c53af70d1778f2719647c747998ee" + integrity sha512-C12e6aN4BXAolRrI601gPn5MDFCRHO7C4TM8Kks+rDtl8eEq+NN1sak0eAzJu363x3TmHXdZn7+Efd2nr9I5dA== + +"@svgr/babel-plugin-svg-dynamic-title@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.3.1.tgz#6ce26d34cbc93eb81737ef528528907c292e7aa2" + integrity sha512-6NU55Mmh3M5u2CfCCt6TX29/pPneutrkJnnDCHbKZnjukZmmgUAZLtZ2g6ZoSPdarowaQmAiBRgAHqHmG0vuqA== + +"@svgr/babel-plugin-svg-em-dimensions@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.3.1.tgz#5ade2a724b290873c30529d1d8cd23523856287a" + integrity sha512-HV1NGHYTTe1vCNKlBgq/gKuCSfaRlKcHIADn7P8w8U3Zvujdw1rmusutghJ1pZJV7pDt3Gt8ws+SVrqHnBO/Qw== + +"@svgr/babel-plugin-transform-react-native-svg@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.3.1.tgz#d654f509d692c3a09dfb475757a44bd9f6ad7ddf" + integrity sha512-2wZhSHvTolFNeKDAN/ZmIeSz2O9JSw72XD+o2bNp2QAaWqa8KGpn5Yk5WHso6xqfSAiRzAE+GXlsrBO4UP9LLw== + +"@svgr/babel-plugin-transform-svg-component@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.3.1.tgz#21a285dbffdce9567c437ebf0d081bf9210807e6" + integrity sha512-cZ8Tr6ZAWNUFfDeCKn/pGi976iWSkS8ijmEYKosP+6ktdZ7lW9HVLHojyusPw3w0j8PI4VBeWAXAmi/2G7owxw== + +"@svgr/babel-preset@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-6.3.1.tgz#8bd1ead79637d395e9362b01dd37cfd59702e152" + integrity sha512-tQtWtzuMMQ3opH7je+MpwfuRA1Hf3cKdSgTtAYwOBDfmhabP7rcTfBi3E7V3MuwJNy/Y02/7/RutvwS1W4Qv9g== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "^6.3.1" + "@svgr/babel-plugin-remove-jsx-attribute" "^6.3.1" + "@svgr/babel-plugin-remove-jsx-empty-expression" "^6.3.1" + "@svgr/babel-plugin-replace-jsx-attribute-value" "^6.3.1" + "@svgr/babel-plugin-svg-dynamic-title" "^6.3.1" + "@svgr/babel-plugin-svg-em-dimensions" "^6.3.1" + "@svgr/babel-plugin-transform-react-native-svg" "^6.3.1" + "@svgr/babel-plugin-transform-svg-component" "^6.3.1" + +"@svgr/core@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-6.3.1.tgz#752adf49d8d5473b15d76ca741961de093f715bd" + integrity sha512-Sm3/7OdXbQreemf9aO25keerZSbnKMpGEfmH90EyYpj1e8wMD4TuwJIb3THDSgRMWk1kYJfSRulELBy4gVgZUA== + dependencies: + "@svgr/plugin-jsx" "^6.3.1" + camelcase "^6.2.0" + cosmiconfig "^7.0.1" + +"@svgr/hast-util-to-babel-ast@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.3.1.tgz#59614e24d2a4a28010e02089213b3448d905769d" + integrity sha512-NgyCbiTQIwe3wHe/VWOUjyxmpUmsrBjdoIxKpXt3Nqc3TN30BpJG22OxBvVzsAh9jqep0w0/h8Ywvdk3D9niNQ== + dependencies: + "@babel/types" "^7.18.4" + entities "^4.3.0" + +"@svgr/plugin-jsx@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-6.3.1.tgz#de7b2de824296b836d6b874d498377896e367f50" + integrity sha512-r9+0mYG3hD4nNtUgsTXWGYJomv/bNd7kC16zvsM70I/bGeoCi/3lhTmYqeN6ChWX317OtQCSZZbH4wq9WwoXbw== + dependencies: + "@babel/core" "^7.18.5" + "@svgr/babel-preset" "^6.3.1" + "@svgr/hast-util-to-babel-ast" "^6.3.1" + svg-parser "^2.0.4" + +"@tauri-apps/api@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-1.3.0.tgz#d0c853ab2cc7506bd826c5f7f260c67c7c15def5" + integrity sha512-AH+3FonkKZNtfRtGrObY38PrzEj4d+1emCbwNGu0V2ENbXjlLHMZQlUh+Bhu/CRmjaIwZMGJ3yFvWaZZgTHoog== + +"@tauri-apps/cli-darwin-arm64@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.3.1.tgz#ef0fe290e0a6e3e53fa2cc4f1a72a0c87921427c" + integrity sha512-QlepYVPgOgspcwA/u4kGG4ZUijlXfdRtno00zEy+LxinN/IRXtk+6ErVtsmoLi1ZC9WbuMwzAcsRvqsD+RtNAg== + +"@tauri-apps/cli-darwin-x64@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.3.1.tgz#4c84ea0f08a5b636b067943d637a38e091a4aad3" + integrity sha512-fKcAUPVFO3jfDKXCSDGY0MhZFF/wDtx3rgFnogWYu4knk38o9RaqRkvMvqJhLYPuWaEM5h6/z1dRrr9KKCbrVg== + +"@tauri-apps/cli-linux-arm-gnueabihf@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.3.1.tgz#a4f1b237189e4f8f89cc890e1dc2eec76d4345be" + integrity sha512-+4H0dv8ltJHYu/Ma1h9ixUPUWka9EjaYa8nJfiMsdCI4LJLNE6cPveE7RmhZ59v9GW1XB108/k083JUC/OtGvA== + +"@tauri-apps/cli-linux-arm64-gnu@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.3.1.tgz#e2391326b64dfe13c7442bdcc13c4988ce5e6df9" + integrity sha512-Pj3odVO1JAxLjYmoXKxcrpj/tPxcA8UP8N06finhNtBtBaxAjrjjxKjO4968KB0BUH7AASIss9EL4Tr0FGnDuw== + +"@tauri-apps/cli-linux-arm64-musl@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.3.1.tgz#49354349f80f879ffc6950c0c03c0aea1395efa5" + integrity sha512-tA0JdDLPFaj42UDIVcF2t8V0tSha40rppcmAR/MfQpTCxih6399iMjwihz9kZE1n4b5O4KTq9GliYo50a8zYlQ== + +"@tauri-apps/cli-linux-x64-gnu@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.3.1.tgz#9a33ffe9e0d9b1b3825db57cbcfcddeb773682c6" + integrity sha512-FDU+Mnvk6NLkqQimcNojdKpMN4Y3W51+SQl+NqG9AFCWprCcSg62yRb84751ujZuf2MGT8HQOfmd0i77F4Q3tQ== + +"@tauri-apps/cli-linux-x64-musl@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.3.1.tgz#5283731e894c17bc070c499e73145cfe2633ef21" + integrity sha512-MpO3akXFmK8lZYEbyQRDfhdxz1JkTBhonVuz5rRqxwA7gnGWHa1aF1+/2zsy7ahjB2tQ9x8DDFDMdVE20o9HrA== + +"@tauri-apps/cli-win32-ia32-msvc@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.3.1.tgz#f31538abfd94f27ade1f17d01f30da6be1660c6f" + integrity sha512-9Boeo3K5sOrSBAZBuYyGkpV2RfnGQz3ZhGJt4hE6P+HxRd62lS6+qDKAiw1GmkZ0l1drc2INWrNeT50gwOKwIQ== + +"@tauri-apps/cli-win32-x64-msvc@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.3.1.tgz#1eb09d55b99916a3cd84cb91c75ef906db67d35d" + integrity sha512-wMrTo91hUu5CdpbElrOmcZEoJR4aooTG+fbtcc87SMyPGQy1Ux62b+ZdwLvL1sVTxnIm//7v6QLRIWGiUjCPwA== + +"@tauri-apps/cli@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-1.3.1.tgz#4c5259bf1f9c97084dd016e6b34dca53de380e24" + integrity sha512-o4I0JujdITsVRm3/0spfJX7FcKYrYV1DXJqzlWIn6IY25/RltjU6qbC1TPgVww3RsRX63jyVUTcWpj5wwFl+EQ== + optionalDependencies: + "@tauri-apps/cli-darwin-arm64" "1.3.1" + "@tauri-apps/cli-darwin-x64" "1.3.1" + "@tauri-apps/cli-linux-arm-gnueabihf" "1.3.1" + "@tauri-apps/cli-linux-arm64-gnu" "1.3.1" + "@tauri-apps/cli-linux-arm64-musl" "1.3.1" + "@tauri-apps/cli-linux-x64-gnu" "1.3.1" + "@tauri-apps/cli-linux-x64-musl" "1.3.1" + "@tauri-apps/cli-win32-ia32-msvc" "1.3.1" + "@tauri-apps/cli-win32-x64-msvc" "1.3.1" + +"@types/fs-extra@^9.0.13": + version "9.0.13" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" + integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA== + dependencies: + "@types/node" "*" + +"@types/js-cookie@^2.x.x": + version "2.2.7" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.7.tgz#226a9e31680835a6188e887f3988e60c04d3f6a3" + integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA== + +"@types/js-cookie@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.2.tgz#451eaeece64c6bdac8b2dde0caab23b085899e0d" + integrity sha512-6+0ekgfusHftJNYpihfkMu8BWdeHs9EOJuGcSofErjstGPfPGEu9yTu4t460lTzzAMl2cM5zngQJqPMHbbnvYA== + +"@types/lodash-es@^4.17.7": + version "4.17.7" + resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.7.tgz#22edcae9f44aff08546e71db8925f05b33c7cc40" + integrity sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.191" + resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" + integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== + +"@types/lodash@^4.14.180": + version "4.14.180" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" + integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g== + +"@types/minimatch@^3.0.3": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" + integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== + +"@types/node@*": + version "17.0.23" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da" + integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/prop-types@*": + version "15.7.4" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" + integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + +"@types/prop-types@^15.7.5": + version "15.7.5" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + +"@types/react-dom@^18.0.11": + version "18.0.11" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33" + integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw== + dependencies: + "@types/react" "*" + +"@types/react-is@^16.7.1 || ^17.0.0": + version "17.0.3" + resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a" + integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw== + dependencies: + "@types/react" "*" + +"@types/react-transition-group@^4.4.5": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416" + integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA== + dependencies: + "@types/react" "*" + +"@types/react@*": + version "17.0.43" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55" + integrity sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + +"@virtuoso.dev/react-urx@^0.2.12": + version "0.2.13" + resolved "https://registry.yarnpkg.com/@virtuoso.dev/react-urx/-/react-urx-0.2.13.tgz#e2cfc42d259d2a002695e7517d34cb97b64ee9c4" + integrity sha512-MY0ugBDjFb5Xt8v2HY7MKcRGqw/3gTpMlLXId2EwQvYJoC8sP7nnXjAxcBtTB50KTZhO0SbzsFimaZ7pSdApwA== + dependencies: + "@virtuoso.dev/urx" "^0.2.13" + +"@virtuoso.dev/urx@^0.2.12", "@virtuoso.dev/urx@^0.2.13": + version "0.2.13" + resolved "https://registry.yarnpkg.com/@virtuoso.dev/urx/-/urx-0.2.13.tgz#a65e7e8d923cb03397ac876bfdd45c7f71c8edf1" + integrity sha512-iirJNv92A1ZWxoOHHDYW/1KPoi83939o83iUBQHIim0i3tMeSKEh+bxhJdTHQ86Mr4uXx9xGUTq69cp52ZP8Xw== + +"@vitejs/plugin-react@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-2.0.1.tgz#3197c01d8e4a4eb9fed829c7888c467a43aadd4e" + integrity sha512-uINzNHmjrbunlFtyVkST6lY1ewSfz/XwLufG0PIqvLGnpk2nOIOa/1CACTDNcKi1/RwaCzJLmsXwm1NsUVV/NA== + dependencies: + "@babel/core" "^7.18.10" + "@babel/plugin-transform-react-jsx" "^7.18.10" + "@babel/plugin-transform-react-jsx-development" "^7.18.6" + "@babel/plugin-transform-react-jsx-self" "^7.18.6" + "@babel/plugin-transform-react-jsx-source" "^7.18.6" + magic-string "^0.26.2" + react-refresh "^0.14.0" + +adm-zip@^0.5.9: + version "0.5.9" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.9.tgz#b33691028333821c0cf95c31374c5462f2905a83" + integrity sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg== + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +ahooks-v3-count@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ahooks-v3-count/-/ahooks-v3-count-1.0.0.tgz#ddeb392e009ad6e748905b3cbf63a9fd8262ca80" + integrity sha512-V7uUvAwnimu6eh/PED4mCDjE7tokeZQLKlxg9lCTMPhN+NjsSbtdacByVlR1oluXQzD3MOw55wylDmQo4+S9ZQ== + +ahooks@^3.7.2: + version "3.7.2" + resolved "https://registry.npmmirror.com/ahooks/-/ahooks-3.7.2.tgz#0afa42625e77ae1cc4b60b19c45cf12a8cf29b56" + integrity sha512-nJPsQJcmJnGaNXiqgZdfO7UMs+o926LQg6VyDYt2vzKhXU8Ze/U87NsA/FeIvlIZB0rQr/j7uotFb1bGPp627A== + dependencies: + "@types/js-cookie" "^2.x.x" + ahooks-v3-count "^1.0.0" + dayjs "^1.9.1" + intersection-observer "^0.12.0" + js-cookie "^2.x.x" + lodash "^4.17.21" + resize-observer-polyfill "^1.5.1" + screenfull "^5.0.0" + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +array-differ@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" + integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +arrify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.1.3: + version "1.1.3" + resolved "https://registry.npmmirror.com/axios/-/axios-1.1.3.tgz#8274250dada2edf53814ed7db644b9c2866c1e35" + integrity sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +babel-plugin-macros@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" + integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +before-after-hook@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" + integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.20.2: + version "4.20.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.3.tgz#eb7572f49ec430e054f56d52ff0ebe9be915f8bf" + integrity sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg== + dependencies: + caniuse-lite "^1.0.30001332" + electron-to-chromium "^1.4.118" + escalade "^3.1.1" + node-releases "^2.0.3" + picocolors "^1.0.0" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001332: + version "1.0.30001341" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001341.tgz#59590c8ffa8b5939cf4161f00827b8873ad72498" + integrity sha512-2SodVrFFtvGENGCv0ChVJIDQ0KPaS1cg7/qtfMaICgeMolDdo/Z2OD32F0Aq9yl6F4YFwGPBS5AaPqNYiW4PoA== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +"chokidar@>=3.0.0 <4.0.0": + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +clsx@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +convert-source-map@^1.5.0, convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + +cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +csstype@^3.0.2: + version "3.0.11" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33" + integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw== + +csstype@^3.1.1: + version "3.1.1" + resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" + integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== + +data-uri-to-buffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b" + integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA== + +dayjs@1.11.5, dayjs@^1.9.1: + version "1.11.5" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93" + integrity sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA== + +debug@4, debug@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + +electron-to-chromium@^1.4.118: + version "1.4.137" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz#186180a45617283f1c012284458510cd99d6787f" + integrity sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA== + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +entities@^4.3.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" + integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +esbuild-android-64@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz#20a7ae1416c8eaade917fb2453c1259302c637a5" + integrity sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA== + +esbuild-android-arm64@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz#9cc0ec60581d6ad267568f29cf4895ffdd9f2f04" + integrity sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ== + +esbuild-darwin-64@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz#428e1730ea819d500808f220fbc5207aea6d4410" + integrity sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg== + +esbuild-darwin-arm64@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz#b6dfc7799115a2917f35970bfbc93ae50256b337" + integrity sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA== + +esbuild-freebsd-64@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz#4e190d9c2d1e67164619ae30a438be87d5eedaf2" + integrity sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA== + +esbuild-freebsd-arm64@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz#18a4c0344ee23bd5a6d06d18c76e2fd6d3f91635" + integrity sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA== + +esbuild-linux-32@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz#9a329731ee079b12262b793fb84eea762e82e0ce" + integrity sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg== + +esbuild-linux-64@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz#532738075397b994467b514e524aeb520c191b6c" + integrity sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw== + +esbuild-linux-arm64@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz#5372e7993ac2da8f06b2ba313710d722b7a86e5d" + integrity sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug== + +esbuild-linux-arm@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz#e734aaf259a2e3d109d4886c9e81ec0f2fd9a9cc" + integrity sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA== + +esbuild-linux-mips64le@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz#c0487c14a9371a84eb08fab0e1d7b045a77105eb" + integrity sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ== + +esbuild-linux-ppc64le@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz#af048ad94eed0ce32f6d5a873f7abe9115012507" + integrity sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w== + +esbuild-linux-riscv64@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz#423ed4e5927bd77f842bd566972178f424d455e6" + integrity sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg== + +esbuild-linux-s390x@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz#21d21eaa962a183bfb76312e5a01cc5ae48ce8eb" + integrity sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ== + +esbuild-netbsd-64@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz#ae75682f60d08560b1fe9482bfe0173e5110b998" + integrity sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg== + +esbuild-openbsd-64@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz#79591a90aa3b03e4863f93beec0d2bab2853d0a8" + integrity sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ== + +esbuild-sunos-64@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz#fd528aa5da5374b7e1e93d36ef9b07c3dfed2971" + integrity sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw== + +esbuild-windows-32@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz#0e92b66ecdf5435a76813c4bc5ccda0696f4efc3" + integrity sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ== + +esbuild-windows-64@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz#0fc761d785414284fc408e7914226d33f82420d0" + integrity sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw== + +esbuild-windows-arm64@0.15.18: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz#5b5bdc56d341d0922ee94965c89ee120a6a86eb7" + integrity sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ== + +esbuild@^0.15.9: + version "0.15.18" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.18.tgz#ea894adaf3fbc036d32320a00d4d6e4978a2f36d" + integrity sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q== + 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" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +estree-walker@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +execa@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.1.5" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.1.5.tgz#0077bf5f3fcdbd9d75a0b5362f77dbb743489863" + integrity sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + +fs-extra@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" + integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-stream@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.9" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" + integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== + +hamt_plus@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/hamt_plus/-/hamt_plus-1.0.2.tgz#e21c252968c7e33b20f6a1b094cd85787a265601" + integrity sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE= + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hoist-non-react-statics@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + +https-proxy-agent@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + +husky@^7.0.0: + version "7.0.4" + resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" + integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== + +i18next@^22.0.4: + version "22.0.4" + resolved "https://registry.npmmirror.com/i18next/-/i18next-22.0.4.tgz#77d8871687b0ab072b38991e3887187823667e30" + integrity sha512-TOp7BTMKDbUkOHMzDlVsCYWpyaFkKakrrO3HNXfSz4EeJaWwnBScRmgQSTaWHScXVHBUFXTvShrCW8uryBYFcg== + dependencies: + "@babel/runtime" "^7.17.2" + +ignore@^5.1.4: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + +immutable@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" + integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +intersection-observer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.12.0.tgz#6c84628f67ce8698e5f9ccf857d97718745837aa" + integrity sha512-2Vkz8z46Dv401zTWudDGwO7KiGHNDkMv417T5ItcNYfmvHR/1qCTVBO9vwH8zZmQ0WkA/1ARwpysR9bsnop4NQ== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.9.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" + integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== + dependencies: + has "^1.0.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +js-cookie@^2.x.x: + version "2.2.1" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" + integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +magic-string@^0.26.2: + version "0.26.3" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.3.tgz#25840b875140f7b4785ab06bddc384270b7dd452" + integrity sha512-u1Po0NDyFcwdg2nzHT88wSK0+Rih0N1M+Ph1Sp08k8yvFFU3KR72wryS7e1qMPJypt99WB7fIFVCA92mQrMjrg== + dependencies: + sourcemap-codec "^1.4.8" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +monaco-editor@^0.34.1: + version "0.34.1" + resolved "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.34.1.tgz#1b75c4ad6bc4c1f9da656d740d98e0b850a22f87" + integrity sha512-FKc80TyiMaruhJKKPz5SpJPIjL+dflGvz4CpuThaPMc94AyN7SeC9HQ8hrvaxX7EyHdJcUY5i4D0gNyJj1vSZQ== + +mri@^1.1.5: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +multimatch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3" + integrity sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ== + dependencies: + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" + minimatch "^3.0.4" + +nanoid@^3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" + integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== + +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + +node-fetch@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.6.tgz#6d4627181697a9d9674aae0d61548e0d629b31b9" + integrity sha512-LAy/HZnLADOVkVPubaxHDft29booGglPFDr2Hw0J1AercRh01UiVFm++KMDnJeH9sHgNB4hsXPii7Sgym/sTbw== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + +node-releases@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476" + integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +postcss@^8.4.18: + version "8.4.21" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" + integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +prettier@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" + integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== + +pretty-quick@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/pretty-quick/-/pretty-quick-3.1.3.tgz#15281108c0ddf446675157ca40240099157b638e" + integrity sha512-kOCi2FJabvuh1as9enxYmrnBC6tVMoVOenMaBqRfsvBHB0cbpYHjdQEpSglpASDFEXVwplpcGR4CLEaisYAFcA== + dependencies: + chalk "^3.0.0" + execa "^4.0.0" + find-up "^4.1.0" + ignore "^5.1.4" + mri "^1.1.5" + multimatch "^4.0.0" + +prop-types@^15.6.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react-error-boundary@^3.1.4: + version "3.1.4" + resolved "https://registry.npmmirror.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + +react-hook-form@^7.39.5: + version "7.39.5" + resolved "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.39.5.tgz#a4272b60288ef5e1bb42bbb6ba3b36d243ab2879" + integrity sha512-OE0HKyz5IPc6svN2wd+e+evidZrw4O4WZWAWYzQVZuHi+hYnHFSLnxOq0ddjbdmaLIsLHut/ab7j72y2QT3+KA== + +react-i18next@^12.0.0: + version "12.0.0" + resolved "https://registry.npmmirror.com/react-i18next/-/react-i18next-12.0.0.tgz#634015a2c035779c5736ae4c2e5c34c1659753b1" + integrity sha512-/O7N6aIEAl1FaWZBNvhdIo9itvF/MO/nRKr9pYqRc9LhuC1u21SlfwpiYQqvaeNSEW3g3qUXLREOWMt+gxrWbg== + dependencies: + "@babel/runtime" "^7.14.5" + html-parse-stringify "^3.0.1" + +react-is@^16.13.1, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +react-refresh@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" + integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== + +react-router-dom@^6.4.3: + version "6.4.3" + resolved "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.4.3.tgz#70093b5f65f85f1df9e5d4182eb7ff3a08299275" + integrity sha512-MiaYQU8CwVCaOfJdYvt84KQNjT78VF0TJrA17SIQgNHRvLnXDJO6qsFqq8F/zzB1BWZjCFIrQpu4QxcshitziQ== + dependencies: + "@remix-run/router" "1.0.3" + react-router "6.4.3" + +react-router@6.4.3: + version "6.4.3" + resolved "https://registry.npmmirror.com/react-router/-/react-router-6.4.3.tgz#9ed3ee4d6e95889e9b075a5d63e29acc7def0d49" + integrity sha512-BT6DoGn6aV1FVP5yfODMOiieakp3z46P1Fk0RNzJMACzE7C339sFuHebfvWtnB4pzBvXXkHP2vscJzWRuUjTtA== + dependencies: + "@remix-run/router" "1.0.3" + +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react-virtuoso@^3.1.3: + version "3.1.3" + resolved "https://registry.npmmirror.com/react-virtuoso/-/react-virtuoso-3.1.3.tgz#db811ff6fdd4749cfe9348f6d0b1333a348e65c4" + integrity sha512-sc4WICEZkyT+XdVc7gA/61UT43ZnMSX0ugh+xBG2cX+EDWs31wP1dSKQ2HSQ0YFLhZXRJ+Jqndqa8MTu4NE4CQ== + dependencies: + "@virtuoso.dev/react-urx" "^0.2.12" + "@virtuoso.dev/urx" "^0.2.12" + +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +recoil@^0.7.6: + version "0.7.6" + resolved "https://registry.npmmirror.com/recoil/-/recoil-0.7.6.tgz#75297ecd70bbfeeb72e861aa6141a86bb6dfcd5e" + integrity sha512-hsBEw7jFdpBCY/tu2GweiyaqHKxVj6EqF2/SfrglbKvJHhpN57SANWvPW+gE90i3Awi+A5gssOd3u+vWlT+g7g== + dependencies: + hamt_plus "1.0.2" + +regenerator-runtime@^0.13.10: + version "0.13.10" + resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee" + integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw== + +regenerator-runtime@^0.13.4: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + +reselect@^4.1.6: + version "4.1.6" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.6.tgz#19ca2d3d0b35373a74dc1c98692cdaffb6602656" + integrity sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ== + +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.19.0, resolve@^1.22.1: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rollup@^2.79.1: + version "2.79.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" + integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== + optionalDependencies: + fsevents "~2.3.2" + +safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +sass@^1.54.0: + version "1.54.8" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.8.tgz#4adef0dd86ea2b1e4074f551eeda4fc5f812a996" + integrity sha512-ib4JhLRRgbg6QVy6bsv5uJxnJMTS2soVcCp9Y88Extyy13A8vV0G1fAwujOzmNkFQbR3LvedudAMbtuNRPbQww== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + +screenfull@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba" + integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA== + +semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +snarkdown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/snarkdown/-/snarkdown-2.0.0.tgz#b1feb4db91b9f94a8ebbd7a50f3e99aee18b1e03" + integrity sha512-MgL/7k/AZdXCTJiNgrO7chgDqaB9FGM/1Tvlcenenb7div6obaDATzs16JhFyHHBGodHT3B7RzRc5qk8pFhg3A== + +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +stylis@4.0.13: + version "4.0.13" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91" + integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag== + +stylis@4.1.3: + version "4.1.3" + resolved "https://registry.npmmirror.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7" + integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svg-parser@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" + integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== + +swr@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8" + integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + +tunnel@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + +typescript@^4.7.4: + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== + +universal-user-agent@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" + integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +vite-plugin-monaco-editor@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.1.0.tgz#a6238c2e13d5e98dd54a1bc51f6f189325219de3" + integrity sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww== + +vite-plugin-svgr@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/vite-plugin-svgr/-/vite-plugin-svgr-2.2.1.tgz#6e1132a3b66f71e1d69e8c5fe989393260184ac3" + integrity sha512-+EqwahbwjETJH/ssA/66dNYyKN1cO0AStq96MuXmq5maU7AePBMf2lDKfQna49tJZAjtRz+R899BWCsUUP45Fg== + dependencies: + "@rollup/pluginutils" "^4.2.1" + "@svgr/core" "^6.3.1" + +vite@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.5.tgz#dee5678172a8a0ab3e547ad4148c3d547f90e86a" + integrity sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ== + dependencies: + esbuild "^0.15.9" + postcss "^8.4.18" + resolve "^1.22.1" + rollup "^2.79.1" + optionalDependencies: + fsevents "~2.3.2" + +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk= + +web-streams-polyfill@^3.0.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965" + integrity sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==