commit 08678dcacc15f7083e2d1bcdc92b59a840351e7d Author: zhom <2717306+zhom@users.noreply.github.com> Date: Thu May 29 10:17:16 2025 +0400 init diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9edd6e0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,28 @@ +version: 2 +updates: + # Enable version updates for Node.js dependencies + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + allow: + - dependency-type: "all" + groups: + all: + patterns: + - "*" + ignore: + - dependency-name: "eslint" + versions: ">= 9" + + # Enable version updates for rust + - package-ecosystem: "cargo" + directory: "/src-tauri" + schedule: + interval: "weekly" + allow: + - dependency-type: "all" + groups: + all: + patterns: + - "*" diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml new file mode 100644 index 0000000..3c44231 --- /dev/null +++ b/.github/workflows/dependabot-automerge.yml @@ -0,0 +1,34 @@ +# Automatically squashes and merges Dependabot dependency upgrades if tests pass + +name: Dependabot Auto-merge + +on: pull_request_target + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + - name: Fetch Dependabot metadata + id: dependabot-metadata + uses: dependabot/fetch-metadata@v2 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Approve Dependabot PR + run: gh pr review --approve "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Auto-merge (squash) Dependabot PR + if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }} + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml new file mode 100644 index 0000000..90432cd --- /dev/null +++ b/.github/workflows/lint-js.yml @@ -0,0 +1,46 @@ +# Installs Node.js dependencies and pnpm, and checks formatting + linting + +name: Lint Node.js + +on: + push: + branches: + - main + pull_request: + paths-ignore: + - "src-tauri/**" + - "README.md" + +jobs: + build: + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + steps: + - name: Disable git core.autocrlf on Windows + if: matrix.os == 'windows-latest' + run: git config --global core.autocrlf false + + - name: Checkout repository code + uses: actions/checkout@v4 + + - name: Set up pnpm package manager + uses: pnpm/action-setup@v4 + with: + version: latest + + - name: Set up Node.js v22 + uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: "pnpm" + + - name: Install dependencies from lockfile + run: pnpm install --frozen-lockfile + + - name: Run lint step + run: pnpm lint diff --git a/.github/workflows/lint-rs.yml b/.github/workflows/lint-rs.yml new file mode 100644 index 0000000..1491c2a --- /dev/null +++ b/.github/workflows/lint-rs.yml @@ -0,0 +1,55 @@ +# Installs Rust and checks formatting + linting + +name: Lint Rust + +on: + push: + branches: + - main + pull_request: + paths-ignore: + - "src/**" + - "package.json" + - "package-lock.json" + - "yarn.lock" + - "pnpm-lock.yaml" + - "README.md" + +jobs: + build: + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + steps: + - name: Disable git core.autocrlf on Windows + if: matrix.os == 'windows-latest' + run: git config --global core.autocrlf false + + - name: Checkout repository code + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Install Linux dependencies + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev + + - name: Create empty 'dist' directory + run: mkdir dist + + - name: Run rustfmt check + run: cargo fmt --all -- --check + working-directory: src-tauri + + - name: Run clippy check and deny warnings + run: cargo clippy --all-targets --all-features -- -D warnings + working-directory: src-tauri diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b4081c2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,123 @@ +name: Release + +on: + push: + tags: + - "v*" + +env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + +jobs: + release: + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - platform: "macos-latest" + args: "--target aarch64-apple-darwin" + arch: "aarch64" + target: "aarch64-apple-darwin" + pkg_target: "latest-macos-arm64" + nodecar_script: "build:aarch64" + - platform: "macos-latest" + args: "--target x86_64-apple-darwin" + arch: "x86_64" + target: "x86_64-apple-darwin" + pkg_target: "latest-macos-x64" + nodecar_script: "build:x86_64" + # Future platforms can be added here: + # - platform: "ubuntu-20.04" + # args: "--target x86_64-unknown-linux-gnu" + # arch: "x86_64" + # target: "x86_64-unknown-linux-gnu" + # pkg_target: "latest-linux-x64" + # nodecar_script: "build:linux-x64" + # - platform: "ubuntu-20.04" + # args: "--target aarch64-unknown-linux-gnu" + # arch: "aarch64" + # target: "aarch64-unknown-linux-gnu" + # pkg_target: "latest-linux-arm64" + # nodecar_script: "build:linux-arm64" + # - platform: "windows-latest" + # args: "--target x86_64-pc-windows-msvc" + # arch: "x86_64" + # target: "x86_64-pc-windows-msvc" + # pkg_target: "latest-win-x64" + # nodecar_script: "build:win-x64" + # - platform: "windows-latest" + # args: "--target aarch64-pc-windows-msvc" + # arch: "aarch64" + # target: "aarch64-pc-windows-msvc" + # pkg_target: "latest-win-arm64" + # nodecar_script: "build:win-arm64" + + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: latest + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install dependencies (Ubuntu only) + if: matrix.platform == 'ubuntu-20.04' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workdir: ./src-tauri + + - name: Install frontend dependencies + run: pnpm install --frozen-lockfile + + - name: Install nodecar dependencies + run: | + cd nodecar + pnpm install --frozen-lockfile + + - name: Build nodecar sidecar + run: | + cd nodecar + pnpm run ${{ matrix.nodecar_script }} + + - name: Copy nodecar binary to Tauri binaries + run: | + mkdir -p src-tauri/binaries + if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then + cp nodecar/dist/nodecar.exe src-tauri/binaries/nodecar-${{ matrix.target }}.exe + else + cp nodecar/dist/nodecar src-tauri/binaries/nodecar-${{ matrix.target }} + fi + + - name: Build frontend + run: pnpm build + + - name: Build Tauri app + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tagName: ${{ github.ref_name }} + releaseName: "Donut Browser ${{ github.ref_name }}" + releaseBody: "See the assets to download this version and install." + releaseDraft: true + prerelease: false + args: ${{ matrix.args }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e07fab4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ +/dist/ + +# production +/build + +# misc +**/*.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# nodecar +nodecar/dist +nodecar/node_modules + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo + +!**/.gitkeep \ No newline at end of file diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..eb08e72 --- /dev/null +++ b/.node-version @@ -0,0 +1,2 @@ +22 + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cfa1d38 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "cSpell.words": [ + "autologin", + "CFURL", + "donutbrowser", + "launchservices", + "mountpoint", + "nodecar", + "ntlm", + "propertylist", + "rlib", + "serde", + "signon", + "sspi", + "staticlib", + "sysinfo", + "systempreferences" + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..92e8940 --- /dev/null +++ b/LICENSE @@ -0,0 +1,660 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2025 zhom@github +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + +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. + +Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +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 AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..53ec24e --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Donut Browser + +TODO diff --git a/assets/preview.png b/assets/preview.png new file mode 100644 index 0000000..36accf5 Binary files /dev/null and b/assets/preview.png differ diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..c7952bb --- /dev/null +++ b/biome.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "useHookAtTopLevel": "error" + }, + "nursery": { + "useGoogleFontDisplay": "error", + "noDocumentImportInPage": "error", + "noHeadElement": "error", + "noHeadImportInDocument": "error", + "noImgElement": "off", + "useComponentExportOnlyModules": { + "level": "error", + "options": { + "allowExportNames": ["metadata"] + } + } + } + } + }, + "css": { + "formatter": { + "quoteStyle": "double" + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + }, + "globals": [] + } +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..a450d2f --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..5e3bedb --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,133 @@ +import { FlatCompat } from "@eslint/eslintrc"; +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +const compat = new FlatCompat({ + baseDirectory: import.meta.dirname, +}); + +const eslintConfig = tseslint.config( + eslint.configs.recommended, + tseslint.configs.strictTypeChecked, + tseslint.configs.stylisticTypeChecked, + ...compat.extends("next/core-web-vitals"), + { + // Disabled rules taken from https://biomejs.dev/linter/rules-sources for ones that + // are already handled by Biome + rules: { + // eslint-plugin-jsx-a11y rules replaced by Biome + "jsx-a11y/alt-text": "off", + "jsx-a11y/anchor-has-content": "off", + "jsx-a11y/anchor-is-valid": "off", + "jsx-a11y/aria-activedescendant-has-tabindex": "off", + "jsx-a11y/aria-props": "off", + "jsx-a11y/aria-proptypes": "off", + "jsx-a11y/aria-role": "off", + "jsx-a11y/aria-unsupported-elements": "off", + "jsx-a11y/autocomplete-valid": "off", + "jsx-a11y/click-events-have-key-events": "off", + "jsx-a11y/heading-has-content": "off", + "jsx-a11y/html-has-lang": "off", + "jsx-a11y/iframe-has-title": "off", + "jsx-a11y/img-redundant-alt": "off", + "jsx-a11y/interactive-supports-focus": "off", + "jsx-a11y/label-has-associated-control": "off", + "jsx-a11y/lang": "off", + "jsx-a11y/media-has-caption": "off", + "jsx-a11y/mouse-events-have-key-events": "off", + "jsx-a11y/no-access-key": "off", + "jsx-a11y/no-aria-hidden-on-focusable": "off", + "jsx-a11y/no-autofocus": "off", + "jsx-a11y/no-distracting-elements": "off", + "jsx-a11y/no-interactive-element-to-noninteractive-role": "off", + "jsx-a11y/no-noninteractive-element-to-interactive-role": "off", + "jsx-a11y/no-noninteractive-tabindex": "off", + "jsx-a11y/no-redundant-roles": "off", + "jsx-a11y/no-static-element-interactions": "off", + "jsx-a11y/prefer-tag-over-role": "off", + "jsx-a11y/role-has-required-aria-props": "off", + "jsx-a11y/role-supports-aria-props": "off", + "jsx-a11y/scope": "off", + "jsx-a11y/tabindex-no-positive": "off", + // eslint-plugin-react rules replaced by Biome + "react/button-has-type": "off", + "react/jsx-boolean-value": "off", + "react/jsx-curly-brace-presence": "off", + "react/jsx-fragments": "off", + "react/jsx-key": "off", + "react/jsx-no-comment-textnodes": "off", + "react/jsx-no-duplicate-props": "off", + "react/jsx-no-target-blank": "off", + "react/jsx-no-useless-fragment": "off", + "react/no-array-index-key": "off", + "react/no-children-prop": "off", + "react/no-danger": "off", + "react/no-danger-with-children": "off", + "react/void-dom-elements-no-children": "off", + // eslint-plugin-react-hooks rules replaced by Biome + "react-hooks/exhaustive-deps": "off", + "react-hooks/rules-of-hooks": "off", + // typescript-eslint rules replaced by Biome + "@typescript-eslint/adjacent-overload-signatures": "off", + "@typescript-eslint/array-type": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/consistent-type-exports": "off", + "@typescript-eslint/consistent-type-imports": "off", + "@typescript-eslint/default-param-last": "off", + "@typescript-eslint/dot-notation": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-member-accessibility": "off", + "@typescript-eslint/naming-convention": "off", + "@typescript-eslint/no-dupe-class-members": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-extra-non-null-assertion": "off", + "@typescript-eslint/no-extraneous-class": "off", + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/no-invalid-void-type": "off", + "@typescript-eslint/no-loss-of-precision": "off", + "@typescript-eslint/no-misused-new": "off", + "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-redeclare": "off", + "@typescript-eslint/no-require-imports": "off", + "@typescript-eslint/no-restricted-imports": "off", + "@typescript-eslint/no-restricted-types": "off", + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-unnecessary-type-constraint": "off", + "@typescript-eslint/no-unsafe-declaration-merging": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-useless-constructor": "off", + "@typescript-eslint/no-useless-empty-export": "off", + "@typescript-eslint/only-throw-error": "off", + "@typescript-eslint/parameter-properties": "off", + "@typescript-eslint/prefer-as-const": "off", + "@typescript-eslint/prefer-enum-initializers": "off", + "@typescript-eslint/prefer-for-of": "off", + "@typescript-eslint/prefer-function-type": "off", + "@typescript-eslint/prefer-literal-enum-member": "off", + "@typescript-eslint/prefer-namespace-keyword": "off", + "@typescript-eslint/prefer-optional-chain": "off", + "@typescript-eslint/require-await": "off", + // Custom rules + "@typescript-eslint/restrict-template-expressions": [ + "error", + { + allowNumber: true, + allowBoolean: true, + allowNever: true, + }, + ], + }, + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +); + +export default eslintConfig; diff --git a/index.html b/index.html new file mode 100644 index 0000000..2dc612a --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + Tauri + React + Typescript + + + +
+ + + diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..1b3be08 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..68dbb10 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,12 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + reactStrictMode: true, + output: "export", + images: { + unoptimized: true, + }, + distDir: "dist", +}; + +export default nextConfig; diff --git a/nodecar/eslint.config.mjs b/nodecar/eslint.config.mjs new file mode 100644 index 0000000..ad226f2 --- /dev/null +++ b/nodecar/eslint.config.mjs @@ -0,0 +1,131 @@ +import { FlatCompat } from "@eslint/eslintrc"; +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +const compat = new FlatCompat({ + baseDirectory: import.meta.dirname, +}); + +const eslintConfig = tseslint.config( + eslint.configs.recommended, + ...compat.extends("next/core-web-vitals"), + { + // Disabled rules taken from https://biomejs.dev/linter/rules-sources for ones that + // are already handled by Biome + rules: { + // eslint-plugin-jsx-a11y rules replaced by Biome + "jsx-a11y/alt-text": "off", + "jsx-a11y/anchor-has-content": "off", + "jsx-a11y/anchor-is-valid": "off", + "jsx-a11y/aria-activedescendant-has-tabindex": "off", + "jsx-a11y/aria-props": "off", + "jsx-a11y/aria-proptypes": "off", + "jsx-a11y/aria-role": "off", + "jsx-a11y/aria-unsupported-elements": "off", + "jsx-a11y/autocomplete-valid": "off", + "jsx-a11y/click-events-have-key-events": "off", + "jsx-a11y/heading-has-content": "off", + "jsx-a11y/html-has-lang": "off", + "jsx-a11y/iframe-has-title": "off", + "jsx-a11y/img-redundant-alt": "off", + "jsx-a11y/interactive-supports-focus": "off", + "jsx-a11y/label-has-associated-control": "off", + "jsx-a11y/lang": "off", + "jsx-a11y/media-has-caption": "off", + "jsx-a11y/mouse-events-have-key-events": "off", + "jsx-a11y/no-access-key": "off", + "jsx-a11y/no-aria-hidden-on-focusable": "off", + "jsx-a11y/no-autofocus": "off", + "jsx-a11y/no-distracting-elements": "off", + "jsx-a11y/no-interactive-element-to-noninteractive-role": "off", + "jsx-a11y/no-noninteractive-element-to-interactive-role": "off", + "jsx-a11y/no-noninteractive-tabindex": "off", + "jsx-a11y/no-redundant-roles": "off", + "jsx-a11y/no-static-element-interactions": "off", + "jsx-a11y/prefer-tag-over-role": "off", + "jsx-a11y/role-has-required-aria-props": "off", + "jsx-a11y/role-supports-aria-props": "off", + "jsx-a11y/scope": "off", + "jsx-a11y/tabindex-no-positive": "off", + // eslint-plugin-react rules replaced by Biome + "react/button-has-type": "off", + "react/jsx-boolean-value": "off", + "react/jsx-curly-brace-presence": "off", + "react/jsx-fragments": "off", + "react/jsx-key": "off", + "react/jsx-no-comment-textnodes": "off", + "react/jsx-no-duplicate-props": "off", + "react/jsx-no-target-blank": "off", + "react/jsx-no-useless-fragment": "off", + "react/no-array-index-key": "off", + "react/no-children-prop": "off", + "react/no-danger": "off", + "react/no-danger-with-children": "off", + "react/void-dom-elements-no-children": "off", + // eslint-plugin-react-hooks rules replaced by Biome + "react-hooks/exhaustive-deps": "off", + "react-hooks/rules-of-hooks": "off", + // typescript-eslint rules replaced by Biome + "@typescript-eslint/adjacent-overload-signatures": "off", + "@typescript-eslint/array-type": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/consistent-type-exports": "off", + "@typescript-eslint/consistent-type-imports": "off", + "@typescript-eslint/default-param-last": "off", + "@typescript-eslint/dot-notation": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-member-accessibility": "off", + "@typescript-eslint/naming-convention": "off", + "@typescript-eslint/no-dupe-class-members": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-extra-non-null-assertion": "off", + "@typescript-eslint/no-extraneous-class": "off", + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/no-invalid-void-type": "off", + "@typescript-eslint/no-loss-of-precision": "off", + "@typescript-eslint/no-misused-new": "off", + "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-redeclare": "off", + "@typescript-eslint/no-require-imports": "off", + "@typescript-eslint/no-restricted-imports": "off", + "@typescript-eslint/no-restricted-types": "off", + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-unnecessary-type-constraint": "off", + "@typescript-eslint/no-unsafe-declaration-merging": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-useless-constructor": "off", + "@typescript-eslint/no-useless-empty-export": "off", + "@typescript-eslint/only-throw-error": "off", + "@typescript-eslint/parameter-properties": "off", + "@typescript-eslint/prefer-as-const": "off", + "@typescript-eslint/prefer-enum-initializers": "off", + "@typescript-eslint/prefer-for-of": "off", + "@typescript-eslint/prefer-function-type": "off", + "@typescript-eslint/prefer-literal-enum-member": "off", + "@typescript-eslint/prefer-namespace-keyword": "off", + "@typescript-eslint/prefer-optional-chain": "off", + "@typescript-eslint/require-await": "off", + // Custom rules + "@typescript-eslint/restrict-template-expressions": [ + "error", + { + allowNumber: true, + allowBoolean: true, + allowNever: true, + }, + ], + }, + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + } +); + +export default eslintConfig; diff --git a/nodecar/package.json b/nodecar/package.json new file mode 100644 index 0000000..5498452 --- /dev/null +++ b/nodecar/package.json @@ -0,0 +1,33 @@ +{ + "name": "nodecar", + "version": "1.0.0", + "description": "", + "main": "src/index.ts", + "scripts": { + "watch": "nodemon --exec ts-node --esm ./src/index.ts --watch src", + "dev": "node --loader ts-node/esm ./src/index.ts", + "start": "node --loader ts-node/esm ./src/index.ts", + "build": "tsc && pkg ./dist/index.js --targets latest-macos-arm64 --output dist/nodecar", + "build:aarch64": "tsc && pkg ./dist/index.js --targets latest-macos-arm64 --output dist/nodecar", + "build:x86_64": "tsc && pkg ./dist/index.js --targets latest-macos-x64 --output dist/nodecar", + "build:linux-x64": "tsc && pkg ./dist/index.js --targets latest-linux-x64 --output dist/nodecar", + "build:linux-arm64": "tsc && pkg ./dist/index.js --targets latest-linux-arm64 --output dist/nodecar", + "build:win-x64": "tsc && pkg ./dist/index.js --targets latest-win-x64 --output dist/nodecar", + "build:win-arm64": "tsc && pkg ./dist/index.js --targets latest-win-arm64 --output dist/nodecar" + }, + "keywords": [], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.6.1", + "dependencies": { + "@types/node": "^22.15.17", + "@yao-pkg/pkg": "^6.4.1", + "commander": "^13.1.0", + "dotenv": "^16.5.0", + "get-port": "^7.1.0", + "nodemon": "^3.1.10", + "proxy-chain": "^2.5.8", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + } +} diff --git a/nodecar/pnpm-lock.yaml b/nodecar/pnpm-lock.yaml new file mode 100644 index 0000000..2995e9b --- /dev/null +++ b/nodecar/pnpm-lock.yaml @@ -0,0 +1,1304 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@types/node': + specifier: ^22.15.17 + version: 22.15.17 + '@yao-pkg/pkg': + specifier: ^6.4.1 + version: 6.4.1 + commander: + specifier: ^13.1.0 + version: 13.1.0 + dotenv: + specifier: ^16.5.0 + version: 16.5.0 + get-port: + specifier: ^7.1.0 + version: 7.1.0 + nodemon: + specifier: ^3.1.10 + version: 3.1.10 + proxy-chain: + specifier: ^2.5.8 + version: 2.5.8 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.15.17)(typescript@5.8.3) + typescript: + specifier: ^5.8.3 + version: 5.8.3 + +packages: + + '@babel/generator@7.27.1': + resolution: {integrity: sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.2': + resolution: {integrity: sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.27.1': + resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} + engines: {node: '>=6.9.0'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/node@22.15.17': + resolution: {integrity: sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==} + + '@yao-pkg/pkg-fetch@3.5.21': + resolution: {integrity: sha512-nlJ+rXersw70CQVSph7OfIN8lN6nCStjU7koXzh0WXiPvztZGqkoQTScHQCe1K8/tuKpeL0bEOYW0rP4QqMJ9A==} + hasBin: true + + '@yao-pkg/pkg@6.4.1': + resolution: {integrity: sha512-pjePVt+DQP+HaJI5DfEZDX1pGsMMFjv1wuqfy/BwXlnffVIRk8lXjw7yVYvLQRcomf8Eaz2chDE5B6gR2SSaQw==} + engines: {node: '>=18.0.0'} + hasBin: true + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + dotenv@16.5.0: + resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + engines: {node: '>=12'} + + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + fdir@6.4.4: + resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + from2@2.3.0: + resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-port@7.1.0: + resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} + engines: {node: '>=16'} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + into-stream@6.0.0: + resolution: {integrity: sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==} + engines: {node: '>=10'} + + ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multistream@4.1.0: + resolution: {integrity: sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==} + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + node-abi@3.75.0: + resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==} + engines: {node: '>=10'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + nodemon@3.1.10: + resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==} + engines: {node: '>=10'} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + p-is-promise@3.0.0: + resolution: {integrity: sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + proxy-chain@2.5.8: + resolution: {integrity: sha512-TqKOYRD/1Gga/JhiwmdYHJoj0zMJkKGofQ9bHQuSm+vexczatt81fkUHTVMyci+2mWczXiTNv1Eom+2v3Da5og==} + engines: {node: '>=14'} + + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.4: + resolution: {integrity: sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + stream-meter@1.0.4: + resolution: {integrity: sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tar-fs@2.1.2: + resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + + tinyglobby@0.2.13: + resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unzipper@0.12.3: + resolution: {integrity: sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + +snapshots: + + '@babel/generator@7.27.1': + dependencies: + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/parser@7.27.2': + dependencies: + '@babel/types': 7.27.1 + + '@babel/types@7.27.1': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/node@22.15.17': + dependencies: + undici-types: 6.21.0 + + '@yao-pkg/pkg-fetch@3.5.21': + dependencies: + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + picocolors: 1.1.1 + progress: 2.0.3 + semver: 7.7.2 + tar-fs: 2.1.2 + yargs: 16.2.0 + transitivePeerDependencies: + - encoding + - supports-color + + '@yao-pkg/pkg@6.4.1': + dependencies: + '@babel/generator': 7.27.1 + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 + '@yao-pkg/pkg-fetch': 3.5.21 + into-stream: 6.0.0 + minimist: 1.2.8 + multistream: 4.1.0 + picocolors: 1.1.1 + picomatch: 4.0.2 + prebuild-install: 7.1.3 + resolve: 1.22.10 + stream-meter: 1.0.4 + tar: 7.4.3 + tinyglobby: 0.2.13 + unzipper: 0.12.3 + transitivePeerDependencies: + - encoding + - supports-color + + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.1 + + acorn@8.14.1: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + agent-base@7.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@4.1.3: {} + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + binary-extensions@2.3.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bluebird@3.7.2: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@1.1.4: {} + + chownr@3.0.0: {} + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@13.1.0: {} + + concat-map@0.0.1: {} + + core-util-is@1.0.3: {} + + create-require@1.1.1: {} + + debug@4.4.0(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + detect-libc@2.0.4: {} + + diff@4.0.2: {} + + dotenv@16.5.0: {} + + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + + emoji-regex@8.0.0: {} + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + escalade@3.2.0: {} + + expand-template@2.0.3: {} + + fdir@6.4.4(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + from2@2.3.0: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + + fs-constants@1.0.0: {} + + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-port@7.1.0: {} + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + graceful-fs@4.2.11: {} + + has-flag@3.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + ieee754@1.2.1: {} + + ignore-by-default@1.0.1: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + into-stream@6.0.0: + dependencies: + from2: 2.3.0 + p-is-promise: 3.0.0 + + ip-address@9.0.5: + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isarray@1.0.0: {} + + jsbn@1.1.0: {} + + jsesc@3.1.0: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + make-error@1.3.6: {} + + mimic-response@3.1.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp-classic@0.5.3: {} + + mkdirp@3.0.1: {} + + ms@2.1.3: {} + + multistream@4.1.0: + dependencies: + once: 1.4.0 + readable-stream: 3.6.2 + + napi-build-utils@2.0.0: {} + + node-abi@3.75.0: + dependencies: + semver: 7.7.2 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-int64@0.4.0: {} + + nodemon@3.1.10: + dependencies: + chokidar: 3.6.0 + debug: 4.4.0(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.7.2 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + normalize-path@3.0.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + p-is-promise@3.0.0: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.0.4 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.75.0 + pump: 3.0.2 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.2 + tunnel-agent: 0.6.0 + + process-nextick-args@2.0.1: {} + + progress@2.0.3: {} + + proxy-chain@2.5.8: + dependencies: + socks: 2.8.4 + socks-proxy-agent: 8.0.5 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + pstree.remy@1.1.8: {} + + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + require-directory@2.1.1: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + semver@7.7.2: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.2 + + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0(supports-color@5.5.0) + socks: 2.8.4 + transitivePeerDependencies: + - supports-color + + socks@2.8.4: + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + + sprintf-js@1.1.3: {} + + stream-meter@1.0.4: + dependencies: + readable-stream: 2.3.8 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@2.0.1: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tar-fs@2.1.2: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.2 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + + tinyglobby@0.2.13: + dependencies: + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + touch@3.1.1: {} + + tr46@0.0.3: {} + + ts-node@10.9.2(@types/node@22.15.17)(typescript@5.8.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.15.17 + acorn: 8.14.1 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.8.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + typescript@5.8.3: {} + + undefsafe@2.0.5: {} + + undici-types@6.21.0: {} + + universalify@2.0.1: {} + + unzipper@0.12.3: + dependencies: + bluebird: 3.7.2 + duplexer2: 0.1.4 + fs-extra: 11.3.0 + graceful-fs: 4.2.11 + node-int64: 0.4.0 + + util-deprecate@1.0.2: {} + + v8-compile-cache-lib@3.0.1: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + y18n@5.0.8: {} + + yallist@5.0.0: {} + + yargs-parser@20.2.9: {} + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yn@3.1.1: {} diff --git a/nodecar/rename-binary.js b/nodecar/rename-binary.js new file mode 100644 index 0000000..4edd193 --- /dev/null +++ b/nodecar/rename-binary.js @@ -0,0 +1,14 @@ +import { execSync } from "child_process"; +import fs from "fs"; + +const ext = process.platform === "win32" ? ".exe" : ""; + +const rustInfo = execSync("rustc -vV"); +const targetTriple = /host: (\S+)/g.exec(rustInfo)[1]; +if (!targetTriple) { + console.error("Failed to determine platform target triple"); +} +fs.renameSync( + `dist/nodecar${ext}`, + `../src-tauri/binaries/nodecar-${targetTriple}${ext}` +); diff --git a/nodecar/src/index.ts b/nodecar/src/index.ts new file mode 100644 index 0000000..9d63750 --- /dev/null +++ b/nodecar/src/index.ts @@ -0,0 +1,94 @@ +import { program } from "commander"; +import { + startProxyProcess, + stopProxyProcess, + stopAllProxyProcesses, +} from "./proxy-runner"; +import { listProxyConfigs } from "./proxy-storage"; +import { runProxyWorker } from "./proxy-worker"; + +// Command for proxy management +program + .command("proxy") + .argument("", "start, stop, or list proxies") + .option( + "-u, --upstream ", + "upstream proxy URL (protocol://[username:password@]host:port)" + ) + .option( + "-p, --port ", + "local port to use (random if not specified)", + parseInt + ) + .option("--ignore-certificate", "ignore certificate errors for HTTPS proxies") + .option("--id ", "proxy ID for stop command") + .description("manage proxy servers") + .action(async (action, options) => { + if (action === "start") { + if (!options.upstream) { + console.error("Error: Upstream proxy URL is required"); + console.log( + "Example: proxy start -u http://username:password@proxy.example.com:8080" + ); + return; + } + + try { + const config = await startProxyProcess(options.upstream, { + port: options.port, + ignoreProxyCertificate: options.ignoreCertificate, + }); + console.log(JSON.stringify(config)); + } catch (error: any) { + console.error(`Failed to start proxy: ${error.message}`); + } + } else if (action === "stop") { + if (options.id) { + const stopped = await stopProxyProcess(options.id); + console.log(`{ + "success": ${stopped}}`); + } else if (options.upstream) { + // Find proxies with this upstream URL + const configs = listProxyConfigs().filter( + (config) => config.upstreamUrl === options.upstream + ); + + if (configs.length === 0) { + console.error(`No proxies found for ${options.upstream}`); + return; + } + + for (const config of configs) { + const stopped = await stopProxyProcess(config.id); + console.log(`{ + "success": ${stopped}}`); + } + } else { + await stopAllProxyProcesses(); + console.log(`{ + "success": true}`); + } + } else if (action === "list") { + const configs = listProxyConfigs(); + console.log(JSON.stringify(configs)); + } else { + console.error("Invalid action. Use 'start', 'stop', or 'list'"); + } + }); + +// Command for proxy worker (internal use) +program + .command("proxy-worker") + .argument("", "start a proxy worker") + .requiredOption("--id ", "proxy configuration ID") + .description("run a proxy worker process") + .action(async (action, options) => { + if (action === "start") { + await runProxyWorker(options.id); + } else { + console.error("Invalid action for proxy-worker. Use 'start'"); + process.exit(1); + } + }); + +program.parse(); diff --git a/nodecar/src/proxy-runner.ts b/nodecar/src/proxy-runner.ts new file mode 100644 index 0000000..57b0b46 --- /dev/null +++ b/nodecar/src/proxy-runner.ts @@ -0,0 +1,111 @@ +import { spawn } from "child_process"; +import path from "path"; +import getPort from "get-port"; +import { + ProxyConfig, + saveProxyConfig, + getProxyConfig, + deleteProxyConfig, + isProcessRunning, + generateProxyId, +} from "./proxy-storage"; + +/** + * Start a proxy in a separate process + * @param upstreamUrl The upstream proxy URL + * @param options Optional configuration + * @returns Promise resolving to the proxy configuration + */ +export async function startProxyProcess( + upstreamUrl: string, + options: { port?: number; ignoreProxyCertificate?: boolean } = {} +): Promise { + // Generate a unique ID for this proxy + const id = generateProxyId(); + + // Get a random available port if not specified + const port = options.port || (await getPort()); + + // Create the proxy configuration + const config: ProxyConfig = { + id, + upstreamUrl, + localPort: port, + ignoreProxyCertificate: options.ignoreProxyCertificate || false, + }; + + // Save the configuration before starting the process + saveProxyConfig(config); + + // Build the command arguments + const args = ["proxy-worker", "start", "--id", id]; + + // Spawn the process + const child = spawn( + process.execPath, + [path.join(__dirname, "index.js"), ...args], + { + detached: true, + stdio: "ignore", + } + ); + + // Unref the child to allow the parent to exit independently + child.unref(); + + // Store the process ID + config.pid = child.pid; + config.localUrl = `http://localhost:${port}`; + + // Update the configuration with the process ID + saveProxyConfig(config); + + // Wait a bit to ensure the proxy has started + await new Promise((resolve) => setTimeout(resolve, 500)); + + return config; +} + +/** + * Stop a proxy process + * @param id The proxy ID to stop + * @returns Promise resolving to true if stopped, false if not found + */ +export async function stopProxyProcess(id: string): Promise { + const config = getProxyConfig(id); + + if (!config || !config.pid) { + return false; + } + + try { + // Check if the process is running + if (isProcessRunning(config.pid)) { + // Send SIGTERM to the process + process.kill(config.pid); + + // Wait a bit to ensure the process has terminated + await new Promise((resolve) => setTimeout(resolve, 300)); + } + + // Delete the configuration + deleteProxyConfig(id); + + return true; + } catch (error) { + console.error(`Error stopping proxy ${id}:`, error); + return false; + } +} + +/** + * Stop all proxy processes + * @returns Promise resolving when all proxies are stopped + */ +export async function stopAllProxyProcesses(): Promise { + const configs = require("./proxy-storage").listProxyConfigs(); + + for (const config of configs) { + await stopProxyProcess(config.id); + } +} diff --git a/nodecar/src/proxy-storage.ts b/nodecar/src/proxy-storage.ts new file mode 100644 index 0000000..117d081 --- /dev/null +++ b/nodecar/src/proxy-storage.ts @@ -0,0 +1,149 @@ +import fs from "fs"; +import path from "path"; +import os from "os"; + +// Define the proxy configuration type +export interface ProxyConfig { + id: string; + upstreamUrl: string; + localPort?: number; + ignoreProxyCertificate?: boolean; + localUrl?: string; + pid?: number; +} + +// Path to store proxy configurations +const STORAGE_DIR = path.join(os.tmpdir(), "donutbrowser", "proxies"); + +// Ensure storage directory exists +if (!fs.existsSync(STORAGE_DIR)) { + fs.mkdirSync(STORAGE_DIR, { recursive: true }); +} + +/** + * Save a proxy configuration to disk + * @param config The proxy configuration to save + */ +export function saveProxyConfig(config: ProxyConfig): void { + const filePath = path.join(STORAGE_DIR, `${config.id}.json`); + fs.writeFileSync(filePath, JSON.stringify(config, null, 2)); +} + +/** + * Get a proxy configuration by ID + * @param id The proxy ID + * @returns The proxy configuration or null if not found + */ +export function getProxyConfig(id: string): ProxyConfig | null { + const filePath = path.join(STORAGE_DIR, `${id}.json`); + + if (!fs.existsSync(filePath)) { + return null; + } + + try { + const content = fs.readFileSync(filePath, "utf-8"); + return JSON.parse(content) as ProxyConfig; + } catch (error) { + console.error(`Error reading proxy config ${id}:`, error); + return null; + } +} + +/** + * Delete a proxy configuration + * @param id The proxy ID to delete + * @returns True if deleted, false if not found + */ +export function deleteProxyConfig(id: string): boolean { + const filePath = path.join(STORAGE_DIR, `${id}.json`); + + if (!fs.existsSync(filePath)) { + return false; + } + + try { + fs.unlinkSync(filePath); + return true; + } catch (error) { + console.error(`Error deleting proxy config ${id}:`, error); + return false; + } +} + +/** + * List all saved proxy configurations + * @returns Array of proxy configurations + */ +export function listProxyConfigs(): ProxyConfig[] { + if (!fs.existsSync(STORAGE_DIR)) { + return []; + } + + try { + return fs + .readdirSync(STORAGE_DIR) + .filter((file) => file.endsWith(".json")) + .map((file) => { + try { + const content = fs.readFileSync( + path.join(STORAGE_DIR, file), + "utf-8" + ); + return JSON.parse(content) as ProxyConfig; + } catch (error) { + console.error(`Error reading proxy config ${file}:`, error); + return null; + } + }) + .filter((config): config is ProxyConfig => config !== null); + } catch (error) { + console.error("Error listing proxy configs:", error); + return []; + } +} + +/** + * Update a proxy configuration + * @param config The proxy configuration to update + * @returns True if updated, false if not found + */ +export function updateProxyConfig(config: ProxyConfig): boolean { + const filePath = path.join(STORAGE_DIR, `${config.id}.json`); + + if (!fs.existsSync(filePath)) { + return false; + } + + try { + fs.writeFileSync(filePath, JSON.stringify(config, null, 2)); + return true; + } catch (error) { + console.error(`Error updating proxy config ${config.id}:`, error); + return false; + } +} + +/** + * Check if a proxy process is running + * @param pid The process ID to check + * @returns True if running, false otherwise + */ +export function isProcessRunning(pid: number): boolean { + try { + // The kill method with signal 0 doesn't actually kill the process + // but checks if it exists + process.kill(pid, 0); + return true; + } catch (error) { + return false; + } +} + +/** + * Generate a unique ID for a proxy + * @returns A unique ID string + */ +export function generateProxyId(): string { + return `proxy_${Date.now()}_${Math.floor(Math.random() * 10000)}`; +} diff --git a/nodecar/src/proxy-worker.ts b/nodecar/src/proxy-worker.ts new file mode 100644 index 0000000..32bb7d7 --- /dev/null +++ b/nodecar/src/proxy-worker.ts @@ -0,0 +1,51 @@ +import { Server } from "proxy-chain"; +import { getProxyConfig } from "./proxy-storage"; + +/** + * Run a proxy server as a worker process + * @param id The proxy configuration ID + */ +export async function runProxyWorker(id: string): Promise { + // Get the proxy configuration + const config = getProxyConfig(id); + + if (!config) { + console.error(`Proxy configuration ${id} not found`); + process.exit(1); + } + + // Create a new proxy server + const server = new Server({ + port: config.localPort, + host: "localhost", + prepareRequestFunction: () => { + return { + upstreamProxyUrl: config.upstreamUrl, + ignoreUpstreamProxyCertificate: config.ignoreProxyCertificate || false, + }; + }, + }); + + // Handle process termination + process.on("SIGTERM", async () => { + console.log(`Proxy worker ${id} received SIGTERM, shutting down...`); + await server.close(true); + process.exit(0); + }); + + process.on("SIGINT", async () => { + console.log(`Proxy worker ${id} received SIGINT, shutting down...`); + await server.close(true); + process.exit(0); + }); + + // Start the server + try { + await server.listen(); + console.log(`Proxy worker ${id} started on port ${server.port}`); + console.log(`Forwarding to upstream proxy: ${config.upstreamUrl}`); + } catch (error) { + console.error(`Failed to start proxy worker ${id}:`, error); + process.exit(1); + } +} \ No newline at end of file diff --git a/nodecar/src/proxy.ts b/nodecar/src/proxy.ts new file mode 100644 index 0000000..b05dc8b --- /dev/null +++ b/nodecar/src/proxy.ts @@ -0,0 +1,73 @@ +import { + startProxyProcess, + stopProxyProcess, + stopAllProxyProcesses +} from "./proxy-runner"; +import { listProxyConfigs } from "./proxy-storage"; + +// Type definitions +interface ProxyOptions { + port?: number; + ignoreProxyCertificate?: boolean; +} + +/** + * Start a local proxy server that forwards to an upstream proxy + * @param upstreamProxyUrl The upstream proxy URL (protocol://[username:password@]host:port) + * @param options Optional configuration + * @returns Promise resolving to the local proxy URL + */ +export async function startProxy( + upstreamProxyUrl: string, + options: ProxyOptions = {} +): Promise { + const config = await startProxyProcess(upstreamProxyUrl, { + port: options.port, + ignoreProxyCertificate: options.ignoreProxyCertificate, + }); + + return config.localUrl || `http://localhost:${config.localPort}`; +} + +/** + * Stop a specific proxy by its upstream URL + * @param upstreamProxyUrl The upstream proxy URL to stop + * @returns Promise resolving to true if proxy was found and stopped, false otherwise + */ +export async function stopProxy(upstreamProxyUrl: string): Promise { + // Find all proxies with this upstream URL + const configs = listProxyConfigs().filter( + config => config.upstreamUrl === upstreamProxyUrl + ); + + if (configs.length === 0) { + return false; + } + + // Stop all matching proxies + let success = true; + for (const config of configs) { + const stopped = await stopProxyProcess(config.id); + if (!stopped) { + success = false; + } + } + + return success; +} + +/** + * Get a list of all active proxy upstream URLs + * @returns Array of upstream proxy URLs + */ +export function getActiveProxies(): string[] { + return listProxyConfigs().map(config => config.upstreamUrl); +} + +/** + * Stop all active proxies + * @returns Promise that resolves when all proxies are stopped + */ +export async function stopAllProxies(): Promise { + await stopAllProxyProcesses(); +} diff --git a/nodecar/tsconfig.json b/nodecar/tsconfig.json new file mode 100644 index 0000000..4f86f47 --- /dev/null +++ b/nodecar/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "CommonJS", + "lib": ["dom", "es6", "es2017", "esnext.asynciterable"], + "sourceMap": false, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "types": ["node"], + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "baseUrl": ".", + "allowSyntheticDefaultImports": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "removeComments": true + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..160dfe2 --- /dev/null +++ b/package.json @@ -0,0 +1,62 @@ +{ + "name": "donutbrowser", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "biome check src/ && next lint", + "tauri": "tauri", + "shadcn:add": "pnpm dlx shadcn@latest add" + }, + "dependencies": { + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-table": "^8.21.3", + "@tauri-apps/api": "^2.5.0", + "@tauri-apps/plugin-fs": "~2.3.0", + "@tauri-apps/plugin-opener": "^2.2.7", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "next": "^15.3.2", + "next-themes": "^0.4.6", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-icons": "^5.5.0", + "sonner": "^2.0.3", + "tailwind-merge": "^3.3.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.27.0", + "@next/eslint-plugin-next": "^15.3.2", + "@tailwindcss/postcss": "^4.1.7", + "@tauri-apps/cli": "^2.5.0", + "@types/node": "^22.15.21", + "@types/react": "^19.1.5", + "@types/react-dom": "^19.1.5", + "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/parser": "^8.32.1", + "@vitejs/plugin-react": "^4.5.0", + "eslint": "^9.27.0", + "eslint-config-next": "^15.3.2", + "eslint-plugin-react-hooks": "^5.2.0", + "tailwindcss": "^4.1.7", + "tw-animate-css": "^1.3.0", + "typescript": "~5.8.3", + "typescript-eslint": "^8.32.1" + }, + "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..96ae5c3 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5935 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@radix-ui/react-checkbox': + specifier: ^1.3.2 + version: 1.3.2(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dialog': + specifier: ^1.1.14 + version: 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.15 + version: 2.1.15(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-label': + specifier: ^2.1.7 + version: 2.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-popover': + specifier: ^1.1.14 + version: 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-progress': + specifier: ^1.1.7 + version: 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-scroll-area': + specifier: ^1.2.9 + version: 1.2.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-select': + specifier: ^2.2.5 + version: 2.2.5(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-tooltip': + specifier: ^1.2.7 + version: 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tauri-apps/api': + specifier: ^2.5.0 + version: 2.5.0 + '@tauri-apps/plugin-fs': + specifier: ~2.3.0 + version: 2.3.0 + '@tauri-apps/plugin-opener': + specifier: ^2.2.7 + version: 2.2.7 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + cmdk: + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next: + specifier: ^15.3.2 + version: 15.3.2(@babel/core@7.27.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@19.1.0) + sonner: + specifier: ^2.0.3 + version: 2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + tailwind-merge: + specifier: ^3.3.0 + version: 3.3.0 + devDependencies: + '@biomejs/biome': + specifier: ^1.9.4 + version: 1.9.4 + '@eslint/eslintrc': + specifier: ^3.3.1 + version: 3.3.1 + '@eslint/js': + specifier: ^9.27.0 + version: 9.27.0 + '@next/eslint-plugin-next': + specifier: ^15.3.2 + version: 15.3.2 + '@tailwindcss/postcss': + specifier: ^4.1.7 + version: 4.1.7 + '@tauri-apps/cli': + specifier: ^2.5.0 + version: 2.5.0 + '@types/node': + specifier: ^22.15.21 + version: 22.15.21 + '@types/react': + specifier: ^19.1.5 + version: 19.1.5 + '@types/react-dom': + specifier: ^19.1.5 + version: 19.1.5(@types/react@19.1.5) + '@typescript-eslint/eslint-plugin': + specifier: ^8.32.1 + version: 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.32.1 + version: 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@vitejs/plugin-react': + specifier: ^4.5.0 + version: 4.5.0(vite@6.2.0(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)) + eslint: + specifier: ^9.27.0 + version: 9.27.0(jiti@2.4.2) + eslint-config-next: + specifier: ^15.3.2 + version: 15.3.2(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-react-hooks: + specifier: ^5.2.0 + version: 5.2.0(eslint@9.27.0(jiti@2.4.2)) + tailwindcss: + specifier: ^4.1.7 + version: 4.1.7 + tw-animate-css: + specifier: ^1.3.0 + version: 1.3.0 + typescript: + specifier: ~5.8.3 + version: 5.8.3 + typescript-eslint: + specifier: ^8.32.1 + version: 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.27.2': + resolution: {integrity: sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.27.1': + resolution: {integrity: sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.27.1': + resolution: {integrity: sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.1': + resolution: {integrity: sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.1': + resolution: {integrity: sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.2': + resolution: {integrity: sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.27.1': + resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.1': + resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@emnapi/core@1.4.3': + resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} + + '@emnapi/runtime@1.4.3': + resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + + '@emnapi/wasi-threads@1.0.2': + resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==} + + '@esbuild/aix-ppc64@0.25.4': + resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.4': + resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.4': + resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.4': + resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.4': + resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.4': + resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.4': + resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.4': + resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.4': + resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.4': + resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.4': + resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.4': + resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.4': + resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.4': + resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.4': + resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.4': + resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.4': + resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.4': + resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.4': + resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.4': + resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.4': + resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.4': + resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.4': + resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.4': + resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.4': + resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.20.0': + resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.2.2': + resolution: {integrity: sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.14.0': + resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.27.0': + resolution: {integrity: sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.1': + resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.0': + resolution: {integrity: sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==} + + '@floating-ui/dom@1.7.0': + resolution: {integrity: sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==} + + '@floating-ui/react-dom@2.1.2': + resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.9': + resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/sharp-darwin-arm64@0.34.2': + resolution: {integrity: sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.2': + resolution: {integrity: sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.1.0': + resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.1.0': + resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.1.0': + resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.1.0': + resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.1.0': + resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.1.0': + resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.1.0': + resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.1.0': + resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.2': + resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.2': + resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.34.2': + resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.2': + resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.2': + resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.2': + resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.2': + resolution: {integrity: sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.2': + resolution: {integrity: sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.2': + resolution: {integrity: sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.2': + resolution: {integrity: sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@napi-rs/wasm-runtime@0.2.10': + resolution: {integrity: sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ==} + + '@next/env@15.3.2': + resolution: {integrity: sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g==} + + '@next/eslint-plugin-next@15.3.2': + resolution: {integrity: sha512-ijVRTXBgnHT33aWnDtmlG+LJD+5vhc9AKTJPquGG5NKXjpKNjc62woIhFtrAcWdBobt8kqjCoaJ0q6sDQoX7aQ==} + + '@next/swc-darwin-arm64@15.3.2': + resolution: {integrity: sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.3.2': + resolution: {integrity: sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.3.2': + resolution: {integrity: sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@15.3.2': + resolution: {integrity: sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@15.3.2': + resolution: {integrity: sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@15.3.2': + resolution: {integrity: sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@15.3.2': + resolution: {integrity: sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.3.2': + resolution: {integrity: sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.2': + resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.2': + resolution: {integrity: sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.14': + resolution: {integrity: sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.10': + resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.15': + resolution: {integrity: sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.2': + resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.15': + resolution: {integrity: sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.14': + resolution: {integrity: sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.7': + resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.4': + resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.10': + resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.9': + resolution: {integrity: sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.5': + resolution: {integrity: sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-tooltip@1.2.7': + resolution: {integrity: sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rolldown/pluginutils@1.0.0-beta.9': + resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==} + + '@rollup/rollup-android-arm-eabi@4.41.0': + resolution: {integrity: sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.41.0': + resolution: {integrity: sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.41.0': + resolution: {integrity: sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.41.0': + resolution: {integrity: sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.41.0': + resolution: {integrity: sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.41.0': + resolution: {integrity: sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.41.0': + resolution: {integrity: sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.41.0': + resolution: {integrity: sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.41.0': + resolution: {integrity: sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.41.0': + resolution: {integrity: sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.41.0': + resolution: {integrity: sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.41.0': + resolution: {integrity: sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.41.0': + resolution: {integrity: sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.41.0': + resolution: {integrity: sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.41.0': + resolution: {integrity: sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.41.0': + resolution: {integrity: sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.41.0': + resolution: {integrity: sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.41.0': + resolution: {integrity: sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.41.0': + resolution: {integrity: sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.41.0': + resolution: {integrity: sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@rushstack/eslint-patch@1.11.0': + resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==} + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.1.7': + resolution: {integrity: sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==} + + '@tailwindcss/oxide-android-arm64@4.1.7': + resolution: {integrity: sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.7': + resolution: {integrity: sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.7': + resolution: {integrity: sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.7': + resolution: {integrity: sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': + resolution: {integrity: sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': + resolution: {integrity: sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.7': + resolution: {integrity: sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.7': + resolution: {integrity: sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.7': + resolution: {integrity: sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.7': + resolution: {integrity: sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': + resolution: {integrity: sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.7': + resolution: {integrity: sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.7': + resolution: {integrity: sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.7': + resolution: {integrity: sha512-88g3qmNZn7jDgrrcp3ZXEQfp9CVox7xjP1HN2TFKI03CltPVd/c61ydn5qJJL8FYunn0OqBaW5HNUga0kmPVvw==} + + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@tauri-apps/api@2.5.0': + resolution: {integrity: sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==} + + '@tauri-apps/cli-darwin-arm64@2.5.0': + resolution: {integrity: sha512-VuVAeTFq86dfpoBDNYAdtQVLbP0+2EKCHIIhkaxjeoPARR0sLpFHz2zs0PcFU76e+KAaxtEtAJAXGNUc8E1PzQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.5.0': + resolution: {integrity: sha512-hUF01sC06cZVa8+I0/VtsHOk9BbO75rd+YdtHJ48xTdcYaQ5QIwL4yZz9OR1AKBTaUYhBam8UX9Pvd5V2/4Dpw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.5.0': + resolution: {integrity: sha512-LQKqttsK252LlqYyX8R02MinUsfFcy3+NZiJwHFgi5Y3+ZUIAED9cSxJkyNtuY5KMnR4RlpgWyLv4P6akN1xhg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.5.0': + resolution: {integrity: sha512-mTQufsPcpdHg5RW0zypazMo4L55EfeE5snTzrPqbLX4yCK2qalN7+rnP8O8GT06xhp6ElSP/Ku1M2MR297SByQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tauri-apps/cli-linux-arm64-musl@2.5.0': + resolution: {integrity: sha512-rQO1HhRUQqyEaal5dUVOQruTRda/TD36s9kv1hTxZiFuSq3558lsTjAcUEnMAtBcBkps20sbyTJNMT0AwYIk8Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tauri-apps/cli-linux-riscv64-gnu@2.5.0': + resolution: {integrity: sha512-7oS18FN46yDxyw1zX/AxhLAd7T3GrLj3Ai6s8hZKd9qFVzrAn36ESL7d3G05s8wEtsJf26qjXnVF4qleS3dYsA==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@tauri-apps/cli-linux-x64-gnu@2.5.0': + resolution: {integrity: sha512-SG5sFNL7VMmDBdIg3nO3EzNRT306HsiEQ0N90ILe3ZABYAVoPDO/ttpCO37ApLInTzrq/DLN+gOlC/mgZvLw1w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tauri-apps/cli-linux-x64-musl@2.5.0': + resolution: {integrity: sha512-QXDM8zp/6v05PNWju5ELsVwF0VH1n6b5pk2E6W/jFbbiwz80Vs1lACl9pv5kEHkrxBj+aWU/03JzGuIj2g3SkQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tauri-apps/cli-win32-arm64-msvc@2.5.0': + resolution: {integrity: sha512-pFSHFK6b+o9y4Un8w0gGLwVyFTZaC3P0kQ7umRt/BLDkzD5RnQ4vBM7CF8BCU5nkwmEBUCZd7Wt3TWZxe41o6Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.5.0': + resolution: {integrity: sha512-EArv1IaRlogdLAQyGlKmEqZqm5RfHCUMhJoedWu7GtdbOMUfSAz6FMX2boE1PtEmNO4An+g188flLeVErrxEKg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.5.0': + resolution: {integrity: sha512-lj43EFYbnAta8pd9JnUq87o+xRUR0odz+4rixBtTUwUgdRdwQ2V9CzFtsMu6FQKpFQ6mujRK6P1IEwhL6ADRsQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.5.0': + resolution: {integrity: sha512-rAtHqG0Gh/IWLjN2zTf3nZqYqbo81oMbqop56rGTjrlWk9pTTAjkqOjSL9XQLIMZ3RbeVjveCqqCA0s8RnLdMg==} + engines: {node: '>= 10'} + hasBin: true + + '@tauri-apps/plugin-fs@2.3.0': + resolution: {integrity: sha512-G9gEyYVUaaxhdRJBgQTTLmzAe0vtHYxYyN1oTQzU3zwvb8T+tVLcAqCdFMWHq0qGeGbmynI5whvYpcXo5LvZ1w==} + + '@tauri-apps/plugin-opener@2.2.7': + resolution: {integrity: sha512-uduEyvOdjpPOEeDRrhwlCspG/f9EQalHumWBtLBnp3fRp++fKGLqDOyUhSIn7PzX45b/rKep//ZQSAQoIxobLA==} + + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@22.15.21': + resolution: {integrity: sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==} + + '@types/react-dom@19.1.5': + resolution: {integrity: sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.5': + resolution: {integrity: sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==} + + '@typescript-eslint/eslint-plugin@8.32.1': + resolution: {integrity: sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/parser@8.32.1': + resolution: {integrity: sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/scope-manager@8.32.1': + resolution: {integrity: sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.32.1': + resolution: {integrity: sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/types@8.32.1': + resolution: {integrity: sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.32.1': + resolution: {integrity: sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/utils@8.32.1': + resolution: {integrity: sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/visitor-keys@8.32.1': + resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-darwin-arm64@1.7.2': + resolution: {integrity: sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.7.2': + resolution: {integrity: sha512-qhVa8ozu92C23Hsmv0BF4+5Dyyd5STT1FolV4whNgbY6mj3kA0qsrGPe35zNR3wAN7eFict3s4Rc2dDTPBTuFQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.7.2': + resolution: {integrity: sha512-zKKdm2uMXqLFX6Ac7K5ElnnG5VIXbDlFWzg4WJ8CGUedJryM5A3cTgHuGMw1+P5ziV8CRhnSEgOnurTI4vpHpg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.7.2': + resolution: {integrity: sha512-8N1z1TbPnHH+iDS/42GJ0bMPLiGK+cUqOhNbMKtWJ4oFGzqSJk/zoXFzcQkgtI63qMcUI7wW1tq2usZQSb2jxw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.7.2': + resolution: {integrity: sha512-tjYzI9LcAXR9MYd9rO45m1s0B/6bJNuZ6jeOxo1pq1K6OBuRMMmfyvJYval3s9FPPGmrldYA3mi4gWDlWuTFGA==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.7.2': + resolution: {integrity: sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.7.2': + resolution: {integrity: sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.7.2': + resolution: {integrity: sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.7.2': + resolution: {integrity: sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.7.2': + resolution: {integrity: sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.7.2': + resolution: {integrity: sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.7.2': + resolution: {integrity: sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.7.2': + resolution: {integrity: sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.7.2': + resolution: {integrity: sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.7.2': + resolution: {integrity: sha512-gtYTh4/VREVSLA+gHrfbWxaMO/00y+34htY7XpioBTy56YN2eBjkPrY1ML1Zys89X3RJDKVaogzwxlM1qU7egg==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.7.2': + resolution: {integrity: sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.7.2': + resolution: {integrity: sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA==} + cpu: [x64] + os: [win32] + + '@vitejs/plugin-react@4.5.0': + resolution: {integrity: sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.10.3: + resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.24.5: + resolution: {integrity: sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001718: + resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.157: + resolution: {integrity: sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} + + es-abstract@1.23.10: + resolution: {integrity: sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.1: + resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild@0.25.4: + resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-next@15.3.2: + resolution: {integrity: sha512-FerU4DYccO4FgeYFFglz0SnaKRe1ejXQrDb8kWUkTAg036YWi+jUsgg4sIGNCDhAsDITsZaL4MzBWKB6f4G1Dg==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.0: + resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.31.0: + resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.3.0: + resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.27.0: + resolution: {integrity: sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.4.4: + resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.4: + resolution: {integrity: sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.2.4: + resolution: {integrity: sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + + next@15.3.2: + resolution: {integrity: sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + + react-icons@5.5.0: + resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + peerDependencies: + react: '*' + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.0: + resolution: {integrity: sha512-sGsQtcjMqdQyijAHytfGEELB8FufGbfXIsvUTe+NLx1GDRJCXtCFLBLUI1eyZCKXXvbEU2C6gai0PZKoIE9Vbg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.41.0: + resolution: {integrity: sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sharp@0.34.2: + resolution: {integrity: sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sonner@2.0.3: + resolution: {integrity: sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwind-merge@3.3.0: + resolution: {integrity: sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==} + + tailwindcss@4.1.7: + resolution: {integrity: sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==} + + tapable@2.2.2: + resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + engines: {node: '>=6'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + + tinyglobby@0.2.13: + resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tw-animate-css@1.3.0: + resolution: {integrity: sha512-jrJ0XenzS9KVuDThJDvnhalbl4IYiMQ/XvpA0a2FL8KmlK+6CSMviO7ROY/I7z1NnUs5NnDhlM6fXmF40xPxzw==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.32.1: + resolution: {integrity: sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unrs-resolver@1.7.2: + resolution: {integrity: sha512-BBKpaylOW8KbHsu378Zky/dGh4ckT/4NW/0SHRABdqRLcQJ2dAOjDo9g97p04sWflm0kqPqpUatxReNV/dqI5A==} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + vite@6.2.0: + resolution: {integrity: sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.27.2': {} + + '@babel/core@7.27.1': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.1 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.1(@babel/core@7.27.1) + '@babel/helpers': 7.27.1 + '@babel/parser': 7.27.2 + '@babel/template': 7.27.2 + '@babel/traverse': 7.27.1 + '@babel/types': 7.27.1 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.27.1': + dependencies: + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.27.2 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.24.5 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.27.1 + '@babel/types': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.1(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.27.1': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.27.1 + + '@babel/parser@7.27.2': + dependencies: + '@babel/types': 7.27.1 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 + + '@babel/traverse@7.27.1': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.1 + '@babel/parser': 7.27.2 + '@babel/template': 7.27.2 + '@babel/types': 7.27.1 + debug: 4.4.1 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.27.1': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true + + '@biomejs/cli-darwin-x64@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64@1.9.4': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true + + '@emnapi/core@1.4.3': + dependencies: + '@emnapi/wasi-threads': 1.0.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.4': + optional: true + + '@esbuild/android-arm64@0.25.4': + optional: true + + '@esbuild/android-arm@0.25.4': + optional: true + + '@esbuild/android-x64@0.25.4': + optional: true + + '@esbuild/darwin-arm64@0.25.4': + optional: true + + '@esbuild/darwin-x64@0.25.4': + optional: true + + '@esbuild/freebsd-arm64@0.25.4': + optional: true + + '@esbuild/freebsd-x64@0.25.4': + optional: true + + '@esbuild/linux-arm64@0.25.4': + optional: true + + '@esbuild/linux-arm@0.25.4': + optional: true + + '@esbuild/linux-ia32@0.25.4': + optional: true + + '@esbuild/linux-loong64@0.25.4': + optional: true + + '@esbuild/linux-mips64el@0.25.4': + optional: true + + '@esbuild/linux-ppc64@0.25.4': + optional: true + + '@esbuild/linux-riscv64@0.25.4': + optional: true + + '@esbuild/linux-s390x@0.25.4': + optional: true + + '@esbuild/linux-x64@0.25.4': + optional: true + + '@esbuild/netbsd-arm64@0.25.4': + optional: true + + '@esbuild/netbsd-x64@0.25.4': + optional: true + + '@esbuild/openbsd-arm64@0.25.4': + optional: true + + '@esbuild/openbsd-x64@0.25.4': + optional: true + + '@esbuild/sunos-x64@0.25.4': + optional: true + + '@esbuild/win32-arm64@0.25.4': + optional: true + + '@esbuild/win32-ia32@0.25.4': + optional: true + + '@esbuild/win32-x64@0.25.4': + optional: true + + '@eslint-community/eslint-utils@4.7.0(eslint@9.27.0(jiti@2.4.2))': + dependencies: + eslint: 9.27.0(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.20.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.2.2': {} + + '@eslint/core@0.14.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.27.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.1': + dependencies: + '@eslint/core': 0.14.0 + levn: 0.4.1 + + '@floating-ui/core@1.7.0': + dependencies: + '@floating-ui/utils': 0.2.9 + + '@floating-ui/dom@1.7.0': + dependencies: + '@floating-ui/core': 1.7.0 + '@floating-ui/utils': 0.2.9 + + '@floating-ui/react-dom@2.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/dom': 1.7.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@floating-ui/utils@0.2.9': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/sharp-darwin-arm64@0.34.2': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.1.0 + optional: true + + '@img/sharp-darwin-x64@0.34.2': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.1.0 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.1.0': + optional: true + + '@img/sharp-libvips-darwin-x64@1.1.0': + optional: true + + '@img/sharp-libvips-linux-arm64@1.1.0': + optional: true + + '@img/sharp-libvips-linux-arm@1.1.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.1.0': + optional: true + + '@img/sharp-libvips-linux-s390x@1.1.0': + optional: true + + '@img/sharp-libvips-linux-x64@1.1.0': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.1.0': + optional: true + + '@img/sharp-linux-arm64@0.34.2': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.1.0 + optional: true + + '@img/sharp-linux-arm@0.34.2': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.1.0 + optional: true + + '@img/sharp-linux-s390x@0.34.2': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.1.0 + optional: true + + '@img/sharp-linux-x64@0.34.2': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.1.0 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.2': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.2': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + optional: true + + '@img/sharp-wasm32@0.34.2': + dependencies: + '@emnapi/runtime': 1.4.3 + optional: true + + '@img/sharp-win32-arm64@0.34.2': + optional: true + + '@img/sharp-win32-ia32@0.34.2': + optional: true + + '@img/sharp-win32-x64@0.34.2': + optional: true + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@napi-rs/wasm-runtime@0.2.10': + dependencies: + '@emnapi/core': 1.4.3 + '@emnapi/runtime': 1.4.3 + '@tybys/wasm-util': 0.9.0 + optional: true + + '@next/env@15.3.2': {} + + '@next/eslint-plugin-next@15.3.2': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@15.3.2': + optional: true + + '@next/swc-darwin-x64@15.3.2': + optional: true + + '@next/swc-linux-arm64-gnu@15.3.2': + optional: true + + '@next/swc-linux-arm64-musl@15.3.2': + optional: true + + '@next/swc-linux-x64-gnu@15.3.2': + optional: true + + '@next/swc-linux-x64-musl@15.3.2': + optional: true + + '@next/swc-win32-arm64-msvc@15.3.2': + optional: true + + '@next/swc-win32-x64-msvc@15.3.2': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.2': {} + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-checkbox@1.3.2(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.5)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-context@1.1.2(@types/react@19.1.5)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-dialog@1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.0(@types/react@19.1.5)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-direction@1.1.1(@types/react@19.1.5)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.5)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-id@1.1.1(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-label@2.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-menu@2.1.15(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.0(@types/react@19.1.5)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-popover@1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.0(@types/react@19.1.5)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-popper@1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/rect': 1.1.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-scroll-area@1.2.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-select@2.2.5(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.0(@types/react@19.1.5)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-slot@1.2.3(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-tooltip@1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.5)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.5)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.1.5)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/rect@1.1.1': {} + + '@rolldown/pluginutils@1.0.0-beta.9': {} + + '@rollup/rollup-android-arm-eabi@4.41.0': + optional: true + + '@rollup/rollup-android-arm64@4.41.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.41.0': + optional: true + + '@rollup/rollup-darwin-x64@4.41.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.41.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.41.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.41.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.41.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.41.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.41.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.41.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.41.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.41.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.41.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.41.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.41.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.41.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.41.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.41.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.41.0': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@rushstack/eslint-patch@1.11.0': {} + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.7': + dependencies: + '@ampproject/remapping': 2.3.0 + enhanced-resolve: 5.18.1 + jiti: 2.4.2 + lightningcss: 1.30.1 + magic-string: 0.30.17 + source-map-js: 1.2.1 + tailwindcss: 4.1.7 + + '@tailwindcss/oxide-android-arm64@4.1.7': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.7': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.7': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.7': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.7': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.7': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.7': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.7': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.7': + optional: true + + '@tailwindcss/oxide@4.1.7': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.7 + '@tailwindcss/oxide-darwin-arm64': 4.1.7 + '@tailwindcss/oxide-darwin-x64': 4.1.7 + '@tailwindcss/oxide-freebsd-x64': 4.1.7 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.7 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.7 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.7 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.7 + '@tailwindcss/oxide-linux-x64-musl': 4.1.7 + '@tailwindcss/oxide-wasm32-wasi': 4.1.7 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.7 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.7 + + '@tailwindcss/postcss@4.1.7': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.7 + '@tailwindcss/oxide': 4.1.7 + postcss: 8.5.3 + tailwindcss: 4.1.7 + + '@tanstack/react-table@8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@tanstack/table-core@8.21.3': {} + + '@tauri-apps/api@2.5.0': {} + + '@tauri-apps/cli-darwin-arm64@2.5.0': + optional: true + + '@tauri-apps/cli-darwin-x64@2.5.0': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.5.0': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.5.0': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.5.0': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.5.0': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.5.0': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.5.0': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.5.0': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.5.0': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.5.0': + optional: true + + '@tauri-apps/cli@2.5.0': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.5.0 + '@tauri-apps/cli-darwin-x64': 2.5.0 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.5.0 + '@tauri-apps/cli-linux-arm64-gnu': 2.5.0 + '@tauri-apps/cli-linux-arm64-musl': 2.5.0 + '@tauri-apps/cli-linux-riscv64-gnu': 2.5.0 + '@tauri-apps/cli-linux-x64-gnu': 2.5.0 + '@tauri-apps/cli-linux-x64-musl': 2.5.0 + '@tauri-apps/cli-win32-arm64-msvc': 2.5.0 + '@tauri-apps/cli-win32-ia32-msvc': 2.5.0 + '@tauri-apps/cli-win32-x64-msvc': 2.5.0 + + '@tauri-apps/plugin-fs@2.3.0': + dependencies: + '@tauri-apps/api': 2.5.0 + + '@tauri-apps/plugin-opener@2.2.7': + dependencies: + '@tauri-apps/api': 2.5.0 + + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.27.1 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.27.1 + + '@types/estree@1.0.7': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@22.15.21': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.1.5(@types/react@19.1.5)': + dependencies: + '@types/react': 19.1.5 + + '@types/react@19.1.5': + dependencies: + csstype: 3.1.3 + + '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.32.1 + '@typescript-eslint/type-utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.32.1 + eslint: 9.27.0(jiti@2.4.2) + graphemer: 1.4.0 + ignore: 7.0.4 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.32.1 + '@typescript-eslint/types': 8.32.1 + '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.32.1 + debug: 4.4.1 + eslint: 9.27.0(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.32.1': + dependencies: + '@typescript-eslint/types': 8.32.1 + '@typescript-eslint/visitor-keys': 8.32.1 + + '@typescript-eslint/type-utils@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + debug: 4.4.1 + eslint: 9.27.0(jiti@2.4.2) + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.32.1': {} + + '@typescript-eslint/typescript-estree@8.32.1(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 8.32.1 + '@typescript-eslint/visitor-keys': 8.32.1 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.32.1 + '@typescript-eslint/types': 8.32.1 + '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) + eslint: 9.27.0(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.32.1': + dependencies: + '@typescript-eslint/types': 8.32.1 + eslint-visitor-keys: 4.2.0 + + '@unrs/resolver-binding-darwin-arm64@1.7.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.7.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.7.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.7.2': + dependencies: + '@napi-rs/wasm-runtime': 0.2.10 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.7.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.7.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.7.2': + optional: true + + '@vitejs/plugin-react@4.5.0(vite@6.2.0(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1))': + dependencies: + '@babel/core': 7.27.1 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1) + '@rolldown/pluginutils': 1.0.0-beta.9 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.2.0(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1) + transitivePeerDependencies: + - supports-color + + acorn-jsx@5.3.2(acorn@8.14.1): + dependencies: + acorn: 8.14.1 + + acorn@8.14.1: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.10 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.10 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.23.10 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.10 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.10 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.10 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.10 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + ast-types-flow@0.0.8: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.10.3: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.24.5: + dependencies: + caniuse-lite: 1.0.30001718 + electron-to-chromium: 1.5.157 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.24.5) + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001718: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chownr@3.0.0: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + client-only@0.0.1: {} + + clsx@2.1.1: {} + + cmdk@1.1.1(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.1.3: {} + + damerau-levenshtein@1.0.8: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + detect-libc@2.0.4: {} + + detect-node-es@1.1.0: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.157: {} + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.18.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + + es-abstract@1.23.10: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.23.10 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild@0.25.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.4 + '@esbuild/android-arm': 0.25.4 + '@esbuild/android-arm64': 0.25.4 + '@esbuild/android-x64': 0.25.4 + '@esbuild/darwin-arm64': 0.25.4 + '@esbuild/darwin-x64': 0.25.4 + '@esbuild/freebsd-arm64': 0.25.4 + '@esbuild/freebsd-x64': 0.25.4 + '@esbuild/linux-arm': 0.25.4 + '@esbuild/linux-arm64': 0.25.4 + '@esbuild/linux-ia32': 0.25.4 + '@esbuild/linux-loong64': 0.25.4 + '@esbuild/linux-mips64el': 0.25.4 + '@esbuild/linux-ppc64': 0.25.4 + '@esbuild/linux-riscv64': 0.25.4 + '@esbuild/linux-s390x': 0.25.4 + '@esbuild/linux-x64': 0.25.4 + '@esbuild/netbsd-arm64': 0.25.4 + '@esbuild/netbsd-x64': 0.25.4 + '@esbuild/openbsd-arm64': 0.25.4 + '@esbuild/openbsd-x64': 0.25.4 + '@esbuild/sunos-x64': 0.25.4 + '@esbuild/win32-arm64': 0.25.4 + '@esbuild/win32-ia32': 0.25.4 + '@esbuild/win32-x64': 0.25.4 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-next@15.3.2(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3): + dependencies: + '@next/eslint-plugin-next': 15.3.2 + '@rushstack/eslint-patch': 1.11.0 + '@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.27.0(jiti@2.4.2) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.27.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.27.0(jiti@2.4.2)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.27.0(jiti@2.4.2)) + eslint-plugin-react: 7.37.5(eslint@9.27.0(jiti@2.4.2)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.27.0(jiti@2.4.2)) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.27.0(jiti@2.4.2)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.1 + eslint: 9.27.0(jiti@2.4.2) + get-tsconfig: 4.10.1 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.13 + unrs-resolver: 1.7.2 + optionalDependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.27.0(jiti@2.4.2)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.27.0(jiti@2.4.2)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.27.0(jiti@2.4.2) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.27.0(jiti@2.4.2)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.27.0(jiti@2.4.2)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.27.0(jiti@2.4.2) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.27.0(jiti@2.4.2)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.27.0(jiti@2.4.2)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.8 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.10.3 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.27.0(jiti@2.4.2) + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@5.2.0(eslint@9.27.0(jiti@2.4.2)): + dependencies: + eslint: 9.27.0(jiti@2.4.2) + + eslint-plugin-react@7.37.5(eslint@9.27.0(jiti@2.4.2)): + dependencies: + array-includes: 3.1.8 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.1 + eslint: 9.27.0(jiti@2.4.2) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.3.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.27.0(jiti@2.4.2): + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0(jiti@2.4.2)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.20.0 + '@eslint/config-helpers': 0.2.2 + '@eslint/core': 0.14.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.27.0 + '@eslint/plugin-kit': 0.3.1 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.7 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 8.3.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.4.2 + transitivePeerDependencies: + - supports-color + + espree@10.3.0: + dependencies: + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) + eslint-visitor-keys: 4.2.0 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.4.4(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@11.12.0: {} + + globals@14.0.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + ignore@5.3.2: {} + + ignore@7.0.4: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.3.2: + optional: true + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jiti@2.4.2: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.8 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp@3.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + napi-postinstall@0.2.4: {} + + natural-compare@1.4.0: {} + + next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + next@15.3.2(@babel/core@7.27.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@next/env': 15.3.2 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.15 + busboy: 1.6.0 + caniuse-lite: 1.0.30001718 + postcss: 8.4.31 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + styled-jsx: 5.1.6(@babel/core@7.27.1)(react@19.1.0) + optionalDependencies: + '@next/swc-darwin-arm64': 15.3.2 + '@next/swc-darwin-x64': 15.3.2 + '@next/swc-linux-arm64-gnu': 15.3.2 + '@next/swc-linux-arm64-musl': 15.3.2 + '@next/swc-linux-x64-gnu': 15.3.2 + '@next/swc-linux-x64-musl': 15.3.2 + '@next/swc-win32-arm64-msvc': 15.3.2 + '@next/swc-win32-x64-msvc': 15.3.2 + sharp: 0.34.2 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-releases@2.0.19: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.10 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.10 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.3: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + + react-icons@5.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + + react-is@16.13.1: {} + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.1.5)(react@19.1.0): + dependencies: + react: 19.1.0 + react-style-singleton: 2.2.3(@types/react@19.1.5)(react@19.1.0) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.5 + + react-remove-scroll@2.7.0(@types/react@19.1.5)(react@19.1.0): + dependencies: + react: 19.1.0 + react-remove-scroll-bar: 2.3.8(@types/react@19.1.5)(react@19.1.0) + react-style-singleton: 2.2.3(@types/react@19.1.5)(react@19.1.0) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.1.5)(react@19.1.0) + use-sidecar: 1.1.3(@types/react@19.1.5)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + + react-style-singleton@2.2.3(@types/react@19.1.5)(react@19.1.0): + dependencies: + get-nonce: 1.0.1 + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.5 + + react@19.1.0: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.10 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.41.0: + dependencies: + '@types/estree': 1.0.7 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.41.0 + '@rollup/rollup-android-arm64': 4.41.0 + '@rollup/rollup-darwin-arm64': 4.41.0 + '@rollup/rollup-darwin-x64': 4.41.0 + '@rollup/rollup-freebsd-arm64': 4.41.0 + '@rollup/rollup-freebsd-x64': 4.41.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.41.0 + '@rollup/rollup-linux-arm-musleabihf': 4.41.0 + '@rollup/rollup-linux-arm64-gnu': 4.41.0 + '@rollup/rollup-linux-arm64-musl': 4.41.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.41.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.41.0 + '@rollup/rollup-linux-riscv64-gnu': 4.41.0 + '@rollup/rollup-linux-riscv64-musl': 4.41.0 + '@rollup/rollup-linux-s390x-gnu': 4.41.0 + '@rollup/rollup-linux-x64-gnu': 4.41.0 + '@rollup/rollup-linux-x64-musl': 4.41.0 + '@rollup/rollup-win32-arm64-msvc': 4.41.0 + '@rollup/rollup-win32-ia32-msvc': 4.41.0 + '@rollup/rollup-win32-x64-msvc': 4.41.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + scheduler@0.26.0: {} + + semver@6.3.1: {} + + semver@7.7.2: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + sharp@0.34.2: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.2 + '@img/sharp-darwin-x64': 0.34.2 + '@img/sharp-libvips-darwin-arm64': 1.1.0 + '@img/sharp-libvips-darwin-x64': 1.1.0 + '@img/sharp-libvips-linux-arm': 1.1.0 + '@img/sharp-libvips-linux-arm64': 1.1.0 + '@img/sharp-libvips-linux-ppc64': 1.1.0 + '@img/sharp-libvips-linux-s390x': 1.1.0 + '@img/sharp-libvips-linux-x64': 1.1.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + '@img/sharp-linux-arm': 0.34.2 + '@img/sharp-linux-arm64': 0.34.2 + '@img/sharp-linux-s390x': 0.34.2 + '@img/sharp-linux-x64': 0.34.2 + '@img/sharp-linuxmusl-arm64': 0.34.2 + '@img/sharp-linuxmusl-x64': 0.34.2 + '@img/sharp-wasm32': 0.34.2 + '@img/sharp-win32-arm64': 0.34.2 + '@img/sharp-win32-ia32': 0.34.2 + '@img/sharp-win32-x64': 0.34.2 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + + sonner@2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + source-map-js@1.2.1: {} + + stable-hash@0.0.5: {} + + streamsearch@1.1.0: {} + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.10 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.23.10 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.23.10 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.23.10 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + styled-jsx@5.1.6(@babel/core@7.27.1)(react@19.1.0): + dependencies: + client-only: 0.0.1 + react: 19.1.0 + optionalDependencies: + '@babel/core': 7.27.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwind-merge@3.3.0: {} + + tailwindcss@4.1.7: {} + + tapable@2.2.2: {} + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + + tinyglobby@0.2.13: + dependencies: + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@2.1.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tw-animate-css@1.3.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.27.0(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + typescript@5.8.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + unrs-resolver@1.7.2: + dependencies: + napi-postinstall: 0.2.4 + optionalDependencies: + '@unrs/resolver-binding-darwin-arm64': 1.7.2 + '@unrs/resolver-binding-darwin-x64': 1.7.2 + '@unrs/resolver-binding-freebsd-x64': 1.7.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.7.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.7.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.7.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.7.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.7.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-x64-musl': 1.7.2 + '@unrs/resolver-binding-wasm32-wasi': 1.7.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.7.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.7.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.7.2 + + update-browserslist-db@1.1.3(browserslist@4.24.5): + dependencies: + browserslist: 4.24.5 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.1.5)(react@19.1.0): + dependencies: + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.5 + + use-sidecar@1.1.3(@types/react@19.1.5)(react@19.1.0): + dependencies: + detect-node-es: 1.1.0 + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.5 + + vite@6.2.0(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1): + dependencies: + esbuild: 0.25.4 + postcss: 8.5.3 + rollup: 4.41.0 + optionalDependencies: + '@types/node': 22.15.21 + fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.30.1 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yallist@5.0.0: {} + + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..0261394 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +onlyBuiltDependencies: + - '@tailwindcss/oxide' + - esbuild + - sharp diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/file.svg b/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/next.svg b/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/tauri-nextjs-template-2_screenshot.png b/public/tauri-nextjs-template-2_screenshot.png new file mode 100644 index 0000000..37f8b03 Binary files /dev/null and b/public/tauri-nextjs-template-2_screenshot.png differ diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/window.svg b/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore new file mode 100644 index 0000000..12ab04f --- /dev/null +++ b/src-tauri/.gitignore @@ -0,0 +1,10 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Generated by Tauri +# will have schema files for capabilities auto-completion +/gen/schemas + +/binaries/** +!**/.gitkeep \ No newline at end of file diff --git a/src-tauri/.rustfmt.toml b/src-tauri/.rustfmt.toml new file mode 100644 index 0000000..346c9d2 --- /dev/null +++ b/src-tauri/.rustfmt.toml @@ -0,0 +1,2 @@ +tab_spaces = 2 +max_width = 100 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..dd96ff3 --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,6097 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +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-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[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.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 0.38.44", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 0.38.44", + "tracing", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.44", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +dependencies = [ + "serde", +] + +[[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 = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d59b4c170e16f0405a2e95aff44432a0d41aa97675f3d52623effe95792a037" +dependencies = [ + "objc2 0.6.1", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytemuck" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.9.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.11", +] + +[[package]] +name = "cargo_toml" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257" +dependencies = [ + "serde", + "toml", +] + +[[package]] +name = "cc" +version = "1.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[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.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +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 = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.10.0", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.98", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.98", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.98", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + +[[package]] +name = "deranged" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.98", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.59.0", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.1", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "dlopen2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "donutbrowser" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64 0.22.1", + "core-foundation 0.10.0", + "directories", + "futures-util", + "lazy_static", + "reqwest", + "serde", + "serde_json", + "sysinfo", + "tauri", + "tauri-build", + "tauri-plugin-deep-link", + "tauri-plugin-fs", + "tauri-plugin-opener", + "tauri-plugin-shell", + "tempfile", + "tokio", + "tokio-test", + "zip", +] + +[[package]] +name = "dpi" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + +[[package]] +name = "embed-resource" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbc6e0d8e0c03a655b53ca813f0463d2c956bc4db8138dbc89f120b066551e3" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml", + "vswhom", + "winreg", +] + +[[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.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +dependencies = [ + "serde", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "flate2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +dependencies = [ + "crc32fast", + "libz-rs-sys", + "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 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +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-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "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.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.9.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "h2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.7.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.14", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa 1.0.14", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.61.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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", + "serde", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.9.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 1.9.3", + "matches", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[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 = "liblzma" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66352d7a8ac12d4877b6e6ea5a9b7650ee094257dc40889955bea5bc5b08c1d0" +dependencies = [ + "liblzma-sys", +] + +[[package]] +name = "liblzma-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5839bad90c3cc2e0b8c4ed8296b80e86040240f81d46b9c0e9bc8dd51ddd3af1" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.0", + "libc", +] + +[[package]] +name = "libz-rs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a" +dependencies = [ + "zlib-rs", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "muda" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.0", + "once_cell", + "png", + "serde", + "thiserror 2.0.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[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.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" +dependencies = [ + "bitflags 2.9.0", + "block2 0.6.0", + "libc", + "objc2 0.6.1", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-foundation 0.3.0", + "objc2-quartz-core 0.3.0", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.1", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.1", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags 2.9.0", + "dispatch2", + "objc2 0.6.1", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56" +dependencies = [ + "objc2 0.6.1", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.9.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" +dependencies = [ + "bitflags 2.9.0", + "block2 0.6.0", + "libc", + "objc2 0.6.1", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.1", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.9.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.9.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.1", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777a571be14a42a3990d4ebedaeb8b54cd17377ec21b92e8200ac03797b3bee1" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce" +dependencies = [ + "bitflags 2.9.0", + "block2 0.6.0", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "foreign-types 0.3.2", + "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.98", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" +dependencies = [ + "cc", + "libc", + "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-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_pipe" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[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_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "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.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" +dependencies = [ + "base64 0.22.1", + "indexmap 2.7.1", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 0.38.44", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[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 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.7", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.24", +] + +[[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.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +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.15", +] + +[[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.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 2.0.11", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry 0.4.0", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rust-ini" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.9.2", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" + +[[package]] +name = "rustls-webpki" +version = "0.103.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[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.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.98", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +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 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa 1.0.14", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +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.14", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.7.1", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared_child" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e297bd52991bbe0686c086957bee142f13df85d1e79b0b21630a99d374ae9dc" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "bytemuck", + "cfg_aliases", + "core-graphics", + "foreign-types 0.5.0", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", + "raw-window-handle", + "redox_syscall", + "wasm-bindgen", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[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.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "sysinfo" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b897c8ea620e181c7955369a31be5f48d9a9121cb59fd33ecef9ff2a34323422" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.61.1", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.10.0", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-foundation 0.3.0", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.1", + "windows-core 0.61.0", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7b0bc1aec81bda6bc455ea98fcaed26b3c98c1648c627ad6ff1c704e8bf8cbc" +dependencies = [ + "anyhow", + "bytes", + "dirs", + "dunce", + "embed_plist", + "futures-util", + "getrandom 0.2.15", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-foundation 0.3.0", + "objc2-ui-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.11", + "tokio", + "tray-icon", + "url", + "urlpattern", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows 0.61.1", +] + +[[package]] +name = "tauri-build" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a0350f0df1db385ca5c02888a83e0e66655c245b7443db8b78a70da7d7f8fc" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93f035551bf7b11b3f51ad9bc231ebbe5e085565527991c16cf326aa38cdf47" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.98", + "tauri-utils", + "thiserror 2.0.11", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8db4df25e2d9d45de0c4c910da61cd5500190da14ae4830749fee3466dddd112" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.98", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a5ebe6a610d1b78a94650896e6f7c9796323f408800cef436e0fa0539de601" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars", + "serde", + "serde_json", + "tauri-utils", + "toml", + "walkdir", +] + +[[package]] +name = "tauri-plugin-deep-link" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4976ac728ebc0487515aa956cfdf200abcc52b784e441493fc544bc6ce369c8" +dependencies = [ + "dunce", + "rust-ini", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.11", + "tracing", + "url", + "windows-registry 0.5.1", + "windows-result", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88371e340ad2f07409a3b68294abe73f20bc9c1bc1b631a31dc37a3d0161f682" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.11", + "toml", + "url", + "uuid", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fdc6cb608e04b7d2b6d1f21e9444ad49245f6d03465ba53323d692d1ceb1a30" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation 0.3.0", + "open", + "schemars", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.11", + "url", + "windows 0.60.0", + "zbus", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d5eb3368b959937ad2aeaf6ef9a8f5d11e01ffe03629d3530707bbcb27ff5d" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.11", + "tokio", +] + +[[package]] +name = "tauri-runtime" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f004905d549854069e6774533d742b03cacfd6f03deb08940a8677586cbe39" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2 0.6.1", + "objc2-ui-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.11", + "url", + "windows 0.61.1", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f85d056f4d4b014fe874814034f3416d57114b617a493a4fe552580851a3f3a2" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-foundation 0.3.0", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.1", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2900399c239a471bcff7f15c4399eb1a8c4fe511ba2853e07c996d771a5e0a4" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.11", + "toml", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d321dbc6f998d825ab3f0d62673e810c861aac2d0de2cc2c395328f1d113b4" +dependencies = [ + "embed-resource", + "indexmap 2.7.1", + "toml", +] + +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom 0.3.1", + "once_cell", + "rustix 1.0.1", + "windows-sys 0.59.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 = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa 1.0.14", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.44.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[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.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.24", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.7.1", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.7.1", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap 2.7.1", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.7.3", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7eee98ec5c90daf179d55c20a49d8c0d043054ce7c26336c09a24d31f14fa0" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.0", + "once_cell", + "png", + "serde", + "thiserror 2.0.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.1", + "serde", +] + +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[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.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.98", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b542b5cfbd9618c46c2784e4d41ba218c336ac70d44c55e47b251033e7d85601" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.61.1", + "windows-core 0.61.0", + "windows-implement 0.60.0", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "webview2-com-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae2d11c4a686e4409659d7891791254cf9286d3cfe0eef54df1523533d22295" +dependencies = [ + "thiserror 2.0.11", + "windows 0.61.1", + "windows-core 0.61.0", +] + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[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-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2 0.6.1", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.0", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529" +dependencies = [ + "windows-collections 0.1.1", + "windows-core 0.60.1", + "windows-future 0.1.1", + "windows-link", + "windows-numerics 0.1.1", +] + +[[package]] +name = "windows" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.0", + "windows-future 0.2.0", + "windows-link", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows-collections" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec" +dependencies = [ + "windows-core 0.60.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.0", +] + +[[package]] +name = "windows-core" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" +dependencies = [ + "windows-implement 0.59.0", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings 0.3.1", +] + +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings 0.4.0", +] + +[[package]] +name = "windows-future" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0" +dependencies = [ + "windows-core 0.60.1", + "windows-link", +] + +[[package]] +name = "windows-future" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" +dependencies = [ + "windows-core 0.61.0", + "windows-link", +] + +[[package]] +name = "windows-implement" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-numerics" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed" +dependencies = [ + "windows-core 0.60.1", + "windows-link", +] + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.0", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings 0.3.1", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-registry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings 0.4.0", +] + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + +[[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.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows-version" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c" +dependencies = [ + "windows-link", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wry" +version = "0.51.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c886a0a9d2a94fd90cfa1d929629b79cfefb1546e2c7430c63a47f0664c0e4e2" +dependencies = [ + "base64 0.22.1", + "block2 0.6.0", + "cookie", + "crossbeam-channel", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.0", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.11", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.1", + "windows-core 0.61.0", + "windows-version", + "x11-dl", +] + +[[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 = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow 0.7.3", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f325ad10eb0d0a3eb060203494c3b7ec3162a01a59db75d2deee100339709fc0" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.98", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.3", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "zip" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.1", + "hmac", + "indexmap 2.7.1", + "liblzma", + "memchr", + "pbkdf2", + "sha1", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zvariant" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2df9ee044893fcffbdc25de30546edef3e32341466811ca18421e3cd6c5a3ac" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "winnow 0.7.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74170caa85b8b84cc4935f2d56a57c7a15ea6185ccdd7eadb57e6edd90f94b2f" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.98", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn 2.0.98", + "winnow 0.7.3", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..ce46ac0 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "donutbrowser" +version = "0.1.0" +description = "A Tauri App" +authors = ["zhom@github"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +# The `_lib` suffix may seem redundant but it is necessary +# to make the lib name unique and wouldn't conflict with the bin name. +# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 +name = "donutbrowser" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +serde_json = "1" +serde = { version = "1", features = ["derive"] } +tauri = { version = "2", features = ["devtools"] } +tauri-plugin-opener = "2" +tauri-plugin-fs = "2" +tauri-plugin-shell = "2" +tauri-plugin-deep-link = "2" +directories = "6" +reqwest = { version = "0.12", features = ["json", "stream"] } +tokio = { version = "1", features = ["full"] } +sysinfo = "0.35" +lazy_static = "1.4" +base64 = "0.22" +zip = "4" +async-trait = "0.1" +core-foundation="0.10" +futures-util = "0.3" + +[dev-dependencies] +tempfile = "3.13.0" +tokio-test = "0.4.4" + +[features] +# by default Tauri runs in production mode +# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` points to the filesystem +default = [ "custom-protocol" ] +# this feature is used used for production builds where `devPath` points to the filesystem +# DO NOT remove this +custom-protocol = [ "tauri/custom-protocol" ] diff --git a/src-tauri/Info.plist b/src-tauri/Info.plist new file mode 100644 index 0000000..11e1e46 --- /dev/null +++ b/src-tauri/Info.plist @@ -0,0 +1,48 @@ + + + + + CFBundleDisplayName + Donut Browser + CFBundleName + Donut Browser + CFBundleIdentifier + com.donutbrowser + CFBundleExecutable + donutbrowser + CFBundleVersion + 1 + CFBundleShortVersionString + 0.1.0 + CFBundlePackageType + APPL + CFBundleIconFile + icon.icns + CFBundleSignature + ???? + CFBundleIconFile + icon.icns + CFBundleURLTypes + + + CFBundleURLName + Web Browser + CFBundleURLSchemes + + http + https + + CFBundleURLIconFile + icon.icns + LSHandlerRank + Owner + + + LSApplicationCategoryType + public.app-category.productivity + NSHumanReadableCopyright + Copyright © 2025 Donut Browser + LSMinimumSystemVersion + 10.13 + + \ No newline at end of file diff --git a/src-tauri/assets/template.pac b/src-tauri/assets/template.pac new file mode 100644 index 0000000..6476447 --- /dev/null +++ b/src-tauri/assets/template.pac @@ -0,0 +1,14 @@ +function FindProxyForURL(url, host) { + const proxyString = "{{proxy_url}}"; + + // Split the proxy string to get the credentials part + const parts = proxyString.split(" ")[1].split("@"); + if (parts.length > 1) { + const credentials = parts[0]; + const encodedCredentials = encodeURIComponent(credentials); + // Replace the original credentials with encoded ones + return proxyString.replace(credentials, encodedCredentials); + } + + return proxyString; +} diff --git a/src-tauri/binaries/.gitkeep b/src-tauri/binaries/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..e52f368 --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,9 @@ +fn main() { + #[cfg(target_os = "macos")] + { + println!("cargo:rustc-link-lib=framework=CoreFoundation"); + println!("cargo:rustc-link-lib=framework=CoreServices"); + } + + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..2661420 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,18 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "enables the default permissions", + "windows": ["main"], + "permissions": [ + "core:default", + "core:event:default", + "opener:default", + "fs:default", + "shell:allow-execute", + "shell:allow-kill", + "shell:allow-open", + "shell:allow-spawn", + "shell:allow-stdin-write", + "deep-link:default" + ] +} diff --git a/src-tauri/entitlements.plist b/src-tauri/entitlements.plist new file mode 100644 index 0000000..c514412 --- /dev/null +++ b/src-tauri/entitlements.plist @@ -0,0 +1,16 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + com.apple.security.files.downloads.read-write + + + \ No newline at end of file diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000..2dcf36e 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..cff0582 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..023b77f Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png new file mode 100644 index 0000000..bcfb767 Binary files /dev/null and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..cf56201 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..bee2506 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..1d325c6 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..a5d6340 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..a8f2320 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..cd37aba 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..ca05ba3 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..15a98c4 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..0b0162d 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..464eaa7 Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..856a082 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8e26f2b Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..856a082 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..d1694ec Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..efa549c Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..d1694ec Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..7171fe2 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..62e1057 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..7171fe2 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..2c606be Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..ac3f981 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..2c606be Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..9a2dfbc Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..27ce75a Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..9a2dfbc Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000..a4e747e 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..88cbead 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..24f5656 Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@1x.png b/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000..f76dbf5 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000..50d951a Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x.png b/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000..50d951a Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@3x.png b/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000..88e54fd Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@1x.png b/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000..0e6bce5 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000..f471de0 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x.png b/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000..f471de0 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@3x.png b/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000..6c8d187 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@1x.png b/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000..50d951a Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000..9c9205a Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x.png b/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000..9c9205a Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@3x.png b/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000..da1c263 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-512@2x.png b/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000..8ff9dd4 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@2x.png b/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000..da1c263 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@3x.png b/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000..572953d Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@1x.png b/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000..4717dfc Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@2x.png b/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000..49b46ea Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000..bd17f39 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/src-tauri/icons/logo.png b/src-tauri/icons/logo.png new file mode 100644 index 0000000..9ac1c48 Binary files /dev/null and b/src-tauri/icons/logo.png differ diff --git a/src-tauri/src/api_client.rs b/src-tauri/src/api_client.rs new file mode 100644 index 0000000..551ad79 --- /dev/null +++ b/src-tauri/src/api_client.rs @@ -0,0 +1,1099 @@ +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; +use directories::BaseDirs; + +use crate::browser::GithubRelease; + +#[derive(Debug, Clone, PartialEq, Eq)] +struct VersionComponent { + major: u32, + minor: u32, + patch: u32, + pre_release: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct PreRelease { + kind: PreReleaseKind, + number: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +enum PreReleaseKind { + Alpha, + Beta, + RC, + Dev, + Pre, +} + +impl VersionComponent { + fn parse(version: &str) -> Self { + let version = version.trim(); + + // Handle special case for Zen Browser twilight releases + if version.to_lowercase().contains("twilight") { + return VersionComponent { + major: u32::MAX, + minor: u32::MAX, + patch: u32::MAX, + pre_release: None, + }; + } + + // Split version into numeric and pre-release parts + let (numeric_part, pre_release_part) = Self::split_version(version); + + // Parse numeric parts (major.minor.patch) + let parts: Vec = numeric_part + .split('.') + .filter_map(|part| part.parse().ok()) + .collect(); + + let major = parts.get(0).copied().unwrap_or(0); + let minor = parts.get(1).copied().unwrap_or(0); + let patch = parts.get(2).copied().unwrap_or(0); + + // Parse pre-release part + let pre_release = pre_release_part.as_deref().and_then(Self::parse_pre_release); + + VersionComponent { + major, + minor, + patch, + pre_release, + } + } + + fn split_version(version: &str) -> (String, Option) { + let version = version.to_lowercase(); + + // Look for pre-release indicators + for (i, ch) in version.char_indices() { + if ch.is_alphabetic() && i > 0 { + // Check if this is a pre-release indicator + let remaining = &version[i..]; + if remaining.starts_with('a') || remaining.starts_with('b') || + remaining.starts_with("alpha") || remaining.starts_with("beta") || + remaining.starts_with("rc") || remaining.starts_with("dev") || + remaining.starts_with("pre") { + return (version[..i].to_string(), Some(remaining.to_string())); + } + } + } + + (version, None) + } + + fn parse_pre_release(pre_release: &str) -> Option { + let pre_release = pre_release.trim().to_lowercase(); + + if pre_release.is_empty() { + return None; + } + + // Extract kind and number + let (kind, number) = if pre_release.starts_with("alpha") { + (PreReleaseKind::Alpha, Self::extract_number(&pre_release[5..])) + } else if pre_release.starts_with("beta") { + (PreReleaseKind::Beta, Self::extract_number(&pre_release[4..])) + } else if pre_release.starts_with("rc") { + (PreReleaseKind::RC, Self::extract_number(&pre_release[2..])) + } else if pre_release.starts_with("dev") { + (PreReleaseKind::Dev, Self::extract_number(&pre_release[3..])) + } else if pre_release.starts_with("pre") { + (PreReleaseKind::Pre, Self::extract_number(&pre_release[3..])) + } else if pre_release.starts_with('a') { + (PreReleaseKind::Alpha, Self::extract_number(&pre_release[1..])) + } else if pre_release.starts_with('b') { + (PreReleaseKind::Beta, Self::extract_number(&pre_release[1..])) + } else { + return None; + }; + + Some(PreRelease { kind, number }) + } + + fn extract_number(s: &str) -> Option { + let numeric_part: String = s.chars().filter(|c| c.is_ascii_digit()).collect(); + numeric_part.parse().ok() + } +} + +impl PartialOrd for VersionComponent { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for VersionComponent { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + use std::cmp::Ordering; + + // Compare major.minor.patch first + match (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch)) { + Ordering::Equal => { + // If numeric parts are equal, compare pre-release + match (&self.pre_release, &other.pre_release) { + (None, None) => Ordering::Equal, + (None, Some(_)) => Ordering::Greater, // Stable > pre-release + (Some(_), None) => Ordering::Less, // Pre-release < stable + (Some(a), Some(b)) => { + // Compare pre-release kinds first + match a.kind.cmp(&b.kind) { + Ordering::Equal => { + // Same kind, compare numbers + match (&a.number, &b.number) { + (None, None) => Ordering::Equal, + (None, Some(_)) => Ordering::Less, + (Some(_), None) => Ordering::Greater, + (Some(a_num), Some(b_num)) => a_num.cmp(b_num), + } + } + other => other, + } + } + } + } + other => other, + } + } +} + +// Helper function to sort versions properly +pub fn sort_versions(versions: &mut [String]) { + versions.sort_by(|a, b| { + let version_a = VersionComponent::parse(a); + let version_b = VersionComponent::parse(b); + version_b.cmp(&version_a) // Descending order (newest first) + }); +} + +// Helper function to sort GitHub releases +pub fn sort_github_releases(releases: &mut [GithubRelease]) { + releases.sort_by(|a, b| { + let version_a = VersionComponent::parse(&a.tag_name); + let version_b = VersionComponent::parse(&b.tag_name); + version_b.cmp(&version_a) // Descending order (newest first) + }); +} + + + +pub fn is_alpha_version(version: &str) -> bool { + let version_comp = VersionComponent::parse(version); + version_comp.pre_release.is_some() +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FirefoxRelease { + pub build_number: u32, + pub category: String, + pub date: String, + pub description: Option, + pub is_security_driven: bool, + pub product: String, + pub version: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FirefoxApiResponse { + pub releases: HashMap, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BrowserRelease { + pub version: String, + pub date: String, + pub is_prerelease: bool, + pub download_url: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct CachedVersionData { + versions: Vec, + timestamp: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +struct CachedGithubData { + releases: Vec, + timestamp: u64, +} + +pub struct ApiClient { + client: Client, +} + +impl ApiClient { + pub fn new() -> Self { + Self { + client: Client::new(), + } + } + + fn get_cache_dir() -> Result> { + let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?; + let app_name = if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }; + let cache_dir = base_dirs.cache_dir().join(app_name).join("version_cache"); + fs::create_dir_all(&cache_dir)?; + Ok(cache_dir) + } + + fn get_current_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() + } + + fn is_cache_valid(timestamp: u64) -> bool { + let current_time = Self::get_current_timestamp(); + let cache_duration = 10 * 60; // 10 minutes in seconds + current_time - timestamp < cache_duration + } + + pub fn load_cached_versions(&self, browser: &str) -> Option> { + let cache_dir = Self::get_cache_dir().ok()?; + let cache_file = cache_dir.join(format!("{}_versions.json", browser)); + + if !cache_file.exists() { + return None; + } + + let content = fs::read_to_string(&cache_file).ok()?; + let cached_data: CachedVersionData = serde_json::from_str(&content).ok()?; + + // Always return cached versions regardless of age - they're always valid + println!("Using cached versions for {}", browser); + Some(cached_data.versions) + } + + pub fn is_cache_expired(&self, browser: &str) -> bool { + let cache_dir = match Self::get_cache_dir() { + Ok(dir) => dir, + Err(_) => return true, // If we can't get cache dir, consider expired + }; + let cache_file = cache_dir.join(format!("{}_versions.json", browser)); + + if !cache_file.exists() { + return true; // No cache file means expired + } + + let content = match fs::read_to_string(&cache_file) { + Ok(content) => content, + Err(_) => return true, // Can't read cache, consider expired + }; + + let cached_data: CachedVersionData = match serde_json::from_str(&content) { + Ok(data) => data, + Err(_) => return true, // Can't parse cache, consider expired + }; + + // Check if cache is older than 10 minutes + !Self::is_cache_valid(cached_data.timestamp) + } + + pub fn save_cached_versions(&self, browser: &str, versions: &[String]) -> Result<(), Box> { + let cache_dir = Self::get_cache_dir()?; + let cache_file = cache_dir.join(format!("{}_versions.json", browser)); + + let cached_data = CachedVersionData { + versions: versions.to_vec(), + timestamp: Self::get_current_timestamp(), + }; + + let content = serde_json::to_string_pretty(&cached_data)?; + fs::write(&cache_file, content)?; + println!("Cached {} versions for {}", versions.len(), browser); + Ok(()) + } + + fn load_cached_github_releases(&self, browser: &str) -> Option> { + let cache_dir = Self::get_cache_dir().ok()?; + let cache_file = cache_dir.join(format!("{}_github.json", browser)); + + if !cache_file.exists() { + return None; + } + + let content = fs::read_to_string(&cache_file).ok()?; + let cached_data: CachedGithubData = serde_json::from_str(&content).ok()?; + + // Always use cached GitHub releases - cache never expires, only gets updated with new versions + println!("Using cached GitHub releases for {}", browser); + Some(cached_data.releases) + } + + fn save_cached_github_releases(&self, browser: &str, releases: &[GithubRelease]) -> Result<(), Box> { + let cache_dir = Self::get_cache_dir()?; + let cache_file = cache_dir.join(format!("{}_github.json", browser)); + + let cached_data = CachedGithubData { + releases: releases.to_vec(), + timestamp: Self::get_current_timestamp(), + }; + + let content = serde_json::to_string_pretty(&cached_data)?; + fs::write(&cache_file, content)?; + println!("Cached {} GitHub releases for {}", releases.len(), browser); + Ok(()) + } + + pub async fn fetch_firefox_releases_with_caching(&self, no_caching: bool) -> Result, Box> { + // Check cache first (unless bypassing) + if !no_caching { + if let Some(cached_versions) = self.load_cached_versions("firefox") { + return Ok(cached_versions.into_iter().map(|version| { + BrowserRelease { + version: version.clone(), + date: "".to_string(), // Cache doesn't store dates + is_prerelease: is_alpha_version(&version), + download_url: Some(format!( + "https://download.mozilla.org/?product=firefox-{}&os=osx&lang=en-US", + version + )), + } + }).collect()); + } + } + + println!("Fetching Firefox releases from Mozilla API..."); + let url = "https://product-details.mozilla.org/1.0/firefox.json"; + + let response = self.client + .get(url) + .header("User-Agent", "donutbrowser") + .send() + .await?; + + if !response.status().is_success() { + return Err(format!("Failed to fetch Firefox versions: {}", response.status()).into()); + } + + let firefox_response: FirefoxApiResponse = response.json().await?; + + // Extract releases and filter for stable versions + let mut releases: Vec = firefox_response + .releases + .into_iter() + .filter_map(|(key, release)| { + // Only include releases that start with "firefox-" and have proper version format + if key.starts_with("firefox-") && !release.version.is_empty() { + let is_stable = matches!(release.category.as_str(), "major" | "stability"); + Some(BrowserRelease { + version: release.version.clone(), + date: release.date, + is_prerelease: !is_stable, + download_url: Some(format!( + "https://download.mozilla.org/?product=firefox-{}&os=osx&lang=en-US", + release.version + )), + }) + } else { + None + } + }) + .collect(); + + // Sort by version number in descending order (newest first) + releases.sort_by(|a, b| { + let version_a = VersionComponent::parse(&a.version); + let version_b = VersionComponent::parse(&b.version); + version_b.cmp(&version_a) + }); + + // Extract versions for caching + let versions: Vec = releases.iter().map(|r| r.version.clone()).collect(); + + // Cache the results (unless bypassing cache) + if !no_caching { + if let Err(e) = self.save_cached_versions("firefox", &versions) { + eprintln!("Failed to cache Firefox versions: {}", e); + } + } + + Ok(releases) + } + + pub async fn fetch_firefox_developer_releases_with_caching(&self, no_caching: bool) -> Result, Box> { + // Check cache first (unless bypassing) + if !no_caching { + if let Some(cached_versions) = self.load_cached_versions("firefox-developer") { + return Ok(cached_versions.into_iter().map(|version| { + BrowserRelease { + version: version.clone(), + date: "".to_string(), // Cache doesn't store dates + is_prerelease: is_alpha_version(&version), + download_url: Some(format!( + "https://download.mozilla.org/?product=devedition-{}&os=osx&lang=en-US", + version + )), + } + }).collect()); + } + } + + println!("Fetching Firefox Developer Edition releases from Mozilla API..."); + let url = "https://product-details.mozilla.org/1.0/devedition.json"; + + let response = self.client + .get(url) + .header("User-Agent", "donutbrowser") + .send() + .await?; + + if !response.status().is_success() { + return Err(format!("Failed to fetch Firefox Developer Edition versions: {}", response.status()).into()); + } + + let firefox_response: FirefoxApiResponse = response.json().await?; + + // Extract releases and filter for developer edition versions + let mut releases: Vec = firefox_response + .releases + .into_iter() + .filter_map(|(key, release)| { + // Only include releases that start with "devedition-" and have proper version format + if key.starts_with("devedition-") && !release.version.is_empty() { + let is_stable = matches!(release.category.as_str(), "major" | "stability"); + Some(BrowserRelease { + version: release.version.clone(), + date: release.date, + is_prerelease: !is_stable, + download_url: Some(format!( + "https://download.mozilla.org/?product=devedition-{}&os=osx&lang=en-US", + release.version + )), + }) + } else { + None + } + }) + .collect(); + + // Sort by version number in descending order (newest first) + releases.sort_by(|a, b| { + let version_a = VersionComponent::parse(&a.version); + let version_b = VersionComponent::parse(&b.version); + version_b.cmp(&version_a) + }); + + // Extract versions for caching + let versions: Vec = releases.iter().map(|r| r.version.clone()).collect(); + + // Cache the results (unless bypassing cache) + if !no_caching { + if let Err(e) = self.save_cached_versions("firefox-developer", &versions) { + eprintln!("Failed to cache Firefox Developer versions: {}", e); + } + } + + Ok(releases) + } + + pub async fn fetch_mullvad_releases(&self) -> Result, Box> { + self.fetch_mullvad_releases_with_caching(false).await + } + + pub async fn fetch_mullvad_releases_with_caching(&self, no_caching: bool) -> Result, Box> { + // Check cache first (unless bypassing) + if !no_caching { + if let Some(cached_releases) = self.load_cached_github_releases("mullvad") { + return Ok(cached_releases); + } + } + + println!("Fetching Mullvad releases from GitHub API..."); + let url = "https://api.github.com/repos/mullvad/mullvad-browser/releases"; + let releases = self + .client + .get(url) + .header("User-Agent", "donutbrowser") + .send() + .await? + .json::>() + .await?; + + let mut releases: Vec = releases + .into_iter() + .map(|mut release| { + release.is_alpha = release.prerelease; + release + }) + .collect(); + + // Sort releases using the new version sorting system + sort_github_releases(&mut releases); + + // Cache the results (unless bypassing cache) + if !no_caching { + if let Err(e) = self.save_cached_github_releases("mullvad", &releases) { + eprintln!("Failed to cache Mullvad releases: {}", e); + } + } + + Ok(releases) + } + + pub async fn fetch_zen_releases(&self) -> Result, Box> { + self.fetch_zen_releases_with_caching(false).await + } + + pub async fn fetch_zen_releases_with_caching(&self, no_caching: bool) -> Result, Box> { + // Check cache first (unless bypassing) + if !no_caching { + if let Some(cached_releases) = self.load_cached_github_releases("zen") { + return Ok(cached_releases); + } + } + + println!("Fetching Zen releases from GitHub API..."); + let url = "https://api.github.com/repos/zen-browser/desktop/releases"; + let mut releases = self + .client + .get(url) + .header("User-Agent", "donutbrowser") + .send() + .await? + .json::>() + .await?; + + // Sort releases using the new version sorting system (twilight releases will be at top) + sort_github_releases(&mut releases); + + // Cache the results (unless bypassing cache) + if !no_caching { + if let Err(e) = self.save_cached_github_releases("zen", &releases) { + eprintln!("Failed to cache Zen releases: {}", e); + } + } + + Ok(releases) + } + + pub async fn fetch_brave_releases(&self) -> Result, Box> { + self.fetch_brave_releases_with_caching(false).await + } + + pub async fn fetch_brave_releases_with_caching(&self, no_caching: bool) -> Result, Box> { + // Check cache first (unless bypassing) + if !no_caching { + if let Some(cached_releases) = self.load_cached_github_releases("brave") { + return Ok(cached_releases); + } + } + + println!("Fetching Brave releases from GitHub API..."); + let url = "https://api.github.com/repos/brave/brave-browser/releases"; + let releases = self + .client + .get(url) + .header("User-Agent", "donutbrowser") + .send() + .await? + .json::>() + .await?; + + // Filter releases that have universal macOS DMG assets + let mut filtered_releases: Vec = releases + .into_iter() + .filter_map(|mut release| { + // Check if this release has a universal DMG asset + let has_universal_dmg = release.assets.iter().any(|asset| { + asset.name.contains(".dmg") && asset.name.contains("universal") + }); + + if has_universal_dmg { + // Set is_alpha based on the release name + // Nightly releases contain "Nightly", stable contain "Release" + release.is_alpha = release.name.to_lowercase().contains("nightly"); + Some(release) + } else { + None + } + }) + .collect(); + + // Sort releases using the new version sorting system + sort_github_releases(&mut filtered_releases); + + // Cache the results (unless bypassing cache) + if !no_caching { + if let Err(e) = self.save_cached_github_releases("brave", &filtered_releases) { + eprintln!("Failed to cache Brave releases: {}", e); + } + } + + Ok(filtered_releases) + } + + pub async fn fetch_chromium_latest_version(&self) -> Result> { + // Use architecture-aware URL for Chromium + let arch = if cfg!(target_arch = "aarch64") { "Mac_Arm" } else { "Mac" }; + let url = format!("https://commondatastorage.googleapis.com/chromium-browser-snapshots/{}/LAST_CHANGE", arch); + let version = self + .client + .get(&url) + .header("User-Agent", "donutbrowser") + .send() + .await? + .text() + .await? + .trim() + .to_string(); + + Ok(version) + } + + pub async fn fetch_chromium_releases_with_caching(&self, no_caching: bool) -> Result, Box> { + // Check cache first (unless bypassing) + if !no_caching { + if let Some(cached_versions) = self.load_cached_versions("chromium") { + return Ok(cached_versions.into_iter().map(|version| { + BrowserRelease { + version: version.clone(), + date: "".to_string(), // Cache doesn't store dates + is_prerelease: false, // Chromium versions are generally stable builds + download_url: None, + } + }).collect()); + } + } + + println!("Fetching Chromium releases..."); + + // Get the latest version first + let latest_version = self.fetch_chromium_latest_version().await?; + let latest_num: u32 = latest_version.parse().unwrap_or(0); + + // Generate a list of recent versions (last 20 builds, going back by 1000 each time) + let mut versions = Vec::new(); + for i in 0..20 { + let version_num = latest_num.saturating_sub(i * 1000); + if version_num > 0 { + versions.push(version_num.to_string()); + } + } + + // Cache the results (unless bypassing cache) + if !no_caching { + if let Err(e) = self.save_cached_versions("chromium", &versions) { + eprintln!("Failed to cache Chromium versions: {}", e); + } + } + + Ok(versions.into_iter().map(|version| { + BrowserRelease { + version: version.clone(), + date: "".to_string(), + is_prerelease: false, + download_url: None, + } + }).collect()) + } + + pub async fn fetch_tor_releases_with_caching(&self, no_caching: bool) -> Result, Box> { + // Check cache first (unless bypassing) + if !no_caching { + if let Some(cached_versions) = self.load_cached_versions("tor-browser") { + return Ok(cached_versions.into_iter().map(|version| { + BrowserRelease { + version: version.clone(), + date: "".to_string(), // Cache doesn't store dates + is_prerelease: false, // Assume all archived versions are stable + download_url: Some(format!( + "https://archive.torproject.org/tor-package-archive/torbrowser/{}/tor-browser-macos-{}.dmg", + version, version + )), + } + }).collect()); + } + } + + println!("Fetching TOR releases from archive..."); + let url = "https://archive.torproject.org/tor-package-archive/torbrowser/"; + let html = self + .client + .get(url) + .header("User-Agent", "donutbrowser") + .send() + .await? + .text() + .await?; + + // Parse HTML to extract version directories + let mut version_candidates = Vec::new(); + + // Look for directory links in the HTML + for line in html.lines() { + if line.contains("") { + // Extract the directory name from the href attribute + if let Some(start) = line.find("") { + let version = &line[start..start + end]; + + // Skip parent directory and non-version entries + if version != ".." && !version.is_empty() && version.chars().next().unwrap_or('a').is_ascii_digit() { + version_candidates.push(version.to_string()); + } + } + } + } + } + + // Sort version candidates using the new version sorting system + sort_versions(&mut version_candidates); + + // Only check the first 10 versions to avoid being too slow + let mut version_strings = Vec::new(); + for version in version_candidates.into_iter().take(10) { + // Check if this version has a macOS DMG file + if let Ok(has_macos) = self.check_tor_version_has_macos(&version).await { + if has_macos { + version_strings.push(version); + } + } + + // Add a small delay to avoid overwhelming the server + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + + // Cache the results (unless bypassing cache) + if !no_caching { + if let Err(e) = self.save_cached_versions("tor-browser", &version_strings) { + eprintln!("Failed to cache TOR versions: {}", e); + } + } + + Ok(version_strings.into_iter().map(|version| { + BrowserRelease { + version: version.clone(), + date: "".to_string(), // TOR archive doesn't provide structured dates + is_prerelease: false, // Assume all archived versions are stable + download_url: Some(format!( + "https://archive.torproject.org/tor-package-archive/torbrowser/{}/tor-browser-macos-{}.dmg", + version, version + )), + } + }).collect()) + } + + async fn check_tor_version_has_macos(&self, version: &str) -> Result> { + let url = format!("https://archive.torproject.org/tor-package-archive/torbrowser/{}/", version); + let html = self + .client + .get(&url) + .header("User-Agent", "donutbrowser") + .send() + .await? + .text() + .await?; + + // Check if there's a macOS DMG file in this version directory + Ok(html.contains("tor-browser-macos-") && html.contains(".dmg")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio; + + #[test] + fn test_version_parsing() { + // Test basic version parsing + let v1 = VersionComponent::parse("1.2.3"); + assert_eq!(v1.major, 1); + assert_eq!(v1.minor, 2); + assert_eq!(v1.patch, 3); + assert!(v1.pre_release.is_none()); + + // Test alpha version + let v2 = VersionComponent::parse("1.2.3a1"); + assert_eq!(v2.major, 1); + assert_eq!(v2.minor, 2); + assert_eq!(v2.patch, 3); + assert!(v2.pre_release.is_some()); + let pre = v2.pre_release.unwrap(); + assert_eq!(pre.kind, PreReleaseKind::Alpha); + assert_eq!(pre.number, Some(1)); + + // Test beta version + let v3 = VersionComponent::parse("137.0b5"); + assert_eq!(v3.major, 137); + assert_eq!(v3.minor, 0); + assert_eq!(v3.patch, 0); + assert!(v3.pre_release.is_some()); + let pre = v3.pre_release.unwrap(); + assert_eq!(pre.kind, PreReleaseKind::Beta); + assert_eq!(pre.number, Some(5)); + + // Test twilight version (Zen Browser) + let v4 = VersionComponent::parse("1.0.0-twilight"); + assert_eq!(v4.major, u32::MAX); + assert_eq!(v4.minor, u32::MAX); + assert_eq!(v4.patch, u32::MAX); + } + + #[test] + fn test_version_comparison() { + // Test basic version comparison + let v1 = VersionComponent::parse("1.2.3"); + let v2 = VersionComponent::parse("1.2.4"); + assert!(v2 > v1); + + // Test major version difference + let v3 = VersionComponent::parse("2.0.0"); + let v4 = VersionComponent::parse("1.9.9"); + assert!(v3 > v4); + + // Test stable vs pre-release + let v5 = VersionComponent::parse("1.2.3"); + let v6 = VersionComponent::parse("1.2.3b1"); + assert!(v5 > v6); // Stable > beta + + // Test different pre-release types + let v7 = VersionComponent::parse("1.2.3a1"); + let v8 = VersionComponent::parse("1.2.3b1"); + assert!(v8 > v7); // Beta > alpha + + // Test pre-release numbers + let v9 = VersionComponent::parse("137.0b4"); + let v10 = VersionComponent::parse("137.0b5"); + assert!(v10 > v9); // b5 > b4 + + // Test twilight version (should be highest) + let v11 = VersionComponent::parse("1.0.0-twilight"); + let v12 = VersionComponent::parse("999.999.999"); + assert!(v11 > v12); + } + + #[test] + fn test_version_sorting() { + let mut versions = vec![ + "1.9.9b".to_string(), + "1.12.6b".to_string(), + "1.10.0".to_string(), + "137.0b4".to_string(), + "137.0b5".to_string(), + "137.0".to_string(), + "1.0.0-twilight".to_string(), + "2.0.0a1".to_string(), + ]; + + sort_versions(&mut versions); + + // Expected order: twilight, 137.0, 137.0b5, 137.0b4, 2.0.0a1, 1.12.6b, 1.10.0, 1.9.9b + assert_eq!(versions[0], "1.0.0-twilight"); + assert_eq!(versions[1], "137.0"); + assert_eq!(versions[2], "137.0b5"); + assert_eq!(versions[3], "137.0b4"); + assert_eq!(versions[4], "2.0.0a1"); + assert_eq!(versions[5], "1.12.6b"); + assert_eq!(versions[6], "1.10.0"); + assert_eq!(versions[7], "1.9.9b"); + } + + #[tokio::test] + async fn test_firefox_api() { + let client = ApiClient::new(); + let result = client.fetch_firefox_releases_with_caching(false).await; + + match result { + Ok(releases) => { + assert!(!releases.is_empty(), "Should have Firefox releases"); + + // Check that releases have required fields + let first_release = &releases[0]; + assert!(!first_release.version.is_empty(), "Version should not be empty"); + assert!(first_release.download_url.is_some(), "Should have download URL"); + + println!("Firefox API test passed. Found {} releases", releases.len()); + println!("Latest version: {}", first_release.version); + } + Err(e) => { + println!("Firefox API test failed: {}", e); + panic!("Firefox API should work"); + } + } + } + + #[tokio::test] + async fn test_firefox_developer_api() { + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; // Rate limiting + + let client = ApiClient::new(); + let result = client.fetch_firefox_developer_releases_with_caching(false).await; + + match result { + Ok(releases) => { + assert!(!releases.is_empty(), "Should have Firefox Developer releases"); + + let first_release = &releases[0]; + assert!(!first_release.version.is_empty(), "Version should not be empty"); + assert!(first_release.download_url.is_some(), "Should have download URL"); + + println!("Firefox Developer API test passed. Found {} releases", releases.len()); + println!("Latest version: {}", first_release.version); + } + Err(e) => { + println!("Firefox Developer API test failed: {}", e); + panic!("Firefox Developer API should work"); + } + } + } + + #[tokio::test] + async fn test_mullvad_api() { + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; // Rate limiting + + let client = ApiClient::new(); + let result = client.fetch_mullvad_releases().await; + + match result { + Ok(releases) => { + assert!(!releases.is_empty(), "Should have Mullvad releases"); + + let first_release = &releases[0]; + assert!(!first_release.tag_name.is_empty(), "Tag name should not be empty"); + + println!("Mullvad API test passed. Found {} releases", releases.len()); + println!("Latest version: {}", first_release.tag_name); + } + Err(e) => { + println!("Mullvad API test failed: {}", e); + panic!("Mullvad API should work"); + } + } + } + + #[tokio::test] + async fn test_zen_api() { + tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await; // Rate limiting + + let client = ApiClient::new(); + let result = client.fetch_zen_releases().await; + + match result { + Ok(releases) => { + assert!(!releases.is_empty(), "Should have Zen releases"); + + let first_release = &releases[0]; + assert!(!first_release.tag_name.is_empty(), "Tag name should not be empty"); + + println!("Zen API test passed. Found {} releases", releases.len()); + println!("Latest version: {}", first_release.tag_name); + } + Err(e) => { + println!("Zen API test failed: {}", e); + panic!("Zen API should work"); + } + } + } + + #[tokio::test] + async fn test_brave_api() { + tokio::time::sleep(tokio::time::Duration::from_millis(2000)).await; // Rate limiting + + let client = ApiClient::new(); + let result = client.fetch_brave_releases().await; + + match result { + Ok(releases) => { + // Note: Brave might not always have macOS releases, so we don't assert non-empty + println!("Brave API test passed. Found {} releases with macOS assets", releases.len()); + if !releases.is_empty() { + println!("Latest version: {}", releases[0].tag_name); + } + } + Err(e) => { + println!("Brave API test failed: {}", e); + panic!("Brave API should work"); + } + } + } + + #[tokio::test] + async fn test_chromium_api() { + tokio::time::sleep(tokio::time::Duration::from_millis(2500)).await; // Rate limiting + + let client = ApiClient::new(); + let result = client.fetch_chromium_latest_version().await; + + match result { + Ok(version) => { + assert!(!version.is_empty(), "Version should not be empty"); + assert!(version.chars().all(|c| c.is_ascii_digit()), "Version should be numeric"); + + println!("Chromium API test passed. Latest version: {}", version); + } + Err(e) => { + println!("Chromium API test failed: {}", e); + panic!("Chromium API should work"); + } + } + } + + #[tokio::test] + async fn test_tor_api() { + tokio::time::sleep(tokio::time::Duration::from_millis(3000)).await; // Rate limiting + + let client = ApiClient::new(); + + // Use a timeout for this test since TOR API can be slow + let timeout_duration = tokio::time::Duration::from_secs(30); + let result = tokio::time::timeout(timeout_duration, client.fetch_tor_releases_with_caching(false)).await; + + match result { + Ok(Ok(releases)) => { + assert!(!releases.is_empty(), "Should have TOR releases"); + + let first_release = &releases[0]; + assert!(!first_release.version.is_empty(), "Version should not be empty"); + assert!(first_release.download_url.is_some(), "Should have download URL"); + + println!("TOR API test passed. Found {} releases", releases.len()); + println!("Latest version: {}", first_release.version); + } + Ok(Err(e)) => { + println!("TOR API test failed: {}", e); + // Don't panic for TOR API since it can be unreliable + println!("TOR API test skipped due to network issues"); + } + Err(_) => { + println!("TOR API test timed out after 30 seconds"); + // Don't panic for timeout, just skip + println!("TOR API test skipped due to timeout"); + } + } + } + + #[tokio::test] + async fn test_tor_version_check() { + tokio::time::sleep(tokio::time::Duration::from_millis(3500)).await; // Rate limiting + + let client = ApiClient::new(); + let result = client.check_tor_version_has_macos("14.0.4").await; + + match result { + Ok(has_macos) => { + assert!(has_macos, "Version 14.0.4 should have macOS support"); + println!("TOR version check test passed. Version 14.0.4 has macOS: {}", has_macos); + } + Err(e) => { + println!("TOR version check test failed: {}", e); + panic!("TOR version check should work"); + } + } + } +} \ No newline at end of file diff --git a/src-tauri/src/auto_updater.rs b/src-tauri/src/auto_updater.rs new file mode 100644 index 0000000..89904b5 --- /dev/null +++ b/src-tauri/src/auto_updater.rs @@ -0,0 +1,778 @@ +use crate::browser_version_service::{BrowserVersionService, BrowserVersionInfo}; +use crate::browser_runner::{BrowserProfile, BrowserRunner}; +use crate::settings_manager::SettingsManager; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +use std::fs; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UpdateNotification { + pub id: String, + pub browser: String, + pub current_version: String, + pub new_version: String, + pub affected_profiles: Vec, + pub is_stable_update: bool, + pub timestamp: u64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AutoUpdateState { + pub pending_updates: Vec, + pub disabled_browsers: HashSet, // browsers disabled during update + #[serde(default)] + pub auto_update_downloads: HashSet, // track auto-update downloads for toast suppression + pub last_check_timestamp: u64, +} + +impl Default for AutoUpdateState { + fn default() -> Self { + Self { + pending_updates: Vec::new(), + disabled_browsers: HashSet::new(), + auto_update_downloads: HashSet::new(), + last_check_timestamp: 0, + } + } +} + +pub struct AutoUpdater { + version_service: BrowserVersionService, + browser_runner: BrowserRunner, + settings_manager: SettingsManager, +} + +impl AutoUpdater { + pub fn new() -> Self { + Self { + version_service: BrowserVersionService::new(), + browser_runner: BrowserRunner::new(), + settings_manager: SettingsManager::new(), + } + } + + /// Check for updates for all profiles + pub async fn check_for_updates(&self) -> Result, Box> { + // Check if auto-updates are enabled + let settings = self.settings_manager.load_settings() + .map_err(|e| format!("Failed to load settings: {}", e))?; + if !settings.auto_updates_enabled { + return Ok(Vec::new()); + } + + let profiles = self.browser_runner.list_profiles() + .map_err(|e| format!("Failed to list profiles: {}", e))?; + let mut notifications = Vec::new(); + let mut browser_versions: HashMap> = HashMap::new(); + + // Group profiles by browser type + let mut browser_profiles: HashMap> = HashMap::new(); + for profile in profiles { + browser_profiles + .entry(profile.browser.clone()) + .or_insert_with(Vec::new) + .push(profile); + } + + // Check each browser type + for (browser, profiles) in browser_profiles { + // Get cached versions first, then try to fetch if needed + let versions = if let Some(cached) = self.version_service.get_cached_browser_versions_detailed(&browser) { + cached + } else if self.version_service.should_update_cache(&browser) { + // Try to fetch fresh versions + match self.version_service.fetch_browser_versions_detailed(&browser, false).await { + Ok(versions) => versions, + Err(_) => continue, // Skip this browser if fetch fails + } + } else { + continue; // No cached versions and cache doesn't need update + }; + + browser_versions.insert(browser.clone(), versions.clone()); + + // Check each profile for updates + for profile in profiles { + if let Some(update) = self.check_profile_update(&profile, &versions)? { + notifications.push(update); + } + } + } + + Ok(notifications) + } + + /// Check if a specific profile has an available update + fn check_profile_update( + &self, + profile: &BrowserProfile, + available_versions: &[BrowserVersionInfo], + ) -> Result, Box> { + let current_version = &profile.version; + let is_current_stable = !self.is_alpha_version(current_version); + + // Find the best available update + let best_update = available_versions + .iter() + .filter(|v| { + // Only consider versions newer than current + self.is_version_newer(&v.version, current_version) && + // Respect version type preference + (is_current_stable == !v.is_prerelease || !is_current_stable) + }) + .max_by(|a, b| self.compare_versions(&a.version, &b.version)); + + if let Some(update_version) = best_update { + let notification = UpdateNotification { + id: format!("{}_{}_to_{}", profile.browser, current_version, update_version.version), + browser: profile.browser.clone(), + current_version: current_version.clone(), + new_version: update_version.version.clone(), + affected_profiles: vec![profile.name.clone()], + is_stable_update: !update_version.is_prerelease, + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + }; + Ok(Some(notification)) + } else { + Ok(None) + } + } + + /// Group update notifications by browser and version + pub fn group_update_notifications(&self, notifications: Vec) -> Vec { + let mut grouped: HashMap = HashMap::new(); + + for notification in notifications { + let key = format!("{}_{}", notification.browser, notification.new_version); + + if let Some(existing) = grouped.get_mut(&key) { + // Merge affected profiles + existing.affected_profiles.extend(notification.affected_profiles); + existing.affected_profiles.sort(); + existing.affected_profiles.dedup(); + } else { + grouped.insert(key, notification); + } + } + + let mut result: Vec = grouped.into_values().collect(); + + // Sort by priority: stable updates first, then by timestamp + result.sort_by(|a, b| { + match (a.is_stable_update, b.is_stable_update) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => b.timestamp.cmp(&a.timestamp), + } + }); + + result + } + + /// Mark download as auto-update + pub fn mark_auto_update_download( + &self, + browser: &str, + version: &str, + ) -> Result<(), Box> { + let mut state = self.load_auto_update_state()?; + let download_key = format!("{}-{}", browser, version); + state.auto_update_downloads.insert(download_key); + self.save_auto_update_state(&state)?; + Ok(()) + } + + /// Remove auto-update download tracking + pub fn remove_auto_update_download( + &self, + browser: &str, + version: &str, + ) -> Result<(), Box> { + let mut state = self.load_auto_update_state()?; + let download_key = format!("{}-{}", browser, version); + state.auto_update_downloads.remove(&download_key); + self.save_auto_update_state(&state)?; + Ok(()) + } + + /// Check if download is marked as auto-update + pub fn is_auto_update_download( + &self, + browser: &str, + version: &str, + ) -> Result> { + let state = self.load_auto_update_state()?; + let download_key = format!("{}-{}", browser, version); + Ok(state.auto_update_downloads.contains(&download_key)) + } + + /// Start browser update process + pub async fn start_browser_update( + &self, + browser: &str, + new_version: &str, + ) -> Result<(), Box> { + // Add browser to disabled list to prevent conflicts during update + let mut state = self.load_auto_update_state()?; + state.disabled_browsers.insert(browser.to_string()); + + // Mark this download as auto-update for toast suppression + let download_key = format!("{}-{}", browser, new_version); + state.auto_update_downloads.insert(download_key); + + self.save_auto_update_state(&state)?; + + // The actual download will be triggered by the frontend + // This function now just marks the browser as updating to prevent conflicts + Ok(()) + } + + /// Complete browser update process + pub async fn complete_browser_update( + &self, + browser: &str, + ) -> Result<(), Box> { + // Remove browser from disabled list + let mut state = self.load_auto_update_state()?; + state.disabled_browsers.remove(browser); + self.save_auto_update_state(&state)?; + + Ok(()) + } + + /// Automatically update all affected profile versions after browser download + pub async fn auto_update_profile_versions( + &self, + browser: &str, + new_version: &str, + ) -> Result, Box> { + let profiles = self.browser_runner.list_profiles() + .map_err(|e| format!("Failed to list profiles: {}", e))?; + + let mut updated_profiles = Vec::new(); + + // Find all profiles for this browser that should be updated + for profile in profiles { + if profile.browser == browser { + // Check if profile is currently running + if profile.process_id.is_some() { + continue; // Skip running profiles + } + + // Check if this is an update (newer version) + if self.is_version_newer(new_version, &profile.version) { + // Update the profile version + match self.browser_runner.update_profile_version(&profile.name, new_version) { + Ok(_) => { + updated_profiles.push(profile.name); + } + Err(e) => { + eprintln!("Failed to update profile {}: {}", profile.name, e); + } + } + } + } + } + + Ok(updated_profiles) + } + + /// Complete browser update process with auto-update of profile versions + pub async fn complete_browser_update_with_auto_update( + &self, + browser: &str, + new_version: &str, + ) -> Result, Box> { + // Auto-update profile versions first + let updated_profiles = self.auto_update_profile_versions(browser, new_version).await?; + + // Remove browser from disabled list and clean up auto-update tracking + let mut state = self.load_auto_update_state()?; + state.disabled_browsers.remove(browser); + let download_key = format!("{}-{}", browser, new_version); + state.auto_update_downloads.remove(&download_key); + self.save_auto_update_state(&state)?; + + Ok(updated_profiles) + } + + /// Check if browser is disabled due to ongoing update + pub fn is_browser_disabled(&self, browser: &str) -> Result> { + let state = self.load_auto_update_state()?; + Ok(state.disabled_browsers.contains(browser)) + } + + /// Dismiss update notification + pub fn dismiss_update_notification(&self, notification_id: &str) -> Result<(), Box> { + let mut state = self.load_auto_update_state()?; + state.pending_updates.retain(|n| n.id != notification_id); + self.save_auto_update_state(&state)?; + Ok(()) + } + + // Helper methods + + fn is_alpha_version(&self, version: &str) -> bool { + version.contains("alpha") || version.contains("beta") || version.contains("rc") || + version.contains("a") || version.contains("b") || version.contains("dev") + } + + fn is_version_newer(&self, version1: &str, version2: &str) -> bool { + self.compare_versions(version1, version2) == std::cmp::Ordering::Greater + } + + fn compare_versions(&self, version1: &str, version2: &str) -> std::cmp::Ordering { + // Basic semantic version comparison + let v1_parts = self.parse_version(version1); + let v2_parts = self.parse_version(version2); + + v1_parts.cmp(&v2_parts) + } + + fn parse_version(&self, version: &str) -> Vec { + version + .split(&['.', 'a', 'b', '-', '_'][..]) + .filter_map(|part| part.parse::().ok()) + .collect() + } + + fn get_auto_update_state_file(&self) -> PathBuf { + self.settings_manager.get_settings_dir().join("auto_update_state.json") + } + + fn load_auto_update_state(&self) -> Result> { + let state_file = self.get_auto_update_state_file(); + + if !state_file.exists() { + return Ok(AutoUpdateState::default()); + } + + let content = fs::read_to_string(state_file)?; + let state: AutoUpdateState = serde_json::from_str(&content)?; + Ok(state) + } + + fn save_auto_update_state(&self, state: &AutoUpdateState) -> Result<(), Box> { + let settings_dir = self.settings_manager.get_settings_dir(); + std::fs::create_dir_all(&settings_dir)?; + + let state_file = self.get_auto_update_state_file(); + let json = serde_json::to_string_pretty(state)?; + fs::write(state_file, json)?; + + Ok(()) + } +} + +// Tauri commands + +#[tauri::command] +pub async fn check_for_browser_updates() -> Result, String> { + let updater = AutoUpdater::new(); + let notifications = updater.check_for_updates().await + .map_err(|e| format!("Failed to check for updates: {}", e))?; + let grouped = updater.group_update_notifications(notifications); + Ok(grouped) +} + +#[tauri::command] +pub async fn start_browser_update(browser: String, new_version: String) -> Result<(), String> { + let updater = AutoUpdater::new(); + updater.start_browser_update(&browser, &new_version).await + .map_err(|e| format!("Failed to start browser update: {}", e)) +} + +#[tauri::command] +pub async fn complete_browser_update(browser: String) -> Result<(), String> { + let updater = AutoUpdater::new(); + updater.complete_browser_update(&browser).await + .map_err(|e| format!("Failed to complete browser update: {}", e)) +} + +#[tauri::command] +pub async fn is_browser_disabled_for_update(browser: String) -> Result { + let updater = AutoUpdater::new(); + updater.is_browser_disabled(&browser) + .map_err(|e| format!("Failed to check browser status: {}", e)) +} + +#[tauri::command] +pub async fn dismiss_update_notification(notification_id: String) -> Result<(), String> { + let updater = AutoUpdater::new(); + updater.dismiss_update_notification(¬ification_id) + .map_err(|e| format!("Failed to dismiss notification: {}", e)) +} + +#[tauri::command] +pub async fn complete_browser_update_with_auto_update(browser: String, new_version: String) -> Result, String> { + let updater = AutoUpdater::new(); + updater.complete_browser_update_with_auto_update(&browser, &new_version).await + .map_err(|e| format!("Failed to complete browser update: {}", e)) +} + +#[tauri::command] +pub async fn mark_auto_update_download(browser: String, version: String) -> Result<(), String> { + let updater = AutoUpdater::new(); + updater.mark_auto_update_download(&browser, &version) + .map_err(|e| format!("Failed to mark auto-update download: {}", e)) +} + +#[tauri::command] +pub async fn remove_auto_update_download(browser: String, version: String) -> Result<(), String> { + let updater = AutoUpdater::new(); + updater.remove_auto_update_download(&browser, &version) + .map_err(|e| format!("Failed to remove auto-update download: {}", e)) +} + +#[tauri::command] +pub async fn is_auto_update_download(browser: String, version: String) -> Result { + let updater = AutoUpdater::new(); + updater.is_auto_update_download(&browser, &version) + .map_err(|e| format!("Failed to check auto-update download: {}", e)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_test_profile(name: &str, browser: &str, version: &str) -> BrowserProfile { + BrowserProfile { + name: name.to_string(), + browser: browser.to_string(), + version: version.to_string(), + profile_path: format!("/tmp/{}", name), + process_id: None, + proxy: None, + last_launch: None, + } + } + + fn create_test_version_info(version: &str, is_prerelease: bool) -> BrowserVersionInfo { + BrowserVersionInfo { + version: version.to_string(), + is_prerelease, + date: "2024-01-01".to_string(), + } + } + + #[test] + fn test_is_alpha_version() { + let updater = AutoUpdater::new(); + + assert!(updater.is_alpha_version("1.0.0-alpha")); + assert!(updater.is_alpha_version("1.0.0-beta")); + assert!(updater.is_alpha_version("1.0.0-rc")); + assert!(updater.is_alpha_version("1.0.0a1")); + assert!(updater.is_alpha_version("1.0.0b1")); + assert!(updater.is_alpha_version("1.0.0-dev")); + + assert!(!updater.is_alpha_version("1.0.0")); + assert!(!updater.is_alpha_version("1.2.3")); + } + + #[test] + fn test_compare_versions() { + let updater = AutoUpdater::new(); + + assert_eq!(updater.compare_versions("1.0.0", "1.0.0"), std::cmp::Ordering::Equal); + assert_eq!(updater.compare_versions("1.0.1", "1.0.0"), std::cmp::Ordering::Greater); + assert_eq!(updater.compare_versions("1.0.0", "1.0.1"), std::cmp::Ordering::Less); + assert_eq!(updater.compare_versions("2.0.0", "1.9.9"), std::cmp::Ordering::Greater); + assert_eq!(updater.compare_versions("1.10.0", "1.9.0"), std::cmp::Ordering::Greater); + } + + #[test] + fn test_is_version_newer() { + let updater = AutoUpdater::new(); + + assert!(updater.is_version_newer("1.0.1", "1.0.0")); + assert!(updater.is_version_newer("2.0.0", "1.9.9")); + assert!(!updater.is_version_newer("1.0.0", "1.0.1")); + assert!(!updater.is_version_newer("1.0.0", "1.0.0")); + } + + #[test] + fn test_check_profile_update_stable_to_stable() { + let updater = AutoUpdater::new(); + let profile = create_test_profile("test", "firefox", "1.0.0"); + let versions = vec![ + create_test_version_info("1.0.1", false), // stable, newer + create_test_version_info("1.1.0-alpha", true), // alpha, should be ignored + create_test_version_info("0.9.0", false), // stable, older + ]; + + let result = updater.check_profile_update(&profile, &versions).unwrap(); + assert!(result.is_some()); + + let update = result.unwrap(); + assert_eq!(update.new_version, "1.0.1"); + assert!(update.is_stable_update); + } + + #[test] + fn test_check_profile_update_alpha_to_alpha() { + let updater = AutoUpdater::new(); + let profile = create_test_profile("test", "firefox", "1.0.0-alpha"); + let versions = vec![ + create_test_version_info("1.0.1", false), // stable, should be included + create_test_version_info("1.1.0-alpha", true), // alpha, newer + create_test_version_info("0.9.0-alpha", true), // alpha, older + ]; + + let result = updater.check_profile_update(&profile, &versions).unwrap(); + assert!(result.is_some()); + + let update = result.unwrap(); + // Should pick the newest version (alpha user can upgrade to stable or newer alpha) + assert_eq!(update.new_version, "1.1.0-alpha"); + assert!(!update.is_stable_update); + } + + #[test] + fn test_check_profile_update_no_update_available() { + let updater = AutoUpdater::new(); + let profile = create_test_profile("test", "firefox", "1.0.0"); + let versions = vec![ + create_test_version_info("0.9.0", false), // older + create_test_version_info("1.0.0", false), // same version + ]; + + let result = updater.check_profile_update(&profile, &versions).unwrap(); + assert!(result.is_none()); + } + + #[test] + fn test_group_update_notifications() { + let updater = AutoUpdater::new(); + let notifications = vec![ + UpdateNotification { + id: "firefox_1.0.0_to_1.1.0_profile1".to_string(), + browser: "firefox".to_string(), + current_version: "1.0.0".to_string(), + new_version: "1.1.0".to_string(), + affected_profiles: vec!["profile1".to_string()], + is_stable_update: true, + timestamp: 1000, + }, + UpdateNotification { + id: "firefox_1.0.0_to_1.1.0_profile2".to_string(), + browser: "firefox".to_string(), + current_version: "1.0.0".to_string(), + new_version: "1.1.0".to_string(), + affected_profiles: vec!["profile2".to_string()], + is_stable_update: true, + timestamp: 1001, + }, + UpdateNotification { + id: "chrome_1.0.0_to_1.1.0-alpha".to_string(), + browser: "chrome".to_string(), + current_version: "1.0.0".to_string(), + new_version: "1.1.0-alpha".to_string(), + affected_profiles: vec!["profile3".to_string()], + is_stable_update: false, + timestamp: 1002, + }, + ]; + + let grouped = updater.group_update_notifications(notifications); + + assert_eq!(grouped.len(), 2); + + // Find the Firefox notification + let firefox_notification = grouped.iter().find(|n| n.browser == "firefox").unwrap(); + assert_eq!(firefox_notification.affected_profiles.len(), 2); + assert!(firefox_notification.affected_profiles.contains(&"profile1".to_string())); + assert!(firefox_notification.affected_profiles.contains(&"profile2".to_string())); + + // Stable updates should come first + assert!(grouped[0].is_stable_update); + } + + #[test] + fn test_auto_update_state_persistence() { + use tempfile::TempDir; + use std::sync::Once; + + static INIT: Once = Once::new(); + INIT.call_once(|| { + // Initialize any required static data + }); + + // Create a temporary directory for testing + let temp_dir = TempDir::new().unwrap(); + + // Create a mock settings manager that uses the temp directory + struct TestSettingsManager { + settings_dir: std::path::PathBuf, + } + + impl TestSettingsManager { + fn new(settings_dir: std::path::PathBuf) -> Self { + Self { settings_dir } + } + + fn get_settings_dir(&self) -> std::path::PathBuf { + self.settings_dir.clone() + } + } + + let test_settings_manager = TestSettingsManager::new(temp_dir.path().to_path_buf()); + + let mut state = AutoUpdateState::default(); + state.disabled_browsers.insert("firefox".to_string()); + state.auto_update_downloads.insert("firefox-1.1.0".to_string()); + state.pending_updates.push(UpdateNotification { + id: "test".to_string(), + browser: "firefox".to_string(), + current_version: "1.0.0".to_string(), + new_version: "1.1.0".to_string(), + affected_profiles: vec!["profile1".to_string()], + is_stable_update: true, + timestamp: 1000, + }); + + // Test save and load + let state_file = test_settings_manager.get_settings_dir().join("auto_update_state.json"); + std::fs::create_dir_all(test_settings_manager.get_settings_dir()).unwrap(); + let json = serde_json::to_string_pretty(&state).unwrap(); + std::fs::write(&state_file, json).unwrap(); + + // Load state + let content = std::fs::read_to_string(&state_file).unwrap(); + let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap(); + + assert_eq!(loaded_state.disabled_browsers.len(), 1); + assert!(loaded_state.disabled_browsers.contains("firefox")); + assert_eq!(loaded_state.auto_update_downloads.len(), 1); + assert!(loaded_state.auto_update_downloads.contains("firefox-1.1.0")); + assert_eq!(loaded_state.pending_updates.len(), 1); + assert_eq!(loaded_state.pending_updates[0].id, "test"); + } + + #[tokio::test] + async fn test_browser_disable_enable_cycle() { + use tempfile::TempDir; + + // Create a temporary directory for testing + let temp_dir = TempDir::new().unwrap(); + + // Create a mock settings manager that uses the temp directory + struct TestSettingsManager { + settings_dir: std::path::PathBuf, + } + + impl TestSettingsManager { + fn new(settings_dir: std::path::PathBuf) -> Self { + Self { settings_dir } + } + + fn get_settings_dir(&self) -> std::path::PathBuf { + self.settings_dir.clone() + } + } + + let test_settings_manager = TestSettingsManager::new(temp_dir.path().to_path_buf()); + + // Test browser disable/enable cycle with manual state management + let state_file = test_settings_manager.get_settings_dir().join("auto_update_state.json"); + std::fs::create_dir_all(test_settings_manager.get_settings_dir()).unwrap(); + + // Initially not disabled (empty state file means default state) + let state = AutoUpdateState::default(); + assert!(!state.disabled_browsers.contains("firefox")); + + // Start update (should disable) + let mut state = AutoUpdateState::default(); + state.disabled_browsers.insert("firefox".to_string()); + state.auto_update_downloads.insert("firefox-1.1.0".to_string()); + let json = serde_json::to_string_pretty(&state).unwrap(); + std::fs::write(&state_file, json).unwrap(); + + // Check that it's disabled + let content = std::fs::read_to_string(&state_file).unwrap(); + let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap(); + assert!(loaded_state.disabled_browsers.contains("firefox")); + assert!(loaded_state.auto_update_downloads.contains("firefox-1.1.0")); + + // Complete update (should enable) + let mut state = loaded_state; + state.disabled_browsers.remove("firefox"); + state.auto_update_downloads.remove("firefox-1.1.0"); + let json = serde_json::to_string_pretty(&state).unwrap(); + std::fs::write(&state_file, json).unwrap(); + + // Check that it's enabled again + let content = std::fs::read_to_string(&state_file).unwrap(); + let final_state: AutoUpdateState = serde_json::from_str(&content).unwrap(); + assert!(!final_state.disabled_browsers.contains("firefox")); + assert!(!final_state.auto_update_downloads.contains("firefox-1.1.0")); + } + + #[test] + fn test_dismiss_update_notification() { + use tempfile::TempDir; + + // Create a temporary directory for testing + let temp_dir = TempDir::new().unwrap(); + + // Create a mock settings manager that uses the temp directory + struct TestSettingsManager { + settings_dir: std::path::PathBuf, + } + + impl TestSettingsManager { + fn new(settings_dir: std::path::PathBuf) -> Self { + Self { settings_dir } + } + + fn get_settings_dir(&self) -> std::path::PathBuf { + self.settings_dir.clone() + } + } + + let test_settings_manager = TestSettingsManager::new(temp_dir.path().to_path_buf()); + + let mut state = AutoUpdateState::default(); + state.pending_updates.push(UpdateNotification { + id: "test_notification".to_string(), + browser: "firefox".to_string(), + current_version: "1.0.0".to_string(), + new_version: "1.1.0".to_string(), + affected_profiles: vec!["profile1".to_string()], + is_stable_update: true, + timestamp: 1000, + }); + + // Save initial state + let state_file = test_settings_manager.get_settings_dir().join("auto_update_state.json"); + std::fs::create_dir_all(test_settings_manager.get_settings_dir()).unwrap(); + let json = serde_json::to_string_pretty(&state).unwrap(); + std::fs::write(&state_file, json).unwrap(); + + // Dismiss notification (remove from pending updates) + let mut state = state; + state.pending_updates.retain(|n| n.id != "test_notification"); + let json = serde_json::to_string_pretty(&state).unwrap(); + std::fs::write(&state_file, json).unwrap(); + + // Check that it's removed + let content = std::fs::read_to_string(&state_file).unwrap(); + let loaded_state: AutoUpdateState = serde_json::from_str(&content).unwrap(); + assert_eq!(loaded_state.pending_updates.len(), 0); + } + + #[test] + fn test_parse_version() { + let updater = AutoUpdater::new(); + + assert_eq!(updater.parse_version("1.2.3"), vec![1, 2, 3]); + assert_eq!(updater.parse_version("1.2.3-alpha"), vec![1, 2, 3]); + assert_eq!(updater.parse_version("1.2.3a1"), vec![1, 2, 3, 1]); + assert_eq!(updater.parse_version("1.2.3b2"), vec![1, 2, 3, 2]); + assert_eq!(updater.parse_version("10.0.0"), vec![10, 0, 0]); + } +} \ No newline at end of file diff --git a/src-tauri/src/browser.rs b/src-tauri/src/browser.rs new file mode 100644 index 0000000..8451d96 --- /dev/null +++ b/src-tauri/src/browser.rs @@ -0,0 +1,557 @@ +use base64::{engine::general_purpose, Engine as _}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ProxySettings { + pub enabled: bool, + pub proxy_type: String, // "http", "https", "socks4", or "socks5" + pub host: String, + pub port: u16, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum BrowserType { + MullvadBrowser, + Chromium, + Firefox, + FirefoxDeveloper, + Brave, + Zen, + TorBrowser, +} + +impl BrowserType { + pub fn as_str(&self) -> &'static str { + match self { + BrowserType::MullvadBrowser => "mullvad-browser", + BrowserType::Chromium => "chromium", + BrowserType::Firefox => "firefox", + BrowserType::FirefoxDeveloper => "firefox-developer", + BrowserType::Brave => "brave", + BrowserType::Zen => "zen", + BrowserType::TorBrowser => "tor-browser", + } + } + + pub fn from_str(s: &str) -> Result { + match s { + "mullvad-browser" => Ok(BrowserType::MullvadBrowser), + "chromium" => Ok(BrowserType::Chromium), + "firefox" => Ok(BrowserType::Firefox), + "firefox-developer" => Ok(BrowserType::FirefoxDeveloper), + "brave" => Ok(BrowserType::Brave), + "zen" => Ok(BrowserType::Zen), + "tor-browser" => Ok(BrowserType::TorBrowser), + _ => Err(format!("Unknown browser type: {}", s)), + } + } +} + +pub trait Browser: Send + Sync { + fn browser_type(&self) -> BrowserType; + fn get_executable_path(&self, install_dir: &Path) -> Result>; + fn create_launch_args( + &self, + profile_path: &str, + _proxy_settings: Option<&ProxySettings>, + url: Option, + ) -> Result, Box>; + fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool; +} + + + +pub struct FirefoxBrowser { + browser_type: BrowserType, +} + +impl FirefoxBrowser { + pub fn new(browser_type: BrowserType) -> Self { + Self { browser_type } + } + +} + +impl Browser for FirefoxBrowser { + fn browser_type(&self) -> BrowserType { + self.browser_type.clone() + } + + + + fn get_executable_path(&self, install_dir: &Path) -> Result> { + // Find the .app directory + let app_path = std::fs::read_dir(install_dir)? + .filter_map(Result::ok) + .find(|entry| entry.path().extension().map_or(false, |ext| ext == "app")) + .ok_or("Browser app not found")?; + + // Construct the browser executable path + let mut executable_dir = app_path.path(); + executable_dir.push("Contents"); + executable_dir.push("MacOS"); + + // Find the first executable in the MacOS directory + let executable_path = std::fs::read_dir(&executable_dir)? + .filter_map(Result::ok) + .find(|entry| { + let binding = entry.file_name(); + let name = binding.to_string_lossy(); + name.starts_with("firefox") || name.starts_with("mullvad") || name.starts_with("zen") || name.starts_with("tor") || name.contains("Browser") + }) + .map(|entry| entry.path()) + .ok_or("No executable found in MacOS directory")?; + + Ok(executable_path) + } + + fn create_launch_args( + &self, + profile_path: &str, + _proxy_settings: Option<&ProxySettings>, + url: Option, + ) -> Result, Box> { + let mut args = vec![ + "-profile".to_string(), + profile_path.to_string(), + ]; + + // Only use -no-remote for browsers that require it for security (Mullvad, Tor) + // Regular Firefox browsers can use remote commands for better URL handling + match self.browser_type { + BrowserType::MullvadBrowser | BrowserType::TorBrowser => { + args.push("-no-remote".to_string()); + } + BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen => { + // Don't use -no-remote so we can communicate with existing instances + } + _ => {} + } + + // Firefox-based browsers use profile directory and user.js for proxy configuration + if let Some(url) = url { + args.push(url); + } + + Ok(args) + } + + fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool { + let browser_dir = binaries_dir + .join(self.browser_type().as_str()) + .join(version); + + println!("Firefox browser checking version {} in directory: {:?}", version, browser_dir); + + // Only check if directory exists and contains a .app file + if browser_dir.exists() { + println!("Directory exists, checking for .app files..."); + if let Ok(entries) = std::fs::read_dir(&browser_dir) { + for entry in entries { + if let Ok(entry) = entry { + println!(" Found entry: {:?}", entry.path()); + if entry.path().extension().map_or(false, |ext| ext == "app") { + println!(" Found .app file: {:?}", entry.path()); + return true; + } + } + } + } + println!("No .app files found in directory"); + } else { + println!("Directory does not exist: {:?}", browser_dir); + } + false + } +} + +// Chromium-based browsers (Chromium, Brave) +pub struct ChromiumBrowser { + browser_type: BrowserType, +} + +impl ChromiumBrowser { + pub fn new(browser_type: BrowserType) -> Self { + Self { browser_type } + } +} + +impl Browser for ChromiumBrowser { + fn browser_type(&self) -> BrowserType { + self.browser_type.clone() + } + + + fn get_executable_path(&self, install_dir: &Path) -> Result> { + // Find the .app directory + let app_path = std::fs::read_dir(install_dir)? + .filter_map(Result::ok) + .find(|entry| entry.path().extension().map_or(false, |ext| ext == "app")) + .ok_or("Browser app not found")?; + + // Construct the browser executable path + let mut executable_dir = app_path.path(); + executable_dir.push("Contents"); + executable_dir.push("MacOS"); + + // Find the first executable in the MacOS directory + let executable_path = std::fs::read_dir(&executable_dir)? + .filter_map(Result::ok) + .find(|entry| { + let binding = entry.file_name(); + let name = binding.to_string_lossy(); + name.contains("Chromium") || name.contains("Brave") || name.contains("Google Chrome") + }) + .map(|entry| entry.path()) + .ok_or("No executable found in MacOS directory")?; + + Ok(executable_path) + } + + fn create_launch_args( + &self, + profile_path: &str, + proxy_settings: Option<&ProxySettings>, + url: Option, + ) -> Result, Box> { + let mut args = vec![ + format!("--user-data-dir={}", profile_path), + "--no-default-browser-check".to_string(), + "--disable-background-mode".to_string(), + "--disable-component-update".to_string(), + "--disable-background-timer-throttling".to_string(), + "--crash-server-url=".to_string(), + ]; + + // Add proxy configuration if provided + if let Some(proxy) = proxy_settings { + if proxy.enabled { + // Read PAC file and encode it as base64 + let pac_path = Path::new(profile_path).join("proxy.pac"); + if pac_path.exists() { + let pac_content = fs::read(&pac_path)?; + let pac_base64 = general_purpose::STANDARD.encode(&pac_content); + args.push(format!( + "--proxy-pac-url=data:application/x-javascript-config;base64,{}", + pac_base64 + )); + } + } + } + + if let Some(url) = url { + args.push(url); + } + + Ok(args) + } + + fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool { + let browser_dir = binaries_dir + .join(self.browser_type().as_str()) + .join(version); + + println!("Chromium browser checking version {} in directory: {:?}", version, browser_dir); + + // Check if directory exists and contains at least one .app file + if browser_dir.exists() { + println!("Directory exists, checking for .app files..."); + if let Ok(entries) = std::fs::read_dir(&browser_dir) { + for entry in entries { + if let Ok(entry) = entry { + println!(" Found entry: {:?}", entry.path()); + if entry.path().extension().map_or(false, |ext| ext == "app") { + println!(" Found .app file: {:?}", entry.path()); + // Try to get the executable path as a final verification + if self.get_executable_path(&browser_dir).is_ok() { + println!(" Executable path verification successful"); + return true; + } else { + println!(" Executable path verification failed"); + } + } + } + } + } + println!("No valid .app files found in directory"); + } else { + println!("Directory does not exist: {:?}", browser_dir); + } + false + } +} + +// Factory function to create browser instances +pub fn create_browser(browser_type: BrowserType) -> Box { + match browser_type { + BrowserType::MullvadBrowser | BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen | BrowserType::TorBrowser => { + Box::new(FirefoxBrowser::new(browser_type)) + } + BrowserType::Chromium | BrowserType::Brave => Box::new(ChromiumBrowser::new(browser_type)), + } +} + +// Add GithubRelease and GithubAsset structs to browser.rs if they don't already exist +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GithubRelease { + pub tag_name: String, + #[serde(default)] + pub name: String, + pub assets: Vec, + #[serde(default)] + pub published_at: String, + #[serde(default)] + pub is_alpha: bool, + #[serde(default)] + pub prerelease: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GithubAsset { + pub name: String, + pub browser_download_url: String, +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::TempDir; + + #[test] + fn test_browser_type_conversions() { + // Test as_str + assert_eq!(BrowserType::MullvadBrowser.as_str(), "mullvad-browser"); + assert_eq!(BrowserType::Firefox.as_str(), "firefox"); + assert_eq!(BrowserType::FirefoxDeveloper.as_str(), "firefox-developer"); + assert_eq!(BrowserType::Chromium.as_str(), "chromium"); + assert_eq!(BrowserType::Brave.as_str(), "brave"); + assert_eq!(BrowserType::Zen.as_str(), "zen"); + assert_eq!(BrowserType::TorBrowser.as_str(), "tor-browser"); + + // Test from_str + assert_eq!(BrowserType::from_str("mullvad-browser").unwrap(), BrowserType::MullvadBrowser); + assert_eq!(BrowserType::from_str("firefox").unwrap(), BrowserType::Firefox); + assert_eq!(BrowserType::from_str("firefox-developer").unwrap(), BrowserType::FirefoxDeveloper); + assert_eq!(BrowserType::from_str("chromium").unwrap(), BrowserType::Chromium); + assert_eq!(BrowserType::from_str("brave").unwrap(), BrowserType::Brave); + assert_eq!(BrowserType::from_str("zen").unwrap(), BrowserType::Zen); + assert_eq!(BrowserType::from_str("tor-browser").unwrap(), BrowserType::TorBrowser); + + // Test invalid browser type + assert!(BrowserType::from_str("invalid").is_err()); + assert!(BrowserType::from_str("").is_err()); + assert!(BrowserType::from_str("Firefox").is_err()); // Case sensitive + } + + #[test] + fn test_firefox_browser_creation() { + let browser = FirefoxBrowser::new(BrowserType::Firefox); + assert_eq!(browser.browser_type(), BrowserType::Firefox); + + let browser = FirefoxBrowser::new(BrowserType::MullvadBrowser); + assert_eq!(browser.browser_type(), BrowserType::MullvadBrowser); + + let browser = FirefoxBrowser::new(BrowserType::TorBrowser); + assert_eq!(browser.browser_type(), BrowserType::TorBrowser); + + let browser = FirefoxBrowser::new(BrowserType::Zen); + assert_eq!(browser.browser_type(), BrowserType::Zen); + } + + #[test] + fn test_chromium_browser_creation() { + let browser = ChromiumBrowser::new(BrowserType::Chromium); + assert_eq!(browser.browser_type(), BrowserType::Chromium); + + let browser = ChromiumBrowser::new(BrowserType::Brave); + assert_eq!(browser.browser_type(), BrowserType::Brave); + } + + #[test] + fn test_browser_factory() { + // Test Firefox-based browsers + let browser = create_browser(BrowserType::Firefox); + assert_eq!(browser.browser_type(), BrowserType::Firefox); + + let browser = create_browser(BrowserType::MullvadBrowser); + assert_eq!(browser.browser_type(), BrowserType::MullvadBrowser); + + let browser = create_browser(BrowserType::Zen); + assert_eq!(browser.browser_type(), BrowserType::Zen); + + let browser = create_browser(BrowserType::TorBrowser); + assert_eq!(browser.browser_type(), BrowserType::TorBrowser); + + let browser = create_browser(BrowserType::FirefoxDeveloper); + assert_eq!(browser.browser_type(), BrowserType::FirefoxDeveloper); + + // Test Chromium-based browsers + let browser = create_browser(BrowserType::Chromium); + assert_eq!(browser.browser_type(), BrowserType::Chromium); + + let browser = create_browser(BrowserType::Brave); + assert_eq!(browser.browser_type(), BrowserType::Brave); + } + + #[test] + fn test_firefox_launch_args() { + // Test regular Firefox (should not use -no-remote) + let browser = FirefoxBrowser::new(BrowserType::Firefox); + let args = browser.create_launch_args("/path/to/profile", None, None).unwrap(); + assert_eq!(args, vec!["-profile", "/path/to/profile"]); + assert!(!args.contains(&"-no-remote".to_string())); + + let args = browser.create_launch_args("/path/to/profile", None, Some("https://example.com".to_string())).unwrap(); + assert_eq!(args, vec!["-profile", "/path/to/profile", "https://example.com"]); + + // Test Mullvad Browser (should use -no-remote) + let browser = FirefoxBrowser::new(BrowserType::MullvadBrowser); + let args = browser.create_launch_args("/path/to/profile", None, None).unwrap(); + assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]); + + // Test Tor Browser (should use -no-remote) + let browser = FirefoxBrowser::new(BrowserType::TorBrowser); + let args = browser.create_launch_args("/path/to/profile", None, None).unwrap(); + assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]); + + // Test Zen Browser (should not use -no-remote) + let browser = FirefoxBrowser::new(BrowserType::Zen); + let args = browser.create_launch_args("/path/to/profile", None, None).unwrap(); + assert_eq!(args, vec!["-profile", "/path/to/profile"]); + assert!(!args.contains(&"-no-remote".to_string())); + } + + + #[test] + fn test_chromium_launch_args() { + let browser = ChromiumBrowser::new(BrowserType::Chromium); + let args = browser.create_launch_args("/path/to/profile", None, None).unwrap(); + + // Test that basic required arguments are present + assert!(args.contains(&"--user-data-dir=/path/to/profile".to_string())); + assert!(args.contains(&"--no-default-browser-check".to_string())); + + // Test that automatic update disabling arguments are present + assert!(args.contains(&"--disable-background-mode".to_string())); + assert!(args.contains(&"--disable-component-update".to_string())); + + let args_with_url = browser.create_launch_args("/path/to/profile", None, Some("https://example.com".to_string())).unwrap(); + assert!(args_with_url.contains(&"https://example.com".to_string())); + + // Verify URL is at the end + assert_eq!(args_with_url.last().unwrap(), "https://example.com"); + } + + #[test] + fn test_proxy_settings_creation() { + let proxy = ProxySettings { + enabled: true, + proxy_type: "http".to_string(), + host: "127.0.0.1".to_string(), + port: 8080, + }; + + assert!(proxy.enabled); + assert_eq!(proxy.proxy_type, "http"); + assert_eq!(proxy.host, "127.0.0.1"); + assert_eq!(proxy.port, 8080); + + // Test different proxy types + let socks_proxy = ProxySettings { + enabled: true, + proxy_type: "socks5".to_string(), + host: "proxy.example.com".to_string(), + port: 1080, + }; + + assert_eq!(socks_proxy.proxy_type, "socks5"); + assert_eq!(socks_proxy.host, "proxy.example.com"); + assert_eq!(socks_proxy.port, 1080); + } + + + #[test] + fn test_version_downloaded_check() { + let temp_dir = TempDir::new().unwrap(); + let binaries_dir = temp_dir.path(); + + // Create a mock Firefox browser installation + let browser_dir = binaries_dir.join("firefox").join("139.0"); + fs::create_dir_all(&browser_dir).unwrap(); + + // Create a mock .app directory + let app_dir = browser_dir.join("Firefox.app"); + fs::create_dir_all(&app_dir).unwrap(); + + let browser = FirefoxBrowser::new(BrowserType::Firefox); + assert!(browser.is_version_downloaded("139.0", binaries_dir)); + assert!(!browser.is_version_downloaded("140.0", binaries_dir)); + + // Test with Chromium browser + let chromium_dir = binaries_dir.join("chromium").join("1465660"); + fs::create_dir_all(&chromium_dir).unwrap(); + let chromium_app_dir = chromium_dir.join("Chromium.app"); + fs::create_dir_all(&chromium_app_dir.join("Contents").join("MacOS")).unwrap(); + + // Create a mock executable + let executable_path = chromium_app_dir.join("Contents").join("MacOS").join("Chromium"); + fs::write(&executable_path, "mock executable").unwrap(); + + let chromium_browser = ChromiumBrowser::new(BrowserType::Chromium); + assert!(chromium_browser.is_version_downloaded("1465660", binaries_dir)); + assert!(!chromium_browser.is_version_downloaded("1465661", binaries_dir)); + } + + #[test] + fn test_version_downloaded_no_app_directory() { + let temp_dir = TempDir::new().unwrap(); + let binaries_dir = temp_dir.path(); + + // Create browser directory but no .app directory + let browser_dir = binaries_dir.join("firefox").join("139.0"); + fs::create_dir_all(&browser_dir).unwrap(); + + // Create some other files but no .app + fs::write(browser_dir.join("readme.txt"), "Some content").unwrap(); + + let browser = FirefoxBrowser::new(BrowserType::Firefox); + assert!(!browser.is_version_downloaded("139.0", binaries_dir)); + } + + #[test] + fn test_browser_type_clone_and_debug() { + let browser_type = BrowserType::Firefox; + let cloned = browser_type.clone(); + assert_eq!(browser_type, cloned); + + // Test Debug trait + let debug_str = format!("{:?}", browser_type); + assert!(debug_str.contains("Firefox")); + } + + #[test] + fn test_proxy_settings_serialization() { + let proxy = ProxySettings { + enabled: true, + proxy_type: "http".to_string(), + host: "127.0.0.1".to_string(), + port: 8080, + }; + + // Test that it can be serialized (implements Serialize) + let json = serde_json::to_string(&proxy).unwrap(); + assert!(json.contains("127.0.0.1")); + assert!(json.contains("8080")); + assert!(json.contains("http")); + + // Test that it can be deserialized (implements Deserialize) + let deserialized: ProxySettings = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.enabled, proxy.enabled); + assert_eq!(deserialized.proxy_type, proxy.proxy_type); + assert_eq!(deserialized.host, proxy.host); + assert_eq!(deserialized.port, proxy.port); + } +} diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs new file mode 100644 index 0000000..705551b --- /dev/null +++ b/src-tauri/src/browser_runner.rs @@ -0,0 +1,2261 @@ +use crate::proxy_manager::PROXY_MANAGER; +use directories::BaseDirs; +use serde::{Deserialize, Serialize}; +use std::fs::{self, create_dir_all}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; +use sysinfo::{Pid, System}; +use tauri::Emitter; + +use crate::browser::{create_browser, BrowserType, ProxySettings}; +use crate::browser_version_service::{BrowserVersionService, BrowserVersionInfo, BrowserVersionsResult}; +use crate::download::{Downloader, DownloadProgress}; +use crate::downloaded_browsers::DownloadedBrowsersRegistry; +use crate::extraction::Extractor; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BrowserProfile { + pub name: String, + pub browser: String, + pub version: String, + pub profile_path: String, + #[serde(default)] + pub proxy: Option, + #[serde(default)] + pub process_id: Option, + #[serde(default)] + pub last_launch: Option, +} + +pub struct BrowserRunner { + base_dirs: BaseDirs, +} +impl BrowserRunner { + pub fn new() -> Self { + Self { + base_dirs: BaseDirs::new().expect("Failed to get base directories"), + } + } + + // Helper function to check if a process matches TOR/Mullvad browser + fn is_tor_or_mullvad_browser(&self, exe_name: &str, cmd: &[std::ffi::OsString], browser_type: &str) -> bool { + match browser_type { + "mullvad-browser" => { + // More specific detection for Mullvad Browser + let has_mullvad_in_exe = exe_name.contains("mullvad"); + let has_firefox_exe = exe_name == "firefox" || exe_name.contains("firefox-bin"); + let has_mullvad_in_cmd = cmd.iter().any(|arg| { + let arg_str = arg.to_str().unwrap_or(""); + arg_str.contains("Mullvad Browser.app") || + arg_str.contains("mullvad") || + arg_str.contains("Mullvad") || + arg_str.contains("/Applications/Mullvad Browser.app/") || + arg_str.contains("MullvadBrowser") + }); + + has_mullvad_in_exe || (has_firefox_exe && has_mullvad_in_cmd) + }, + "tor-browser" => { + // More specific detection for TOR Browser + let has_tor_in_exe = exe_name.contains("tor"); + let has_firefox_exe = exe_name == "firefox" || exe_name.contains("firefox-bin"); + let has_tor_in_cmd = cmd.iter().any(|arg| { + let arg_str = arg.to_str().unwrap_or(""); + arg_str.contains("Tor Browser.app") || + arg_str.contains("tor-browser") || + arg_str.contains("TorBrowser") || + arg_str.contains("/Applications/Tor Browser.app/") || + arg_str.contains("TorBrowser-Data") + }); + + has_tor_in_exe || (has_firefox_exe && has_tor_in_cmd) + }, + _ => false, + } + } + + // Helper function to validate PID for TOR/Mullvad browsers + fn validate_tor_mullvad_pid(&self, profile: &BrowserProfile, pid: u32) -> bool { + let system = System::new_all(); + + if let Some(process) = system.process(Pid::from(pid as usize)) { + let exe_name = process.name().to_string_lossy().to_lowercase(); + let cmd = process.cmd(); + + // Check if this is the correct browser type + let is_correct_browser = match profile.browser.as_str() { + "mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "mullvad-browser"), + "tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "tor-browser"), + _ => return false, + }; + + if !is_correct_browser { + println!("PID {} is not the correct browser type for {}", pid, profile.browser); + return false; + } + + // Check profile path match + let profile_path_match = cmd.iter().any(|s| { + let arg = s.to_str().unwrap_or(""); + arg == &profile.profile_path || + arg == format!("-profile={}", profile.profile_path) || + (arg == "-profile" && cmd.iter().any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path)) + }); + + if !profile_path_match { + println!("PID {} does not match profile path for {}", pid, profile.name); + return false; + } + + println!("PID {} validated successfully for {} profile {}", pid, profile.browser, profile.name); + return true; + } else { + println!("PID {} does not exist", pid); + return false; + } + } + pub fn get_binaries_dir(&self) -> PathBuf { + let mut path = self.base_dirs.data_local_dir().to_path_buf(); + path.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" }); + path.push("binaries"); + path + } + + pub fn get_profiles_dir(&self) -> PathBuf { + let mut path = self.base_dirs.data_local_dir().to_path_buf(); + path.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" }); + path.push("profiles"); + path + } + + pub fn create_profile( + &self, + name: &str, + browser: &str, + version: &str, + proxy: Option, + ) -> Result> { + // Check if a profile with this name already exists (case insensitive) + let existing_profiles = self.list_profiles()?; + if existing_profiles + .iter() + .any(|p| p.name.to_lowercase() == name.to_lowercase()) + { + return Err(format!("Profile with name '{}' already exists", name).into()); + } + + let snake_case_name = name.to_lowercase().replace(" ", "_"); + + // Create profile directory + let mut profile_path = self.get_profiles_dir(); + profile_path.push(&snake_case_name); + create_dir_all(&profile_path)?; + + let profile = BrowserProfile { + name: name.to_string(), + browser: browser.to_string(), + version: version.to_string(), + profile_path: profile_path.to_string_lossy().to_string(), + proxy: proxy.clone(), + process_id: None, + last_launch: None, + }; + + // Save profile info + self.save_profile(&profile)?; + + // Create user.js with common Firefox preferences and apply proxy settings if provided + if let Some(proxy_settings) = &proxy { + self.apply_proxy_settings_to_profile(&profile_path, proxy_settings, None)?; + } else { + // Create user.js with common Firefox preferences but no proxy + self.disable_proxy_settings_in_profile(&profile_path)?; + } + + Ok(profile) + } + + pub fn update_profile_proxy( + &self, + profile_name: &str, + proxy: Option, + ) -> Result> { + let profiles_dir = self.get_profiles_dir(); + let profile_file = profiles_dir.join(format!( + "{}.json", + profile_name.to_lowercase().replace(" ", "_") + )); + let profile_path = profiles_dir.join(profile_name.to_lowercase().replace(" ", "_")); + + if !profile_file.exists() { + return Err(format!("Profile {} not found", profile_name).into()); + } + + // Read the profile + let content = fs::read_to_string(&profile_file)?; + let mut profile: BrowserProfile = serde_json::from_str(&content)?; + + // Update proxy settings + profile.proxy = proxy.clone(); + + // Save the updated profile + self.save_profile(&profile)?; + + // Get internal proxy if the browser is running + let internal_proxy = if let Some(pid) = profile.process_id { + PROXY_MANAGER.get_proxy_settings(pid) + } else { + None + }; + + // Apply proxy settings if provided + if let Some(proxy_settings) = &proxy { + self.apply_proxy_settings_to_profile( + &profile_path, + proxy_settings, + internal_proxy.as_ref(), + )?; + } else { + self.disable_proxy_settings_in_profile(&profile_path)?; + } + + Ok(profile) + } + + pub fn update_profile_version( + &self, + profile_name: &str, + version: &str, + ) -> Result> { + let profiles_dir = self.get_profiles_dir(); + let profile_file = profiles_dir.join(format!( + "{}.json", + profile_name.to_lowercase().replace(" ", "_") + )); + + if !profile_file.exists() { + return Err(format!("Profile {} not found", profile_name).into()); + } + + // Read the profile + let content = fs::read_to_string(&profile_file)?; + let mut profile: BrowserProfile = serde_json::from_str(&content)?; + + // Check if the browser is currently running + if profile.process_id.is_some() { + return Err("Cannot update version while browser is running. Please stop the browser first.".into()); + } + + // Verify the new version is downloaded + let browser_type = BrowserType::from_str(&profile.browser) + .map_err(|_| format!("Invalid browser type: {}", profile.browser))?; + let browser = create_browser(browser_type.clone()); + let binaries_dir = self.get_binaries_dir(); + + if !browser.is_version_downloaded(version, &binaries_dir) { + return Err(format!("Browser version {} is not downloaded", version).into()); + } + + // Update version + profile.version = version.to_string(); + + // Save the updated profile + self.save_profile(&profile)?; + + Ok(profile) + } + + fn get_common_firefox_preferences(&self) -> Vec { + vec![ + // Disable default browser check + "user_pref(\"browser.shell.checkDefaultBrowser\", false);".to_string(), + + // Disable automatic updates + "user_pref(\"app.update.enabled\", false);".to_string(), + "user_pref(\"app.update.auto\", false);".to_string(), + "user_pref(\"app.update.mode\", 0);".to_string(), + "user_pref(\"app.update.service.enabled\", false);".to_string(), + "user_pref(\"app.update.silent\", false);".to_string(), + + // Disable update checking entirely + "user_pref(\"app.update.checkInstallTime\", false);".to_string(), + "user_pref(\"app.update.url\", \"\");".to_string(), + "user_pref(\"app.update.url.manual\", \"\");".to_string(), + "user_pref(\"app.update.url.details\", \"\");".to_string(), + + // Disable background update downloads + "user_pref(\"app.update.download.attemptOnce\", false);".to_string(), + "user_pref(\"app.update.idletime\", -1);".to_string(), + + // Additional update-related preferences for completeness + "user_pref(\"security.tls.insecure_fallback_hosts\", \"\");".to_string(), + "user_pref(\"app.update.staging.enabled\", false);".to_string(), + ] + } + + fn apply_proxy_settings_to_profile( + &self, + profile_path: &Path, + proxy: &ProxySettings, + internal_proxy: Option<&ProxySettings>, + ) -> Result<(), Box> { + let user_js_path = profile_path.join("user.js"); + let mut preferences = Vec::new(); + + // Add common Firefox preferences (like disabling default browser check) + preferences.extend(self.get_common_firefox_preferences()); + + if proxy.enabled { + // Create PAC file from template + let template_path = Path::new("assets/template.pac"); + let pac_content = fs::read_to_string(template_path)?; + + // Format proxy URL based on type and whether we have an internal proxy + let proxy_url = if let Some(internal) = internal_proxy { + // Use internal proxy as the primary proxy + format!("HTTP {}:{}", internal.host, internal.port) + } else { + // Use user-configured proxy directly + match proxy.proxy_type.as_str() { + "http" => format!("HTTP {}:{}", proxy.host, proxy.port), + "https" => format!("HTTPS {}:{}", proxy.host, proxy.port), + "socks4" => format!("SOCKS4 {}:{}", proxy.host, proxy.port), + "socks5" => format!("SOCKS5 {}:{}", proxy.host, proxy.port), + _ => return Err(format!("Unsupported proxy type: {}", proxy.proxy_type).into()), + } + }; + + // Replace placeholders in PAC file + let pac_content = pac_content + .replace("{{proxy_url}}", &proxy_url) + .replace("{{proxy_credentials}}", ""); // Credentials are now handled by the PAC file + + // Save PAC file in profile directory + let pac_path = profile_path.join("proxy.pac"); + fs::write(&pac_path, pac_content)?; + + // Configure Firefox to use the PAC file + preferences.extend([ + "user_pref(\"network.proxy.type\", 2);".to_string(), + format!( + "user_pref(\"network.proxy.autoconfig_url\", \"file://{}\");", + pac_path.to_string_lossy() + ), + "user_pref(\"network.proxy.failover_direct\", false);".to_string(), + "user_pref(\"network.proxy.socks_remote_dns\", true);".to_string(), + "user_pref(\"network.proxy.no_proxies_on\", \"\");".to_string(), + "user_pref(\"signon.autologin.proxy\", true);".to_string(), + "user_pref(\"network.proxy.share_proxy_settings\", false);".to_string(), + "user_pref(\"network.automatic-ntlm-auth.allow-proxies\", false);".to_string(), + "user_pref(\"network.auth-use-sspi\", false);".to_string(), + ]); + } else { + preferences.push("user_pref(\"network.proxy.type\", 0);".to_string()); + preferences.push("user_pref(\"network.proxy.failover_direct\", true);".to_string()); + + let pac_content = "function FindProxyForURL(url, host) { return 'DIRECT'; }"; + let pac_path = profile_path.join("proxy.pac"); + fs::write(&pac_path, pac_content)?; + preferences.push(format!( + "user_pref(\"network.proxy.autoconfig_url\", \"file://{}\");", + pac_path.to_string_lossy() + )); + } + + // Write settings to user.js file + fs::write(user_js_path, preferences.join("\n"))?; + + Ok(()) + } + + pub fn disable_proxy_settings_in_profile( + &self, + profile_path: &Path, + ) -> Result<(), Box> { + let user_js_path = profile_path.join("user.js"); + let mut preferences = Vec::new(); + + // Add common Firefox preferences (like disabling default browser check) + preferences.extend(self.get_common_firefox_preferences()); + + preferences.push("user_pref(\"network.proxy.type\", 0);".to_string()); + preferences.push("user_pref(\"network.proxy.failover_direct\", true);".to_string()); + + fs::write(user_js_path, preferences.join("\n"))?; + + Ok(()) + } + + pub fn save_profile(&self, profile: &BrowserProfile) -> Result<(), Box> { + let profiles_dir = self.get_profiles_dir(); + let profile_file = profiles_dir.join(format!( + "{}.json", + profile.name.to_lowercase().replace(" ", "_") + )); + + let json = serde_json::to_string_pretty(profile)?; + fs::write(profile_file, json)?; + + Ok(()) + } + + pub fn list_profiles(&self) -> Result, Box> { + let profiles_dir = self.get_profiles_dir(); + if !profiles_dir.exists() { + return Ok(vec![]); + } + + let mut profiles = Vec::new(); + for entry in fs::read_dir(profiles_dir)? { + let entry = entry?; + let path = entry.path(); + + if path.extension().map_or(false, |ext| ext == "json") { + let content = fs::read_to_string(path)?; + let profile: BrowserProfile = serde_json::from_str(&content)?; + profiles.push(profile); + } + } + + Ok(profiles) + } + + pub async fn launch_browser( + &self, + profile: &BrowserProfile, + url: Option, + ) -> Result> { + // Create browser instance + let browser_type = BrowserType::from_str(&profile.browser) + .map_err(|_| format!("Invalid browser type: {}", profile.browser))?; + let browser = create_browser(browser_type.clone()); + + // Get executable path + let mut browser_dir = self.get_binaries_dir(); + browser_dir.push(&profile.browser); + browser_dir.push(&profile.version); + + let executable_path = browser.get_executable_path(&browser_dir).expect("Failed to get executable path"); + + // Get launch arguments + let browser_args = + browser.create_launch_args(&profile.profile_path, profile.proxy.as_ref(), url).expect("Failed to create launch arguments"); + + // Launch browser + let child = Command::new(executable_path).args(&browser_args).spawn()?; + let launcher_pid = child.id(); + + println!("Launched browser with launcher PID: {} for profile: {}", launcher_pid, profile.name); + + // For TOR and Mullvad browsers, we need to find the actual browser process + // because they use launcher scripts that spawn the real browser process + let actual_pid = if matches!(browser_type, BrowserType::TorBrowser | BrowserType::MullvadBrowser) { + println!("Waiting for TOR/Mullvad browser to fully start..."); + + // Wait a bit for the browser to fully start + tokio::time::sleep(tokio::time::Duration::from_millis(3000)).await; + + // Search for the actual browser process + let system = System::new_all(); + let mut found_pid: Option = None; + + // Try multiple times to find the process as it might take time to start + for attempt in 1..=5 { + println!("Attempt {} to find actual browser process...", attempt); + + for (pid, process) in system.processes() { + let cmd = process.cmd(); + if cmd.len() >= 2 { + // Check if this is the right browser executable + let exe_name = process.name().to_string_lossy().to_lowercase(); + let is_correct_browser = match profile.browser.as_str() { + "mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "mullvad-browser"), + "tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "tor-browser"), + _ => false, + }; + + if !is_correct_browser { + continue; + } + + // Check for profile path match + let profile_path_match = cmd.iter().any(|s| { + let arg = s.to_str().unwrap_or(""); + arg == &profile.profile_path || + arg == format!("-profile={}", profile.profile_path) || + (arg == "-profile" && cmd.iter().any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path)) + }); + + if profile_path_match { + found_pid = Some(pid.as_u32()); + println!("Found actual browser process with PID: {} for profile: {}", pid.as_u32(), profile.name); + break; + } + } + } + + if found_pid.is_some() { + break; + } + + // Wait before next attempt + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; + } + + found_pid.unwrap_or(launcher_pid) + } else { + // For other browsers, the launcher PID is usually the actual browser PID + launcher_pid + }; + + // Update profile with process info + let mut updated_profile = profile.clone(); + updated_profile.process_id = Some(actual_pid); + updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()); + + // Save the updated profile + self.save_process_info(&updated_profile).expect("Failed to save process info"); + + println!("Browser launched successfully with PID: {} for profile: {}", actual_pid, profile.name); + Ok(updated_profile) + } + + pub async fn open_url_in_existing_browser( + &self, + app_handle: tauri::AppHandle, + profile: &BrowserProfile, + url: &str, + ) -> Result<(), Box> { + // Use the comprehensive browser status check + let is_running = self.check_browser_status(app_handle, profile).await?; + + if !is_running { + return Err("Browser is not running".into()); + } + + // Get the updated profile with current PID + let profiles = self.list_profiles().expect("Failed to list profiles"); + let updated_profile = profiles.into_iter() + .find(|p| p.name == profile.name) + .unwrap_or_else(|| profile.clone()); + + // Ensure we have a valid process ID + if updated_profile.process_id.is_none() { + return Err("No valid process ID found for the browser".into()); + } + + let browser_type = BrowserType::from_str(&updated_profile.browser) + .map_err(|_| format!("Invalid browser type: {}", updated_profile.browser))?; + + match browser_type { + BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen => { + // These browsers don't use -no-remote, so we can use Firefox remote commands + #[cfg(target_os = "macos")] + { + let pid = updated_profile.process_id.unwrap(); + + // First try: Use Firefox remote command (most reliable for these browsers) + println!("Trying Firefox remote command for PID: {}", pid); + let mut browser_dir = self.get_binaries_dir(); + browser_dir.push(&updated_profile.browser); + browser_dir.push(&updated_profile.version); + + let browser = create_browser(browser_type); + if let Ok(executable_path) = browser.get_executable_path(&browser_dir) { + let remote_args = vec![ + "-profile".to_string(), + updated_profile.profile_path.clone(), + "-new-tab".to_string(), + url.to_string(), + ]; + + let remote_output = Command::new(executable_path) + .args(&remote_args) + .output(); + + match remote_output { + Ok(output) if output.status.success() => { + println!("Firefox remote command succeeded"); + return Ok(()); + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + println!("Firefox remote command failed with stderr: {}, trying AppleScript fallback", stderr); + } + Err(e) => { + println!("Firefox remote command error: {}, trying AppleScript fallback", e); + } + } + } + + // Fallback: Use AppleScript if remote command fails + let escaped_url = url.replace("\"", "\\\"").replace("\\", "\\\\").replace("'", "\\'"); + + let script = format!( + r#" +try + tell application "System Events" + -- Find the exact process by PID + set targetProcess to (first application process whose unix id is {}) + + -- Verify the process exists + if not (exists targetProcess) then + error "No process found with PID {}" + end if + + -- Get the process name for verification + set processName to name of targetProcess + + -- Bring the process to the front first + set frontmost of targetProcess to true + delay 1.0 + + -- Check if the process has any visible windows + set windowList to windows of targetProcess + set hasVisibleWindow to false + repeat with w in windowList + if visible of w is true then + set hasVisibleWindow to true + exit repeat + end if + end repeat + + if not hasVisibleWindow then + -- No visible windows, create a new one + tell targetProcess + keystroke "n" using command down + delay 2.0 + end tell + end if + + -- Ensure the process is frontmost again + set frontmost of targetProcess to true + delay 0.5 + + -- Focus on the address bar and open URL + tell targetProcess + -- Open a new tab + keystroke "t" using command down + delay 1.5 + + -- Focus address bar (Cmd+L) + keystroke "l" using command down + delay 0.5 + + -- Type the URL + keystroke "{}" + delay 0.5 + + -- Press Enter to navigate + keystroke return + end tell + + return "Successfully opened URL in " & processName & " (PID: {})" + end tell +on error errMsg number errNum + return "AppleScript failed: " & errMsg & " (Error " & errNum & ")" +end try + "#, + pid, pid, escaped_url, pid + ); + + println!("Executing AppleScript fallback for Firefox-based browser (PID: {})...", pid); + let output = Command::new("osascript") + .args(["-e", &script]) + .output()?; + + if !output.status.success() { + let error_msg = String::from_utf8_lossy(&output.stderr); + println!("AppleScript failed: {}", error_msg); + return Err(format!( + "Both Firefox remote command and AppleScript failed. AppleScript error: {}", + error_msg + ).into()); + } else { + println!("AppleScript succeeded"); + } + } + + #[cfg(not(target_os = "macos"))] + { + // For non-macOS platforms, use Firefox remote command + let mut browser_dir = self.get_binaries_dir(); + browser_dir.push(&updated_profile.browser); + browser_dir.push(&updated_profile.version); + + let browser = create_browser(browser_type); + let executable_path = browser.get_executable_path(&browser_dir)?; + + let output = Command::new(executable_path) + .args([ + "-profile", + &updated_profile.profile_path, + "-new-tab", + url, + ]) + .output()?; + + if !output.status.success() { + return Err(format!( + "Failed to open URL in existing browser: {}", + String::from_utf8_lossy(&output.stderr) + ).into()); + } + } + } + BrowserType::MullvadBrowser | BrowserType::TorBrowser => { + // These browsers use -no-remote, so we need a different approach that doesn't require accessibility permissions + #[cfg(target_os = "macos")] + { + let pid = updated_profile.process_id.unwrap(); + + println!("Opening URL in TOR/Mullvad browser using file-based approach (PID: {})", pid); + + // Validate that we have the correct PID for this TOR/Mullvad browser + if !self.validate_tor_mullvad_pid(&updated_profile, pid) { + return Err(format!( + "PID {} is not valid for {} profile {}. The browser process may have changed.", + pid, updated_profile.browser, updated_profile.name + ).into()); + } + + // Method 1: Try using a temporary HTML file approach that doesn't require accessibility permissions + println!("Attempting file-based URL opening for TOR/Mullvad browser"); + + // Create a temporary HTML file that redirects to the target URL + let temp_dir = std::env::temp_dir(); + let temp_file_name = format!("donut_browser_url_{}.html", std::process::id()); + let temp_file_path = temp_dir.join(&temp_file_name); + + let html_content = format!( + r#" + + + + + Redirecting... + + + +

Redirecting to {}...

+ +"#, + url, url, url, url + ); + + // Write the HTML file + match std::fs::write(&temp_file_path, html_content) { + Ok(()) => { + println!("Created temporary HTML file: {:?}", temp_file_path); + + // Get the browser executable path to use with 'open' + let mut browser_dir = self.get_binaries_dir(); + browser_dir.push(&updated_profile.browser); + browser_dir.push(&updated_profile.version); + + let browser = create_browser(browser_type.clone()); + if let Ok(executable_path) = browser.get_executable_path(&browser_dir) { + // Use 'open' command to open the HTML file with the specific browser instance + // This approach works because it uses the existing browser process + let open_result = Command::new("open") + .args([ + "-a", + executable_path.to_str().unwrap(), + temp_file_path.to_str().unwrap(), + ]) + .output(); + + // Clean up the temporary file after a short delay + let temp_file_path_clone = temp_file_path.clone(); + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + let _ = std::fs::remove_file(temp_file_path_clone); + }); + + match open_result { + Ok(output) if output.status.success() => { + println!("Successfully opened URL using file-based approach"); + return Ok(()); + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + println!("File-based approach failed: {}", stderr); + } + Err(e) => { + println!("File-based approach error: {}", e); + } + } + } + + // Clean up temp file if the approach failed + let _ = std::fs::remove_file(&temp_file_path); + } + Err(e) => { + println!("Failed to create temporary HTML file: {}", e); + } + } + + // Method 2: Try using the 'open' command directly with the URL + println!("Attempting direct URL opening with 'open' command"); + + // Get the browser executable path + let mut browser_dir = self.get_binaries_dir(); + browser_dir.push(&updated_profile.browser); + browser_dir.push(&updated_profile.version); + + let browser = create_browser(browser_type.clone()); + if let Ok(executable_path) = browser.get_executable_path(&browser_dir) { + // Try to open the URL directly with the browser + let direct_open_result = Command::new("open") + .args([ + "-a", + executable_path.to_str().unwrap(), + url, + ]) + .output(); + + match direct_open_result { + Ok(output) if output.status.success() => { + println!("Successfully opened URL using direct 'open' command"); + return Ok(()); + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + println!("Direct 'open' command failed: {}", stderr); + } + Err(e) => { + println!("Direct 'open' command error: {}", e); + } + } + } + + // Method 3: Try using osascript without accessibility features (just to bring window to front) + println!("Attempting minimal AppleScript approach without accessibility features"); + + let minimal_script = format!( + r#" +try + tell application "System Events" + set targetProcess to (first application process whose unix id is {}) + if not (exists targetProcess) then + error "No process found with PID {}" + end if + + -- Just bring the process to front without trying to control it + set frontmost of targetProcess to true + + return "Process brought to front successfully" + end tell +on error errMsg + return "Minimal AppleScript failed: " & errMsg +end try + "#, + pid, pid + ); + + let minimal_output = Command::new("osascript") + .args(["-e", &minimal_script]) + .output(); + + match minimal_output { + Ok(output) => { + let result = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if output.status.success() && result.contains("successfully") { + println!("Successfully brought browser to front: {}", result); + + // Now try to use the system's default URL opening mechanism + let system_open_result = Command::new("open") + .args([url]) + .output(); + + match system_open_result { + Ok(output) if output.status.success() => { + println!("Successfully opened URL using system default handler"); + return Ok(()); + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + println!("System default URL opening failed: {}", stderr); + } + Err(e) => { + println!("System default URL opening error: {}", e); + } + } + } else { + println!("Minimal AppleScript failed: {}", result); + } + } + Err(e) => { + println!("Minimal AppleScript execution error: {}", e); + } + } + + // If all methods fail, return a more helpful error message + return Err(format!( + "Failed to open URL in existing TOR/Mullvad browser (PID: {}). All methods failed:\n\ + 1. File-based approach failed\n\ + 2. Direct 'open' command failed\n\ + 3. Minimal AppleScript approach failed\n\ + \n\ + This may be due to browser security restrictions or the browser process may have changed.\n\ + Try closing and reopening the browser, or manually paste the URL: {}", + pid, url + ).into()); + } + + #[cfg(not(target_os = "macos"))] + { + // For non-macOS platforms, we can't use AppleScript, so try a different approach + // This is a limitation - Firefox with -no-remote can't receive remote commands + return Err("Opening URLs in existing Firefox-based browsers is not supported on this platform when using -no-remote".into()); + } + } + BrowserType::Chromium | BrowserType::Brave => { + // For Chromium-based browsers, use a more targeted approach + #[cfg(target_os = "macos")] + { + let pid = updated_profile.process_id.unwrap(); + + // First, try using the browser's built-in URL opening capability + println!("Trying Chromium URL opening for PID: {}", pid); + let mut browser_dir = self.get_binaries_dir(); + browser_dir.push(&updated_profile.browser); + browser_dir.push(&updated_profile.version); + + let browser = create_browser(browser_type); + if let Ok(executable_path) = browser.get_executable_path(&browser_dir) { + // Try to open URL in existing instance using the same user data dir + let remote_output = Command::new(executable_path) + .args([ + &format!("--user-data-dir={}", updated_profile.profile_path), + url, + ]) + .output(); + + match remote_output { + Ok(output) if output.status.success() => { + println!("Chromium URL opening succeeded"); + return Ok(()); + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + println!("Chromium URL opening failed: {}, trying AppleScript", stderr); + } + Err(e) => { + println!("Chromium URL opening error: {}, trying AppleScript", e); + } + } + } + + // Fallback to AppleScript with more precise targeting + let escaped_url = url.replace("\"", "\\\"").replace("\\", "\\\\").replace("'", "\\'"); + + let script = format!( + r#" +try + tell application "System Events" + -- Find the exact process by PID + set targetProcess to (first application process whose unix id is {}) + + -- Verify the process exists + if not (exists targetProcess) then + error "No process found with PID {}" + end if + + -- Get the process name for verification + set processName to name of targetProcess + + -- Bring the process to the front first + set frontmost of targetProcess to true + delay 1.0 + + -- Check if the process has any visible windows + set windowList to windows of targetProcess + set hasVisibleWindow to false + repeat with w in windowList + if visible of w is true then + set hasVisibleWindow to true + exit repeat + end if + end repeat + + if not hasVisibleWindow then + -- No visible windows, create a new one + tell targetProcess + keystroke "n" using command down + delay 2.0 + end tell + end if + + -- Ensure the process is frontmost again + set frontmost of targetProcess to true + delay 0.5 + + -- Focus on the address bar and open URL + tell targetProcess + -- Open a new tab + keystroke "t" using command down + delay 1.5 + + -- Focus address bar (Cmd+L) + keystroke "l" using command down + delay 0.5 + + -- Type the URL + keystroke "{}" + delay 0.5 + + -- Press Enter to navigate + keystroke return + end tell + + return "Successfully opened URL in " & processName & " (PID: {})" + end tell +on error errMsg number errNum + return "AppleScript failed: " & errMsg & " (Error " & errNum & ")" +end try + "#, + pid, pid, escaped_url, pid + ); + + println!("Executing AppleScript for Chromium-based browser (PID: {})...", pid); + let output = Command::new("osascript") + .args(["-e", &script]) + .output()?; + + if !output.status.success() { + let error_msg = String::from_utf8_lossy(&output.stderr); + println!("AppleScript failed: {}", error_msg); + return Err(format!( + "Failed to open URL in existing Chromium-based browser: {}", + error_msg + ).into()); + } else { + println!("AppleScript succeeded"); + } + } + + #[cfg(not(target_os = "macos"))] + { + // For non-macOS platforms, try using the browser's remote opening capability + let mut browser_dir = self.get_binaries_dir(); + browser_dir.push(&updated_profile.browser); + browser_dir.push(&updated_profile.version); + + let browser = create_browser(browser_type); + let executable_path = browser.get_executable_path(&browser_dir)?; + + // Try to open in existing instance + let output = Command::new(executable_path) + .args([ + &format!("--user-data-dir={}", updated_profile.profile_path), + url, + ]) + .output()?; + + if !output.status.success() { + return Err(format!( + "Failed to open URL in existing Chromium-based browser: {}", + String::from_utf8_lossy(&output.stderr) + ).into()); + } + } + } + } + + Ok(()) + } + + pub async fn launch_or_open_url( + &self, + app_handle: tauri::AppHandle, + profile: &BrowserProfile, + url: Option, + ) -> Result> { + // Get the most up-to-date profile data + let profiles = self.list_profiles().expect("Failed to list profiles"); + let updated_profile = profiles.into_iter() + .find(|p| p.name == profile.name) + .unwrap_or_else(|| profile.clone()); + + // Check if browser is already running + let is_running = self.check_browser_status(app_handle.clone(), &updated_profile).await?; + + // Get the updated profile again after status check (PID might have been updated) + let profiles = self.list_profiles().expect("Failed to list profiles"); + let final_profile = profiles.into_iter() + .find(|p| p.name == profile.name) + .unwrap_or_else(|| updated_profile.clone()); + + println!("Browser status check - Profile: {}, Running: {}, URL: {:?}, PID: {:?}", + final_profile.name, is_running, url, final_profile.process_id); + + if is_running && url.is_some() { + // Browser is running and we have a URL to open + println!("Opening URL in existing browser: {}", url.as_ref().unwrap()); + + // For TOR/Mullvad browsers, add extra verification + if matches!(final_profile.browser.as_str(), "tor-browser" | "mullvad-browser") { + println!("TOR/Mullvad browser detected - ensuring we have correct PID"); + if final_profile.process_id.is_none() { + println!("ERROR: No PID found for running TOR/Mullvad browser - this should not happen"); + return Err("No PID found for running browser".into()); + } + } + match self.open_url_in_existing_browser(app_handle, &final_profile, url.as_ref().unwrap()).await { + Ok(()) => { + println!("Successfully opened URL in existing browser"); + Ok(final_profile) + } + Err(e) => { + println!("Failed to open URL in existing browser: {}", e); + + // For Mullvad and Tor browsers, don't fall back to new instance since they use -no-remote + // and can't have multiple instances with the same profile + match final_profile.browser.as_str() { + "mullvad-browser" | "tor-browser" => { + return Err(format!( + "Failed to open URL in existing {} browser. Cannot launch new instance due to profile conflict: {}", + final_profile.browser, e + ).into()); + } + _ => { + println!("Falling back to new instance for browser: {}", final_profile.browser); + // Fallback to launching a new instance for other browsers + self.launch_browser(&final_profile, url).await + } + } + } + } + } else { + // Browser is not running or no URL provided, launch new instance + if !is_running { + println!("Launching new browser instance - browser not running"); + } else { + println!("Launching new browser instance - no URL provided"); + } + self.launch_browser(&final_profile, url).await + } + } + + pub fn rename_profile( + &self, + old_name: &str, + new_name: &str, + ) -> Result> { + let profiles_dir = self.get_profiles_dir(); + let old_profile_file = profiles_dir.join(format!( + "{}.json", + old_name.to_lowercase().replace(" ", "_") + )); + let old_profile_path = profiles_dir.join(old_name.to_lowercase().replace(" ", "_")); + + // Check if new name already exists (case insensitive) + let existing_profiles = self.list_profiles()?; + if existing_profiles + .iter() + .any(|p| p.name.to_lowercase() == new_name.to_lowercase()) + { + return Err(format!("Profile with name '{}' already exists", new_name).into()); + } + + // Read the profile + let content = fs::read_to_string(&old_profile_file)?; + let mut profile: BrowserProfile = serde_json::from_str(&content)?; + + // Update profile name + profile.name = new_name.to_string(); + + // Create new paths + let _new_profile_file = profiles_dir.join(format!( + "{}.json", + new_name.to_lowercase().replace(" ", "_") + )); + let new_profile_path = profiles_dir.join(new_name.to_lowercase().replace(" ", "_")); + + // Rename directory + if old_profile_path.exists() { + fs::rename(&old_profile_path, &new_profile_path)?; + } + + // Update profile path + profile.profile_path = new_profile_path.to_string_lossy().to_string(); + + // Save profile with new name + self.save_profile(&profile)?; + + // Delete old profile file + if old_profile_file.exists() { + fs::remove_file(old_profile_file)?; + } + + Ok(profile) + } + + pub fn get_saved_mullvad_releases(&self) -> Result, Box> { + let mut data_path = self.base_dirs.data_local_dir().to_path_buf(); + data_path.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" }); + data_path.push("data"); + let releases_file = data_path.join("mullvad_releases.json"); + + if !releases_file.exists() { + return Ok(vec![]); + } + + let mut versions = Vec::new(); + let mut browser_dir = self.base_dirs.data_local_dir().to_path_buf(); + browser_dir.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" }); + browser_dir.push("binaries"); + browser_dir.push("mullvad-browser"); + for entry in fs::read_dir(browser_dir)? { + let entry = entry?; + if entry.path().is_dir() { + if let Some(version_str) = entry.file_name().to_str() { + versions.push(version_str.to_string()); + } + } + } + + // Sort versions in descending order (newest first) + versions.sort_by(|a, b| b.cmp(a)); + + Ok(versions) + } + + fn save_process_info(&self, profile: &BrowserProfile) -> Result<(), Box> { + let profiles_dir = self.get_profiles_dir(); + let profile_file = profiles_dir.join(format!( + "{}.json", + profile.name.to_lowercase().replace(" ", "_") + )); + let json = serde_json::to_string_pretty(&profile)?; + fs::write(profile_file, json)?; + Ok(()) + } + + pub fn delete_profile(&self, profile_name: &str) -> Result<(), Box> { + let profiles_dir = self.get_profiles_dir(); + let profile_file = profiles_dir.join(format!( + "{}.json", + profile_name.to_lowercase().replace(" ", "_") + )); + let profile_path = profiles_dir.join(profile_name.to_lowercase().replace(" ", "_")); + + // Delete profile directory + if profile_path.exists() { + fs::remove_dir_all(profile_path)? + } + + // Delete profile JSON file + if profile_file.exists() { + fs::remove_file(profile_file)? + } + + Ok(()) + } + + pub async fn check_browser_status( + &self, + app_handle: tauri::AppHandle, + profile: &BrowserProfile, + ) -> Result> { + let mut inner_profile = profile.clone(); + let system = System::new_all(); + let mut is_running = false; + let mut found_pid: Option = None; + + // First check if the stored PID is still valid + if let Some(pid) = profile.process_id { + if let Some(process) = system.process(Pid::from(pid as usize)) { + let cmd = process.cmd(); + // Verify this process is actually our browser with the correct profile + let profile_path_match = cmd.iter().any(|s| { + let arg = s.to_str().unwrap_or(""); + // For Firefox-based browsers, check for exact profile path match + if profile.browser == "tor-browser" || profile.browser == "firefox" || + profile.browser == "firefox-developer" || profile.browser == "mullvad-browser" || + profile.browser == "zen" { + arg == &profile.profile_path || + arg == format!("-profile={}", profile.profile_path) || + (arg == "-profile" && cmd.iter().any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path)) + } else { + // For Chromium-based browsers, check for user-data-dir + arg.contains(&format!("--user-data-dir={}", profile.profile_path)) || + arg == &profile.profile_path + } + }); + + if profile_path_match { + is_running = true; + found_pid = Some(pid); + println!("Found existing browser process with PID: {} for profile: {}", pid, profile.name); + } else { + println!("PID {} exists but doesn't match our profile path exactly, searching for correct process...", pid); + } + } else { + println!("Stored PID {} no longer exists, searching for browser process...", pid); + } + } + + // If we didn't find the browser with the stored PID, search all processes + if !is_running { + for (pid, process) in system.processes() { + let cmd = process.cmd(); + if cmd.len() >= 2 { + // Check if this is the right browser executable first + let exe_name = process.name().to_string_lossy().to_lowercase(); + let is_correct_browser = match profile.browser.as_str() { + "firefox" => exe_name.contains("firefox") && !exe_name.contains("developer") && !exe_name.contains("tor") && !exe_name.contains("mullvad"), + "firefox-developer" => exe_name.contains("firefox") && exe_name.contains("developer"), + "mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "mullvad-browser"), + "tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "tor-browser"), + "zen" => exe_name.contains("zen"), + "chromium" => exe_name.contains("chromium"), + "brave" => exe_name.contains("brave"), + _ => false, + }; + + if !is_correct_browser { + continue; + } + + // Check for profile path match + let profile_path_match = cmd.iter().any(|s| { + let arg = s.to_str().unwrap_or(""); + // For Firefox-based browsers, check for exact profile path match + if profile.browser == "tor-browser" || profile.browser == "firefox" || + profile.browser == "firefox-developer" || profile.browser == "mullvad-browser" || + profile.browser == "zen" { + arg == &profile.profile_path || + arg == format!("-profile={}", profile.profile_path) || + (arg == "-profile" && cmd.iter().any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path)) + } else { + // For Chromium-based browsers, check for user-data-dir + arg.contains(&format!("--user-data-dir={}", profile.profile_path)) || + arg == &profile.profile_path + } + }); + + if profile_path_match { + // Found a matching process + found_pid = Some(pid.as_u32()); + is_running = true; + println!("Found browser process with PID: {} for profile: {}", pid.as_u32(), profile.name); + break; + } + } + } + } + + // Update the process ID if we found a different one + if let Some(pid) = found_pid { + if inner_profile.process_id != Some(pid) { + inner_profile.process_id = Some(pid); + if let Err(e) = self.save_process_info(&inner_profile) { + println!("Warning: Failed to update process info: {}", e); + } else { + println!("Updated process ID for profile '{}' to: {}", inner_profile.name, pid); + } + } + } else if is_running { + println!("Browser is running but no PID found - this shouldn't happen"); + } else { + // Browser is not running, clear the PID if it was set + if inner_profile.process_id.is_some() { + inner_profile.process_id = None; + if let Err(e) = self.save_process_info(&inner_profile) { + println!("Warning: Failed to clear process info: {}", e); + } else { + println!("Cleared process ID for profile '{}'", inner_profile.name); + } + } + } + + // Handle proxy management based on browser status + if let Some(proxy) = &inner_profile.proxy { + if proxy.enabled { + if is_running { + // Browser is running, check if proxy is active + let proxy_active = PROXY_MANAGER + .get_proxy_settings(inner_profile.process_id.unwrap_or(0)) + .is_some(); + + if !proxy_active { + // Browser is running but proxy is not - restart the proxy + if let Some((upstream_url, _preferred_port)) = + PROXY_MANAGER.get_profile_proxy_info(&inner_profile.name) + { + // Restart the proxy with the same configuration + match PROXY_MANAGER + .start_proxy( + app_handle, + &upstream_url, + inner_profile.process_id.unwrap(), + Some(&inner_profile.name), + ) + .await + { + Ok(_) => { + println!("Restarted proxy for profile {}", inner_profile.name); + } + Err(e) => { + eprintln!( + "Failed to restart proxy for profile {}: {}", + inner_profile.name, e + ); + } + } + } + } + } else { + // Browser is not running, stop the proxy if it exists + if let Some(pid) = profile.process_id { + let _ = PROXY_MANAGER.stop_proxy(app_handle, pid).await; + } + } + } + } + + Ok(is_running) + } + + pub async fn kill_browser_process( + &self, + app_handle: tauri::AppHandle, + profile: &BrowserProfile, + ) -> Result<(), Box> { + // Get the current process ID + let pid = if let Some(pid) = profile.process_id { + pid + } else { + // Try to find the process by searching all processes + let system = System::new_all(); + let mut found_pid: Option = None; + + for (pid, process) in system.processes() { + let cmd = process.cmd(); + if cmd.len() >= 2 { + // Check if this is the right browser executable first + let exe_name = process.name().to_string_lossy().to_lowercase(); + let is_correct_browser = match profile.browser.as_str() { + "firefox" => exe_name.contains("firefox") && !exe_name.contains("developer") && !exe_name.contains("tor") && !exe_name.contains("mullvad"), + "firefox-developer" => exe_name.contains("firefox") && exe_name.contains("developer"), + "mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "mullvad-browser"), + "tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "tor-browser"), + "zen" => exe_name.contains("zen"), + "chromium" => exe_name.contains("chromium"), + "brave" => exe_name.contains("brave"), + _ => false, + }; + + if !is_correct_browser { + continue; + } + + // Check for profile path match + let profile_path_match = cmd.iter().any(|s| { + let arg = s.to_str().unwrap_or(""); + // For Firefox-based browsers, check for exact profile path match + if profile.browser == "tor-browser" || profile.browser == "firefox" || + profile.browser == "firefox-developer" || profile.browser == "mullvad-browser" || + profile.browser == "zen" { + arg == &profile.profile_path || arg == format!("-profile={}", profile.profile_path) + } else { + // For Chromium-based browsers, check for user-data-dir + arg.contains(&format!("--user-data-dir={}", profile.profile_path)) || + arg == &profile.profile_path + } + }); + + if profile_path_match { + found_pid = Some(pid.as_u32()); + break; + } + } + } + + found_pid.ok_or("Browser process not found")? + }; + + println!("Attempting to kill browser process with PID: {}", pid); + + // Stop any associated proxy first + if let Err(e) = PROXY_MANAGER.stop_proxy(app_handle, pid).await { + println!("Warning: Failed to stop proxy for PID {}: {}", pid, e); + } + + // Kill the process + #[cfg(target_os = "macos")] + { + use std::process::Command; + + // First try SIGTERM (graceful shutdown) + let output = Command::new("kill") + .args(["-TERM", &pid.to_string()]) + .output() + .map_err(|e| format!("Failed to execute kill command: {}", e))?; + + if !output.status.success() { + // If SIGTERM fails, try SIGKILL (force kill) + let output = Command::new("kill") + .args(["-KILL", &pid.to_string()]) + .output()?; + + if !output.status.success() { + return Err(format!("Failed to kill process {}: {}", pid, String::from_utf8_lossy(&output.stderr)).into()); + } + } + } + + #[cfg(not(target_os = "macos"))] + { + // For other platforms, use the sysinfo crate + let system = System::new_all(); + if let Some(process) = system.process(Pid::from(pid as usize)) { + if !process.kill() { + return Err(format!("Failed to kill process {}", pid).into()); + } + } else { + return Err(format!("Process {} not found", pid).into()); + } + } + + // Clear the process ID from the profile + let mut updated_profile = profile.clone(); + updated_profile.process_id = None; + self.save_process_info(&updated_profile) + .map_err(|e| format!("Failed to update profile: {}", e))?; + + println!("Successfully killed browser process with PID: {}", pid); + Ok(()) + } +} + +#[tauri::command] +pub fn create_browser_profile( + name: String, + browser: String, + version: String, + proxy: Option, +) -> Result { + let browser_runner = BrowserRunner::new(); + browser_runner + .create_profile(&name, &browser, &version, proxy) + .map_err(|e| format!("Failed to create profile: {}", e)) +} + +#[tauri::command] +pub fn list_browser_profiles() -> Result, String> { + let browser_runner = BrowserRunner::new(); + browser_runner + .list_profiles() + .map_err(|e| format!("Failed to list profiles: {}", e)) +} + +#[tauri::command] +pub async fn launch_browser_profile( + app_handle: tauri::AppHandle, + profile: BrowserProfile, + url: Option, +) -> Result { + let browser_runner = BrowserRunner::new(); + + // Launch browser or open URL in existing instance + let updated_profile = browser_runner + .launch_or_open_url(app_handle.clone(), &profile, url) + .await + .expect("Failed to launch browser or open URL"); + + // If the profile has proxy settings, start a proxy for it + if let Some(proxy) = &profile.proxy { + if proxy.enabled { + // Get the process ID + if let Some(pid) = updated_profile.process_id { + // Start a proxy for the upstream URL + let upstream_url = format!("{}://{}:{}", proxy.proxy_type, proxy.host, proxy.port); + + // Start the proxy + match PROXY_MANAGER + .start_proxy(app_handle.clone(), &upstream_url, pid, Some(&profile.name)) + .await + { + Ok(internal_proxy_settings) => { + let browser_runner = BrowserRunner::new(); + let profiles_dir = browser_runner.get_profiles_dir(); + let profile_path = profiles_dir.join(profile.name.to_lowercase().replace(" ", "_")); + + // Apply the proxy settings with the internal proxy + browser_runner + .apply_proxy_settings_to_profile(&profile_path, proxy, Some(&internal_proxy_settings)) + .map_err(|e| format!("Failed to update profile proxy: {}", e))?; + } + Err(e) => { + eprintln!("Failed to start proxy: {}", e); + // Continue without proxy + } + } + } + } + } + + Ok(updated_profile) +} + +// Add Tauri command to get saved releases +#[tauri::command] +pub fn get_saved_mullvad_releases() -> Result, String> { + let browser_runner = BrowserRunner::new(); + browser_runner + .get_saved_mullvad_releases() + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn update_profile_proxy( + profile_name: String, + proxy: Option, +) -> Result { + let browser_runner = BrowserRunner::new(); + browser_runner + .update_profile_proxy(&profile_name, proxy) + .map_err(|e| format!("Failed to update profile: {}", e)) +} + +#[tauri::command] +pub fn update_profile_version( + profile_name: String, + version: String, +) -> Result { + let browser_runner = BrowserRunner::new(); + browser_runner + .update_profile_version(&profile_name, &version) + .map_err(|e| format!("Failed to update profile version: {}", e)) +} + +#[tauri::command] +pub async fn check_browser_status( + app_handle: tauri::AppHandle, + profile: BrowserProfile, +) -> Result { + let browser_runner = BrowserRunner::new(); + browser_runner + .check_browser_status(app_handle, &profile) + .await + .map_err(|e| format!("Failed to check browser status: {}", e)) +} + +#[tauri::command] +pub fn rename_profile( + _app_handle: tauri::AppHandle, + old_name: &str, + new_name: &str, +) -> Result { + let browser_runner = BrowserRunner::new(); + browser_runner + .rename_profile(old_name, new_name) + .map_err(|e| format!("Failed to delete profile: {}", e)) +} + +#[tauri::command] +pub fn delete_profile(_app_handle: tauri::AppHandle, profile_name: String) -> Result<(), String> { + let browser_runner = BrowserRunner::new(); + browser_runner + .delete_profile(profile_name.as_str()) + .map_err(|e| format!("Failed to delete profile: {}", e)) +} + +#[tauri::command] +pub fn get_supported_browsers() -> Result, String> { + Ok(vec![ + BrowserType::MullvadBrowser.as_str(), + BrowserType::Firefox.as_str(), + BrowserType::FirefoxDeveloper.as_str(), + BrowserType::Chromium.as_str(), + BrowserType::Brave.as_str(), + BrowserType::Zen.as_str(), + BrowserType::TorBrowser.as_str(), + ]) +} + +#[tauri::command] +pub async fn fetch_browser_versions_detailed(browser_str: String) -> Result, String> { + let service = BrowserVersionService::new(); + service.fetch_browser_versions_detailed(&browser_str, false).await + .map_err(|e| format!("Failed to fetch detailed browser versions: {}", e)) +} + +#[tauri::command] +pub async fn fetch_browser_versions_cached_first(browser_str: String) -> Result, String> { + let service = BrowserVersionService::new(); + + // Get cached versions immediately if available + if let Some(cached_versions) = service.get_cached_browser_versions_detailed(&browser_str) { + // Check if we should update cache in background + if service.should_update_cache(&browser_str) { + // Start background update but return cached data immediately + let service_clone = BrowserVersionService::new(); + let browser_str_clone = browser_str.clone(); + tokio::spawn(async move { + if let Err(e) = service_clone.fetch_browser_versions_detailed(&browser_str_clone, false).await { + eprintln!("Background version update failed for {}: {}", browser_str_clone, e); + } + }); + } + Ok(cached_versions) + } else { + // No cache available, fetch fresh + service.fetch_browser_versions_detailed(&browser_str, false).await + .map_err(|e| format!("Failed to fetch detailed browser versions: {}", e)) + } +} + +#[tauri::command] +pub async fn fetch_browser_versions_with_count_cached_first(browser_str: String) -> Result { + let service = BrowserVersionService::new(); + + // Get cached versions immediately if available + if let Some(cached_versions) = service.get_cached_browser_versions(&browser_str) { + // Check if we should update cache in background + if service.should_update_cache(&browser_str) { + // Start background update but return cached data immediately + let service_clone = BrowserVersionService::new(); + let browser_str_clone = browser_str.clone(); + tokio::spawn(async move { + if let Err(e) = service_clone.fetch_browser_versions_with_count(&browser_str_clone, false).await { + eprintln!("Background version update failed for {}: {}", browser_str_clone, e); + } + }); + } + + // Return cached data in the expected format + Ok(BrowserVersionsResult { + versions: cached_versions.clone(), + new_versions_count: None, // No new versions when returning cached data + total_versions_count: cached_versions.len(), + }) + } else { + // No cache available, fetch fresh + service.fetch_browser_versions_with_count(&browser_str, false).await + .map_err(|e| format!("Failed to fetch browser versions: {}", e)) + } +} + +#[tauri::command] +pub fn get_cached_browser_versions_detailed(browser_str: String) -> Result>, String> { + let service = BrowserVersionService::new(); + Ok(service.get_cached_browser_versions_detailed(&browser_str)) +} + +#[tauri::command] +pub fn should_update_browser_cache(browser_str: String) -> Result { + let service = BrowserVersionService::new(); + Ok(service.should_update_cache(&browser_str)) +} + +#[tauri::command] +pub async fn download_browser( + app_handle: tauri::AppHandle, + browser_str: String, + version: String, +) -> Result { + let browser_runner = BrowserRunner::new(); + let browser_type = + BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {}", e))?; + let browser = create_browser(browser_type.clone()); + + // Load registry and check if already downloaded + let mut registry = DownloadedBrowsersRegistry::load() + .map_err(|e| format!("Failed to load browser registry: {}", e))?; + + if registry.is_browser_downloaded(&browser_str, &version) { + return Ok(version); + } + + // Use the centralized browser version service for download info + let version_service = BrowserVersionService::new(); + let download_info = version_service + .get_download_info(&browser_str, &version) + .map_err(|e| format!("Failed to get download info: {}", e))?; + + // Create browser directory + let mut browser_dir = browser_runner.get_binaries_dir(); + browser_dir.push(browser_type.as_str()); + browser_dir.push(&version); + + // Clean up any failed previous download + if let Err(e) = registry.cleanup_failed_download(&browser_str, &version) { + println!("Warning: Failed to cleanup previous download: {}", e); + } + + create_dir_all(&browser_dir).map_err(|e| format!("Failed to create browser directory: {}", e))?; + + // Mark download as started in registry + registry.mark_download_started(&browser_str, &version, browser_dir.clone()); + registry.save().map_err(|e| format!("Failed to save registry: {}", e))?; + + // Use the new download module + let downloader = Downloader::new(); + let download_path = match downloader + .download_browser(&app_handle, browser_type.clone(), &version, &download_info, &browser_dir) + .await + { + Ok(path) => path, + Err(e) => { + // Clean up failed download + let _ = registry.cleanup_failed_download(&browser_str, &version); + let _ = registry.save(); + return Err(format!("Failed to download browser: {}", e)); + } + }; + + // Use the new extraction module + if download_info.is_archive { + let extractor = Extractor::new(); + match extractor + .extract_browser(&app_handle, browser_type.clone(), &version, &download_path, &browser_dir) + .await + { + Ok(_) => { + // Clean up the downloaded archive + if let Err(e) = std::fs::remove_file(&download_path) { + println!("Warning: Could not delete archive file: {}", e); + } + } + Err(e) => { + // Clean up failed download + let _ = registry.cleanup_failed_download(&browser_str, &version); + let _ = registry.save(); + return Err(format!("Failed to extract browser: {}", e)); + } + } + + // Give filesystem a moment to settle after extraction + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + } + + // Emit verification progress + let progress = DownloadProgress { + browser: browser_str.clone(), + version: version.clone(), + downloaded_bytes: 0, + total_bytes: None, + percentage: 100.0, + speed_bytes_per_sec: 0.0, + eta_seconds: None, + stage: "verifying".to_string(), + }; + let _ = app_handle.emit("download-progress", &progress); + + // Verify the browser was downloaded correctly + println!("Verifying download for browser: {}, version: {}", browser_str, version); + + // Use the browser's own verification method + let binaries_dir = browser_runner.get_binaries_dir(); + if !browser.is_version_downloaded(&version, &binaries_dir) { + let _ = registry.cleanup_failed_download(&browser_str, &version); + let _ = registry.save(); + return Err("Browser download completed but verification failed".to_string()); + } + + // Mark download as completed in registry + let actual_version = if browser_str == "chromium" { + Some(version.clone()) + } else { + None + }; + + registry.mark_download_completed_with_actual_version(&browser_str, &version, actual_version) + .map_err(|e| format!("Failed to mark download as completed: {}", e))?; + registry.save().map_err(|e| format!("Failed to save registry: {}", e))?; + + // Emit completion + let progress = DownloadProgress { + browser: browser_str.clone(), + version: version.clone(), + downloaded_bytes: 0, + total_bytes: None, + percentage: 100.0, + speed_bytes_per_sec: 0.0, + eta_seconds: Some(0.0), + stage: "completed".to_string(), + }; + let _ = app_handle.emit("download-progress", &progress); + + Ok(version) +} + +#[tauri::command] +pub fn is_browser_downloaded(browser_str: String, version: String) -> bool { + if let Ok(registry) = DownloadedBrowsersRegistry::load() { + if registry.is_browser_downloaded(&browser_str, &version) { + return true; + } + } + let browser_type = BrowserType::from_str(&browser_str).expect("Invalid browser type"); + let browser_runner = BrowserRunner::new(); + let browser = create_browser(browser_type.clone()); + let binaries_dir = browser_runner.get_binaries_dir(); + browser.is_version_downloaded(&version, &binaries_dir) +} + +#[tauri::command] +pub fn check_browser_exists(browser_str: String, version: String) -> bool { + // This is an alias for is_browser_downloaded to provide clearer semantics for auto-updates + is_browser_downloaded(browser_str, version) +} + +#[tauri::command] +pub async fn kill_browser_profile( + app_handle: tauri::AppHandle, + profile: BrowserProfile, +) -> Result<(), String> { + let browser_runner = BrowserRunner::new(); + browser_runner.kill_browser_process(app_handle, &profile).await + .map_err(|e| format!("Failed to kill browser: {}", e)) +} + +#[tauri::command] +pub fn create_browser_profile_new( + name: String, + browser_str: String, + version: String, + proxy: Option, +) -> Result { + let browser_type = BrowserType::from_str(&browser_str) + .map_err(|e| format!("Invalid browser type: {}", e))?; + create_browser_profile(name, browser_type.as_str().to_string(), version, proxy) +} + +#[tauri::command] +pub async fn fetch_browser_versions(browser_str: String) -> Result, String> { + let service = BrowserVersionService::new(); + service.fetch_browser_versions(&browser_str, false).await + .map_err(|e| format!("Failed to fetch browser versions: {}", e)) +} + +#[tauri::command] +pub async fn fetch_browser_versions_with_count(browser_str: String) -> Result { + let service = BrowserVersionService::new(); + service.fetch_browser_versions_with_count(&browser_str, false).await + .map_err(|e| format!("Failed to fetch browser versions: {}", e)) +} + +#[tauri::command] +pub fn get_downloaded_browser_versions(browser_str: String) -> Result, String> { + let registry = DownloadedBrowsersRegistry::load() + .map_err(|e| format!("Failed to load browser registry: {}", e))?; + Ok(registry.get_downloaded_versions(&browser_str)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::browser::ProxySettings; + use tempfile::TempDir; + + fn create_test_browser_runner() -> (BrowserRunner, TempDir) { + let temp_dir = TempDir::new().unwrap(); + + // Mock the base directories by setting environment variables + std::env::set_var("HOME", temp_dir.path()); + + let browser_runner = BrowserRunner::new(); + (browser_runner, temp_dir) + } + + #[test] + fn test_browser_runner_creation() { + let (_runner, _temp_dir) = create_test_browser_runner(); + // If we get here without panicking, the test passes + } + + #[test] + fn test_get_binaries_dir() { + let (runner, _temp_dir) = create_test_browser_runner(); + let binaries_dir = runner.get_binaries_dir(); + + assert!(binaries_dir.to_string_lossy().contains("DonutBrowser")); + assert!(binaries_dir.to_string_lossy().contains("binaries")); + } + + #[test] + fn test_get_profiles_dir() { + let (runner, _temp_dir) = create_test_browser_runner(); + let profiles_dir = runner.get_profiles_dir(); + + assert!(profiles_dir.to_string_lossy().contains("DonutBrowser")); + assert!(profiles_dir.to_string_lossy().contains("profiles")); + } + + #[test] + fn test_create_profile() { + let (runner, _temp_dir) = create_test_browser_runner(); + + let profile = runner.create_profile( + "Test Profile", + "firefox", + "139.0", + None + ).unwrap(); + + assert_eq!(profile.name, "Test Profile"); + assert_eq!(profile.browser, "firefox"); + assert_eq!(profile.version, "139.0"); + assert!(profile.proxy.is_none()); + assert!(profile.process_id.is_none()); + } + + #[test] + fn test_create_profile_with_proxy() { + let (runner, _temp_dir) = create_test_browser_runner(); + + let proxy = ProxySettings { + enabled: true, + proxy_type: "http".to_string(), + host: "127.0.0.1".to_string(), + port: 8080, + }; + + let profile = runner.create_profile( + "Test Profile with Proxy", + "firefox", + "139.0", + Some(proxy.clone()) + ).unwrap(); + + assert_eq!(profile.name, "Test Profile with Proxy"); + assert!(profile.proxy.is_some()); + let profile_proxy = profile.proxy.unwrap(); + assert_eq!(profile_proxy.proxy_type, "http"); + assert_eq!(profile_proxy.host, "127.0.0.1"); + assert_eq!(profile_proxy.port, 8080); + } + + #[test] + fn test_save_and_load_profile() { + let (runner, _temp_dir) = create_test_browser_runner(); + + let profile = runner.create_profile( + "Test Save Load", + "firefox", + "139.0", + None + ).unwrap(); + + // Save the profile + runner.save_profile(&profile).unwrap(); + + // Load profiles and verify + let profiles = runner.list_profiles().unwrap(); + assert_eq!(profiles.len(), 1); + assert_eq!(profiles[0].name, "Test Save Load"); + assert_eq!(profiles[0].browser, "firefox"); + assert_eq!(profiles[0].version, "139.0"); + } + + #[test] + fn test_update_profile_proxy() { + let (runner, _temp_dir) = create_test_browser_runner(); + + // Create profile without proxy + let profile = runner.create_profile( + "Test Update Proxy", + "firefox", + "139.0", + None + ).unwrap(); + + assert!(profile.proxy.is_none()); + + // Update with proxy + let proxy = ProxySettings { + enabled: true, + proxy_type: "socks5".to_string(), + host: "192.168.1.1".to_string(), + port: 1080, + }; + + let updated_profile = runner.update_profile_proxy( + "Test Update Proxy", + Some(proxy.clone()) + ).unwrap(); + + assert!(updated_profile.proxy.is_some()); + let profile_proxy = updated_profile.proxy.unwrap(); + assert_eq!(profile_proxy.proxy_type, "socks5"); + assert_eq!(profile_proxy.host, "192.168.1.1"); + assert_eq!(profile_proxy.port, 1080); + } + + #[test] + fn test_rename_profile() { + let (runner, _temp_dir) = create_test_browser_runner(); + + // Create profile + let _profile = runner.create_profile( + "Original Name", + "firefox", + "139.0", + None + ).unwrap(); + + // Rename profile + let renamed_profile = runner.rename_profile( + "Original Name", + "New Name" + ).unwrap(); + + assert_eq!(renamed_profile.name, "New Name"); + + // Verify old profile is gone and new one exists + let profiles = runner.list_profiles().unwrap(); + assert_eq!(profiles.len(), 1); + assert_eq!(profiles[0].name, "New Name"); + } + + #[test] + fn test_delete_profile() { + let (runner, _temp_dir) = create_test_browser_runner(); + + // Create profile + let _profile = runner.create_profile( + "To Delete", + "firefox", + "139.0", + None + ).unwrap(); + + // Verify profile exists + let profiles = runner.list_profiles().unwrap(); + assert_eq!(profiles.len(), 1); + + // Delete profile + runner.delete_profile("To Delete").unwrap(); + + // Verify profile is gone + let profiles = runner.list_profiles().unwrap(); + assert_eq!(profiles.len(), 0); + } + + #[test] + fn test_profile_name_sanitization() { + let (runner, _temp_dir) = create_test_browser_runner(); + + // Create profile with spaces and special characters + let profile = runner.create_profile( + "Test Profile With Spaces", + "firefox", + "139.0", + None + ).unwrap(); + + // Profile path should use snake_case + assert!(profile.profile_path.contains("test_profile_with_spaces")); + } + + #[test] + fn test_multiple_profiles() { + let (runner, _temp_dir) = create_test_browser_runner(); + + // Create multiple profiles + let _profile1 = runner.create_profile("Profile 1", "firefox", "139.0", None).unwrap(); + let _profile2 = runner.create_profile("Profile 2", "chromium", "1465660", None).unwrap(); + let _profile3 = runner.create_profile("Profile 3", "brave", "v1.81.9", None).unwrap(); + + // List profiles + let profiles = runner.list_profiles().unwrap(); + assert_eq!(profiles.len(), 3); + + let profile_names: Vec<&str> = profiles.iter().map(|p| p.name.as_str()).collect(); + assert!(profile_names.contains(&"Profile 1")); + assert!(profile_names.contains(&"Profile 2")); + assert!(profile_names.contains(&"Profile 3")); + } + + #[test] + fn test_profile_validation() { + let (runner, _temp_dir) = create_test_browser_runner(); + + // Test that we can't rename to an existing profile name + let _profile1 = runner.create_profile("Profile 1", "firefox", "139.0", None).unwrap(); + let _profile2 = runner.create_profile("Profile 2", "firefox", "139.0", None).unwrap(); + + // Try to rename profile2 to profile1's name (should fail) + let result = runner.rename_profile("Profile 2", "Profile 1"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("already exists")); + } + + #[test] + fn test_error_handling() { + let (runner, _temp_dir) = create_test_browser_runner(); + + // Test updating non-existent profile + let result = runner.update_profile_proxy("Non Existent", None); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("not found")); + + // Test deleting non-existent profile + let result = runner.delete_profile("Non Existent"); + assert!(result.is_ok()); // Delete should be idempotent + + // Test renaming non-existent profile + let result = runner.rename_profile("Non Existent", "New Name"); + assert!(result.is_err()); + } + + #[test] + fn test_firefox_default_browser_preferences() { + let (runner, _temp_dir) = create_test_browser_runner(); + + // Create profile without proxy + let profile = runner.create_profile( + "Test Firefox Prefs", + "firefox", + "139.0", + None + ).unwrap(); + + // Check that user.js file was created with default browser preference + let user_js_path = std::path::Path::new(&profile.profile_path).join("user.js"); + assert!(user_js_path.exists()); + + let user_js_content = std::fs::read_to_string(user_js_path).unwrap(); + assert!(user_js_content.contains("browser.shell.checkDefaultBrowser")); + assert!(user_js_content.contains("false")); + + // Verify automatic update disabling preferences are present + assert!(user_js_content.contains("app.update.enabled")); + assert!(user_js_content.contains("app.update.auto")); + + // Create profile with proxy + let proxy = ProxySettings { + enabled: true, + proxy_type: "http".to_string(), + host: "127.0.0.1".to_string(), + port: 8080, + }; + + let profile_with_proxy = runner.create_profile( + "Test Firefox Prefs Proxy", + "firefox", + "139.0", + Some(proxy) + ).unwrap(); + + // Check that user.js file contains both proxy settings and default browser preference + let user_js_path_proxy = std::path::Path::new(&profile_with_proxy.profile_path).join("user.js"); + assert!(user_js_path_proxy.exists()); + + let user_js_content_proxy = std::fs::read_to_string(user_js_path_proxy).unwrap(); + assert!(user_js_content_proxy.contains("browser.shell.checkDefaultBrowser")); + assert!(user_js_content_proxy.contains("network.proxy.type")); + + // Verify automatic update disabling preferences are present even with proxy + assert!(user_js_content_proxy.contains("app.update.enabled")); + assert!(user_js_content_proxy.contains("app.update.auto")); + } +} diff --git a/src-tauri/src/browser_version_service.rs b/src-tauri/src/browser_version_service.rs new file mode 100644 index 0000000..a765a27 --- /dev/null +++ b/src-tauri/src/browser_version_service.rs @@ -0,0 +1,669 @@ +use crate::api_client::{ApiClient, BrowserRelease, sort_versions}; +use crate::browser::GithubRelease; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BrowserVersionInfo { + pub version: String, + pub is_prerelease: bool, + pub date: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BrowserVersionsResult { + pub versions: Vec, + pub new_versions_count: Option, + pub total_versions_count: usize, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DownloadInfo { + pub url: String, + pub filename: String, + pub is_archive: bool, // true for .dmg, .zip, etc. +} + +pub struct BrowserVersionService { + api_client: ApiClient, +} + +impl BrowserVersionService { + pub fn new() -> Self { + Self { + api_client: ApiClient::new(), + } + } + + /// Get cached browser versions immediately (returns None if no cache exists) + pub fn get_cached_browser_versions(&self, browser: &str) -> Option> { + self.api_client.load_cached_versions(browser) + } + + /// Get cached detailed browser version information immediately + pub fn get_cached_browser_versions_detailed(&self, browser: &str) -> Option> { + let cached_versions = self.api_client.load_cached_versions(browser)?; + + // Convert cached versions to detailed info (without dates since cache doesn't store them) + let detailed_info: Vec = cached_versions.into_iter().map(|version| { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: crate::api_client::is_alpha_version(&version), + date: "".to_string(), // Cache doesn't store dates + } + }).collect(); + + Some(detailed_info) + } + + /// Check if cache should be updated (expired or doesn't exist) + pub fn should_update_cache(&self, browser: &str) -> bool { + self.api_client.is_cache_expired(browser) + } + + /// Fetch browser versions with optional caching + pub async fn fetch_browser_versions( + &self, + browser: &str, + no_caching: bool, + ) -> Result, Box> { + let result = self.fetch_browser_versions_with_count(browser, no_caching).await?; + Ok(result.versions) + } + + /// Fetch browser versions with new count information and optional caching + pub async fn fetch_browser_versions_with_count( + &self, + browser: &str, + no_caching: bool, + ) -> Result> { + // Get existing cached versions to compare and merge + let existing_versions = self.api_client.load_cached_versions(browser).unwrap_or_default(); + let existing_set: HashSet = existing_versions.into_iter().collect(); + + // Fetch fresh versions from API + let fresh_versions = match browser { + "firefox" => self.fetch_firefox_versions(true).await?, // Always fetch fresh for merging + "firefox-developer" => self.fetch_firefox_developer_versions(true).await?, + "mullvad-browser" => self.fetch_mullvad_versions(true).await?, + "zen" => self.fetch_zen_versions(true).await?, + "brave" => self.fetch_brave_versions(true).await?, + "chromium" => self.fetch_chromium_versions(true).await?, + "tor-browser" => self.fetch_tor_versions(true).await?, + _ => return Err(format!("Unsupported browser: {}", browser).into()), + }; + + let fresh_set: HashSet = fresh_versions.into_iter().collect(); + + // Find new versions (in fresh but not in existing cache) + let new_versions: Vec = fresh_set.difference(&existing_set).cloned().collect(); + let new_versions_count = if existing_set.is_empty() { None } else { Some(new_versions.len()) }; + + // Merge existing and fresh versions + let mut merged_versions: Vec = existing_set.union(&fresh_set).cloned().collect(); + + // Sort versions using the existing sorting logic + crate::api_client::sort_versions(&mut merged_versions); + + // Save the merged cache (unless explicitly bypassing cache) + if !no_caching { + if let Err(e) = self.api_client.save_cached_versions(browser, &merged_versions) { + eprintln!("Failed to save merged cache for {}: {}", browser, e); + } + } + + let total_versions_count = merged_versions.len(); + + Ok(BrowserVersionsResult { + versions: merged_versions, + new_versions_count, + total_versions_count, + }) + } + + /// Fetch detailed browser version information with optional caching + pub async fn fetch_browser_versions_detailed( + &self, + browser: &str, + no_caching: bool, + ) -> Result, Box> { + // For detailed versions, we'll use the merged versions from fetch_browser_versions_with_count + // to ensure consistency with the version list + let versions_result = self.fetch_browser_versions_with_count(browser, no_caching).await?; + let merged_versions = versions_result.versions; + + // Convert the version strings to BrowserVersionInfo + // Since we don't have detailed date/prerelease info for cached versions, + // we'll fetch fresh detailed info and map it to our merged versions + let detailed_info: Vec = match browser { + "firefox" => { + let releases = self.fetch_firefox_releases_detailed(true).await?; + merged_versions.into_iter().map(|version| { + // Try to find matching release info, otherwise create basic info + if let Some(release) = releases.iter().find(|r| r.version == version) { + BrowserVersionInfo { + version: release.version.clone(), + is_prerelease: release.is_prerelease, + date: release.date.clone(), + } + } else { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: crate::api_client::is_alpha_version(&version), + date: "".to_string(), + } + } + }).collect() + } + "firefox-developer" => { + let releases = self.fetch_firefox_developer_releases_detailed(true).await?; + merged_versions.into_iter().map(|version| { + if let Some(release) = releases.iter().find(|r| r.version == version) { + BrowserVersionInfo { + version: release.version.clone(), + is_prerelease: release.is_prerelease, + date: release.date.clone(), + } + } else { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: crate::api_client::is_alpha_version(&version), + date: "".to_string(), + } + } + }).collect() + } + "mullvad-browser" => { + let releases = self.fetch_mullvad_releases_detailed(true).await?; + merged_versions.into_iter().map(|version| { + if let Some(release) = releases.iter().find(|r| r.tag_name == version) { + BrowserVersionInfo { + version: release.tag_name.clone(), + is_prerelease: release.is_alpha, + date: release.published_at.clone(), + } + } else { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: false, // Mullvad usually stable releases + date: "".to_string(), + } + } + }).collect() + } + "zen" => { + let releases = self.fetch_zen_releases_detailed(true).await?; + merged_versions.into_iter().map(|version| { + if let Some(release) = releases.iter().find(|r| r.tag_name == version) { + BrowserVersionInfo { + version: release.tag_name.clone(), + is_prerelease: release.prerelease, + date: release.published_at.clone(), + } + } else { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: version.contains("alpha") || version.contains("beta"), + date: "".to_string(), + } + } + }).collect() + } + "brave" => { + let releases = self.fetch_brave_releases_detailed(true).await?; + merged_versions.into_iter().map(|version| { + if let Some(release) = releases.iter().find(|r| r.tag_name == version) { + BrowserVersionInfo { + version: release.tag_name.clone(), + is_prerelease: release.prerelease, + date: release.published_at.clone(), + } + } else { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: version.contains("beta") || version.contains("dev"), + date: "".to_string(), + } + } + }).collect() + } + "chromium" => { + let releases = self.fetch_chromium_releases_detailed(true).await?; + merged_versions.into_iter().map(|version| { + if let Some(release) = releases.iter().find(|r| r.version == version) { + BrowserVersionInfo { + version: release.version.clone(), + is_prerelease: release.is_prerelease, + date: release.date.clone(), + } + } else { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: false, // Chromium versions are usually stable + date: "".to_string(), + } + } + }).collect() + } + "tor-browser" => { + let releases = self.fetch_tor_releases_detailed(true).await?; + merged_versions.into_iter().map(|version| { + if let Some(release) = releases.iter().find(|r| r.version == version) { + BrowserVersionInfo { + version: release.version.clone(), + is_prerelease: release.is_prerelease, + date: release.date.clone(), + } + } else { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: version.contains("alpha") || version.contains("rc"), + date: "".to_string(), + } + } + }).collect() + } + _ => return Err(format!("Unsupported browser: {}", browser).into()), + }; + + Ok(detailed_info) + } + + /// Update browser versions incrementally (for background updates) + pub async fn update_browser_versions_incrementally( + &self, + browser: &str, + ) -> Result> { + // Get existing cached versions + let existing_versions = self.api_client.load_cached_versions(browser) + .unwrap_or_default(); + let existing_set: HashSet = existing_versions.into_iter().collect(); + + // Fetch new versions (always bypass cache for background updates) + let new_versions = self.fetch_browser_versions(browser, true).await?; + let new_set: HashSet = new_versions.into_iter().collect(); + + // Find truly new versions (not in existing cache) + let really_new_versions: Vec = new_set.difference(&existing_set).cloned().collect(); + let new_versions_count = really_new_versions.len(); + + // Merge existing and new versions + let mut all_versions: Vec = existing_set.union(&new_set).cloned().collect(); + + // Sort versions using the existing sorting logic + sort_versions(&mut all_versions); + + // Save the updated cache + if let Err(e) = self.api_client.save_cached_versions(browser, &all_versions) { + eprintln!("Failed to save updated cache for {}: {}", browser, e); + } + + Ok(new_versions_count) + } + + /// Get download information for a specific browser and version + pub fn get_download_info(&self, browser: &str, version: &str) -> Result> { + match browser { + "firefox" => Ok(DownloadInfo { + url: format!("https://download.mozilla.org/?product=firefox-{}&os=osx&lang=en-US", version), + filename: format!("firefox-{}.dmg", version), + is_archive: true, + }), + "firefox-developer" => Ok(DownloadInfo { + url: format!("https://download.mozilla.org/?product=devedition-{}&os=osx&lang=en-US", version), + filename: format!("firefox-developer-{}.dmg", version), + is_archive: true, + }), + "mullvad-browser" => Ok(DownloadInfo { + url: format!( + "https://github.com/mullvad/mullvad-browser/releases/download/{}/mullvad-browser-macos-{}.dmg", + version, version + ), + filename: format!("mullvad-browser-{}.dmg", version), + is_archive: true, + }), + "zen" => Ok(DownloadInfo { + url: format!( + "https://github.com/zen-browser/desktop/releases/download/{}/zen.macos-universal.dmg", + version + ), + filename: format!("zen-{}.dmg", version), + is_archive: true, + }), + "brave" => { + // For Brave, we use a placeholder URL since we need to resolve the actual asset URL dynamically + // The actual URL will be resolved in the download service using the GitHub API + Ok(DownloadInfo { + url: format!( + "https://github.com/brave/brave-browser/releases/download/{}/Brave-Browser-universal.dmg", + version + ), + filename: format!("brave-{}.dmg", version), + is_archive: true, + }) + } + "chromium" => { + let arch = if cfg!(target_arch = "aarch64") { "Mac_Arm" } else { "Mac" }; + Ok(DownloadInfo { + url: format!( + "https://commondatastorage.googleapis.com/chromium-browser-snapshots/{}/{}/chrome-mac.zip", + arch, version + ), + filename: format!("chromium-{}.zip", version), + is_archive: true, + }) + } + "tor-browser" => Ok(DownloadInfo { + url: format!( + "https://archive.torproject.org/tor-package-archive/torbrowser/{}/tor-browser-macos-{}.dmg", + version, version + ), + filename: format!("tor-browser-{}.dmg", version), + is_archive: true, + }), + _ => Err(format!("Unsupported browser: {}", browser).into()), + } + } + + // Private helper methods for each browser type + + async fn fetch_firefox_versions(&self, no_caching: bool) -> Result, Box> { + let releases = self.fetch_firefox_releases_detailed(no_caching).await?; + Ok(releases.into_iter().map(|r| r.version).collect()) + } + + async fn fetch_firefox_releases_detailed(&self, no_caching: bool) -> Result, Box> { + self.api_client.fetch_firefox_releases_with_caching(no_caching).await + } + + async fn fetch_firefox_developer_versions(&self, no_caching: bool) -> Result, Box> { + let releases = self.fetch_firefox_developer_releases_detailed(no_caching).await?; + Ok(releases.into_iter().map(|r| r.version).collect()) + } + + async fn fetch_firefox_developer_releases_detailed(&self, no_caching: bool) -> Result, Box> { + self.api_client.fetch_firefox_developer_releases_with_caching(no_caching).await + } + + async fn fetch_mullvad_versions(&self, no_caching: bool) -> Result, Box> { + let releases = self.fetch_mullvad_releases_detailed(no_caching).await?; + Ok(releases.into_iter().map(|r| r.tag_name).collect()) + } + + async fn fetch_mullvad_releases_detailed(&self, no_caching: bool) -> Result, Box> { + self.api_client.fetch_mullvad_releases_with_caching(no_caching).await + } + + async fn fetch_zen_versions(&self, no_caching: bool) -> Result, Box> { + let releases = self.fetch_zen_releases_detailed(no_caching).await?; + Ok(releases.into_iter().map(|r| r.tag_name).collect()) + } + + async fn fetch_zen_releases_detailed(&self, no_caching: bool) -> Result, Box> { + self.api_client.fetch_zen_releases_with_caching(no_caching).await + } + + async fn fetch_brave_versions(&self, no_caching: bool) -> Result, Box> { + let releases = self.fetch_brave_releases_detailed(no_caching).await?; + Ok(releases.into_iter().map(|r| r.tag_name).collect()) + } + + async fn fetch_brave_releases_detailed(&self, no_caching: bool) -> Result, Box> { + self.api_client.fetch_brave_releases_with_caching(no_caching).await + } + + async fn fetch_chromium_versions(&self, no_caching: bool) -> Result, Box> { + let releases = self.fetch_chromium_releases_detailed(no_caching).await?; + Ok(releases.into_iter().map(|r| r.version).collect()) + } + + async fn fetch_chromium_releases_detailed(&self, no_caching: bool) -> Result, Box> { + self.api_client.fetch_chromium_releases_with_caching(no_caching).await + } + + async fn fetch_tor_versions(&self, no_caching: bool) -> Result, Box> { + let releases = self.fetch_tor_releases_detailed(no_caching).await?; + Ok(releases.into_iter().map(|r| r.version).collect()) + } + + async fn fetch_tor_releases_detailed(&self, no_caching: bool) -> Result, Box> { + self.api_client.fetch_tor_releases_with_caching(no_caching).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_browser_version_service_creation() { + let _service = BrowserVersionService::new(); + // Test passes if we can create the service without panicking + assert!(true); + } + + #[tokio::test] + async fn test_fetch_firefox_versions() { + let service = BrowserVersionService::new(); + + // Test with caching + let result_cached = service.fetch_browser_versions("firefox", false).await; + assert!(result_cached.is_ok(), "Should fetch Firefox versions with caching"); + + if let Ok(versions) = result_cached { + assert!(!versions.is_empty(), "Should have Firefox versions"); + println!("Firefox cached test passed. Found {} versions", versions.len()); + } + + // Small delay to avoid rate limiting + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + // Test without caching + let result_no_cache = service.fetch_browser_versions("firefox", true).await; + assert!(result_no_cache.is_ok(), "Should fetch Firefox versions without caching"); + + if let Ok(versions) = result_no_cache { + assert!(!versions.is_empty(), "Should have Firefox versions without caching"); + println!("Firefox no-cache test passed. Found {} versions", versions.len()); + } + } + + #[tokio::test] + async fn test_fetch_browser_versions_with_count() { + let service = BrowserVersionService::new(); + + let result = service.fetch_browser_versions_with_count("firefox", false).await; + assert!(result.is_ok(), "Should fetch Firefox versions with count"); + + if let Ok(result) = result { + assert!(!result.versions.is_empty(), "Should have versions"); + assert_eq!(result.total_versions_count, result.versions.len(), "Total count should match versions length"); + println!("Firefox count test passed. Found {} versions, new: {:?}", + result.total_versions_count, result.new_versions_count); + } + } + + #[tokio::test] + async fn test_fetch_detailed_versions() { + let service = BrowserVersionService::new(); + + let result = service.fetch_browser_versions_detailed("firefox", false).await; + assert!(result.is_ok(), "Should fetch detailed Firefox versions"); + + if let Ok(versions) = result { + assert!(!versions.is_empty(), "Should have detailed versions"); + + // Check that the first version has all required fields + let first_version = &versions[0]; + assert!(!first_version.version.is_empty(), "Version should not be empty"); + println!("Firefox detailed test passed. Found {} detailed versions", versions.len()); + } + } + + #[tokio::test] + async fn test_unsupported_browser() { + let service = BrowserVersionService::new(); + + let result = service.fetch_browser_versions("unsupported", false).await; + assert!(result.is_err(), "Should return error for unsupported browser"); + + if let Err(e) = result { + assert!(e.to_string().contains("Unsupported browser"), "Error should mention unsupported browser"); + } + } + + #[tokio::test] + async fn test_incremental_update() { + let service = BrowserVersionService::new(); + + // This test might fail if there are no cached versions yet, which is fine + let result = service.update_browser_versions_incrementally("firefox").await; + + // The test should complete without panicking + match result { + Ok(count) => { + println!("Incremental update test passed. Found {} new versions", count); + } + Err(e) => { + println!("Incremental update test failed (expected for first run): {}", e); + // Don't fail the test, as this is expected behavior for first run + } + } + } + + #[tokio::test] + async fn test_all_supported_browsers() { + let service = BrowserVersionService::new(); + let browsers = vec![ + "firefox", "firefox-developer", "mullvad-browser", + "zen", "brave", "chromium", "tor-browser" + ]; + + for browser in browsers { + // Test that we can at least call the function without panicking + let result = service.fetch_browser_versions(browser, false).await; + + match result { + Ok(versions) => { + println!("{} test passed. Found {} versions", browser, versions.len()); + } + Err(e) => { + // Some browsers might fail due to network issues, but shouldn't panic + println!("{} test failed (network issue): {}", browser, e); + } + } + + // Small delay between requests to avoid rate limiting + tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; + } + } + + #[tokio::test] + async fn test_no_caching_parameter() { + let service = BrowserVersionService::new(); + + // Test with caching enabled (default) + let result_cached = service.fetch_browser_versions("firefox", false).await; + assert!(result_cached.is_ok(), "Should fetch Firefox versions with caching"); + + // Small delay to avoid rate limiting + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + // Test with caching disabled (no_caching = true) + let result_no_cache = service.fetch_browser_versions("firefox", true).await; + assert!(result_no_cache.is_ok(), "Should fetch Firefox versions without caching"); + + // Both should return versions + if let (Ok(cached_versions), Ok(no_cache_versions)) = (result_cached, result_no_cache) { + assert!(!cached_versions.is_empty(), "Cached versions should not be empty"); + assert!(!no_cache_versions.is_empty(), "No-cache versions should not be empty"); + println!("No-caching test passed. Cached: {} versions, No-cache: {} versions", + cached_versions.len(), no_cache_versions.len()); + } + } + + #[tokio::test] + async fn test_detailed_versions_with_no_caching() { + let service = BrowserVersionService::new(); + + // Test detailed versions with caching + let result_cached = service.fetch_browser_versions_detailed("firefox", false).await; + assert!(result_cached.is_ok(), "Should fetch detailed Firefox versions with caching"); + + // Small delay to avoid rate limiting + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + // Test detailed versions without caching + let result_no_cache = service.fetch_browser_versions_detailed("firefox", true).await; + assert!(result_no_cache.is_ok(), "Should fetch detailed Firefox versions without caching"); + + // Both should return detailed version info + if let (Ok(cached_versions), Ok(no_cache_versions)) = (result_cached, result_no_cache) { + assert!(!cached_versions.is_empty(), "Cached detailed versions should not be empty"); + assert!(!no_cache_versions.is_empty(), "No-cache detailed versions should not be empty"); + + // Check that detailed versions have all required fields + let first_cached = &cached_versions[0]; + let first_no_cache = &no_cache_versions[0]; + + assert!(!first_cached.version.is_empty(), "Cached version should not be empty"); + assert!(!first_no_cache.version.is_empty(), "No-cache version should not be empty"); + + println!("Detailed no-caching test passed. Cached: {} versions, No-cache: {} versions", + cached_versions.len(), no_cache_versions.len()); + } + } + + #[test] + fn test_get_download_info() { + let service = BrowserVersionService::new(); + + // Test Firefox + let firefox_info = service.get_download_info("firefox", "139.0").unwrap(); + assert_eq!(firefox_info.filename, "firefox-139.0.dmg"); + assert!(firefox_info.url.contains("firefox-139.0")); + assert!(firefox_info.is_archive); + + // Test Firefox Developer + let firefox_dev_info = service.get_download_info("firefox-developer", "139.0b1").unwrap(); + assert_eq!(firefox_dev_info.filename, "firefox-developer-139.0b1.dmg"); + assert!(firefox_dev_info.url.contains("devedition-139.0b1")); + assert!(firefox_dev_info.is_archive); + + // Test Mullvad Browser + let mullvad_info = service.get_download_info("mullvad-browser", "14.5a6").unwrap(); + assert_eq!(mullvad_info.filename, "mullvad-browser-14.5a6.dmg"); + assert!(mullvad_info.url.contains("mullvad-browser-macos-14.5a6")); + assert!(mullvad_info.is_archive); + + // Test Zen Browser + let zen_info = service.get_download_info("zen", "1.11b").unwrap(); + assert_eq!(zen_info.filename, "zen-1.11b.dmg"); + assert!(zen_info.url.contains("zen.macos-universal.dmg")); + assert!(zen_info.is_archive); + + // Test Tor Browser + let tor_info = service.get_download_info("tor-browser", "14.0.4").unwrap(); + assert_eq!(tor_info.filename, "tor-browser-14.0.4.dmg"); + assert!(tor_info.url.contains("tor-browser-macos-14.0.4")); + assert!(tor_info.is_archive); + + // Test Chromium + let chromium_info = service.get_download_info("chromium", "1465660").unwrap(); + assert_eq!(chromium_info.filename, "chromium-1465660.zip"); + assert!(chromium_info.url.contains("chrome-mac.zip")); + assert!(chromium_info.is_archive); + + // Test Brave + let brave_info = service.get_download_info("brave", "v1.81.9").unwrap(); + assert_eq!(brave_info.filename, "brave-v1.81.9.dmg"); + assert!(brave_info.url.contains("Brave-Browser")); + assert!(brave_info.is_archive); + + // Test unsupported browser + let unsupported_result = service.get_download_info("unsupported", "1.0.0"); + assert!(unsupported_result.is_err()); + + println!("Download info test passed for all browsers"); + } +} \ No newline at end of file diff --git a/src-tauri/src/default_browser.rs b/src-tauri/src/default_browser.rs new file mode 100644 index 0000000..ab46787 --- /dev/null +++ b/src-tauri/src/default_browser.rs @@ -0,0 +1,169 @@ +use tauri::command; + +#[cfg(target_os = "macos")] +mod macos { + use core_foundation::base::OSStatus; + use core_foundation::string::CFStringRef; + use core_foundation::{ + base::TCFType, + string::CFString, + }; + + #[link(name = "CoreServices", kind = "framework")] + extern "C" { + fn LSSetDefaultHandlerForURLScheme(scheme: CFStringRef, bundle_id: CFStringRef) -> OSStatus; + fn LSCopyDefaultHandlerForURLScheme(scheme: CFStringRef) -> CFStringRef; + } + + pub fn is_default_browser() -> Result { + let schemes = ["http", "https"]; + let bundle_id = "com.donutbrowser"; + + for scheme in schemes { + let scheme_str = CFString::new(scheme); + unsafe { + let current_handler = LSCopyDefaultHandlerForURLScheme(scheme_str.as_concrete_TypeRef()); + if current_handler.is_null() { + return Ok(false); + } + + let current_handler_cf = CFString::wrap_under_create_rule(current_handler); + let current_handler_str = current_handler_cf.to_string(); + + if current_handler_str != bundle_id { + return Ok(false); + } + } + } + Ok(true) + } + + pub fn set_as_default_browser() -> Result<(), String> { + let bundle_id = CFString::new("com.donutbrowser"); + let schemes = ["http", "https"]; + + for scheme in schemes { + let scheme_str = CFString::new(scheme); + unsafe { + let status = LSSetDefaultHandlerForURLScheme( + scheme_str.as_concrete_TypeRef(), + bundle_id.as_concrete_TypeRef(), + ); + if status != 0 { + let error_msg = match status { + -54 => format!( + "Failed to set as default browser for scheme '{}'. The app is not properly registered as a browser. Please:\n1. Build and install the app properly\n2. Manually set Donut Browser as default in System Settings > General > Default web browser\n3. Make sure the app is in your Applications folder", + scheme + ), + _ => format!( + "Failed to set as default browser for scheme '{}'. Status code: {}. Please manually set Donut Browser as default in System Settings > General > Default web browser.", + scheme, status + ) + }; + return Err(error_msg); + } + } + } + Ok(()) + } +} + +#[cfg(target_os = "windows")] +mod windows { + pub fn is_default_browser() -> Result { + // Windows implementation would go here + Err("Windows support not implemented yet".to_string()) + } + + pub fn set_as_default_browser() -> Result<(), String> { + Err("Windows support not implemented yet".to_string()) + } +} + +#[cfg(target_os = "linux")] +mod linux { + pub fn is_default_browser() -> Result { + // Linux implementation would go here + Err("Linux support not implemented yet".to_string()) + } + + pub fn set_as_default_browser() -> Result<(), String> { + Err("Linux support not implemented yet".to_string()) + } +} + +#[command] +pub async fn is_default_browser() -> Result { + #[cfg(target_os = "macos")] + return macos::is_default_browser(); + + #[cfg(target_os = "windows")] + return windows::is_default_browser(); + + #[cfg(target_os = "linux")] + return linux::is_default_browser(); + + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + Err("Unsupported platform".to_string()) +} + +#[command] +pub async fn set_as_default_browser() -> Result<(), String> { + #[cfg(target_os = "macos")] + return macos::set_as_default_browser(); + + #[cfg(target_os = "windows")] + return windows::set_as_default_browser(); + + #[cfg(target_os = "linux")] + return linux::set_as_default_browser(); + + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + Err("Unsupported platform".to_string()) +} + +#[tauri::command] +pub async fn open_url_with_profile(app_handle: tauri::AppHandle, profile_name: String, url: String) -> Result<(), String> { + use crate::browser_runner::BrowserRunner; + + let runner = BrowserRunner::new(); + + // Get the profile by name + let profiles = runner.list_profiles().map_err(|e| format!("Failed to list profiles: {}", e))?; + let profile = profiles.into_iter() + .find(|p| p.name == profile_name) + .ok_or_else(|| format!("Profile '{}' not found", profile_name))?; + + println!("Opening URL '{}' with profile '{}'", url, profile_name); + + // Use launch_or_open_url which handles both launching new instances and opening in existing ones + runner + .launch_or_open_url(app_handle, &profile, Some(url.clone())) + .await + .map_err(|e| { + println!("Failed to open URL with profile '{}': {}", profile_name, e); + format!("Failed to open URL with profile: {}", e) + })?; + + println!("Successfully opened URL '{}' with profile '{}'", url, profile_name); + Ok(()) +} + +#[tauri::command] +pub async fn smart_open_url(_app_handle: tauri::AppHandle, _url: String, _is_startup: Option) -> Result { + use crate::browser_runner::BrowserRunner; + + let runner = BrowserRunner::new(); + + // Get all profiles + let profiles = runner.list_profiles().map_err(|e| format!("Failed to list profiles: {}", e))?; + + if profiles.is_empty() { + return Err("no_profiles".to_string()); + } + + println!("URL opening - Total profiles: {}, showing profile selector", profiles.len()); + + // Always show the profile selector so the user can choose + Err("show_selector".to_string()) +} diff --git a/src-tauri/src/download.rs b/src-tauri/src/download.rs new file mode 100644 index 0000000..9515dfb --- /dev/null +++ b/src-tauri/src/download.rs @@ -0,0 +1,368 @@ +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::io; +use std::path::{Path, PathBuf}; +use tauri::Emitter; + +use crate::api_client::ApiClient; +use crate::browser::BrowserType; +use crate::browser_version_service::DownloadInfo; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DownloadProgress { + pub browser: String, + pub version: String, + pub downloaded_bytes: u64, + pub total_bytes: Option, + pub percentage: f64, + pub speed_bytes_per_sec: f64, + pub eta_seconds: Option, + pub stage: String, // "downloading", "extracting", "verifying" +} + +pub struct Downloader { + client: Client, + api_client: ApiClient, +} + +impl Downloader { + pub fn new() -> Self { + Self { + client: Client::new(), + api_client: ApiClient::new(), + } + } + + /// Resolve the actual download URL for browsers that need dynamic asset resolution + pub async fn resolve_download_url( + &self, + browser_type: BrowserType, + version: &str, + download_info: &DownloadInfo, + ) -> Result> { + match browser_type { + BrowserType::Brave => { + // For Brave, we need to find the actual macOS asset + let releases = self.api_client.fetch_brave_releases().await?; + + // Find the release with the matching version + let release = releases + .iter() + .find(|r| r.tag_name == version || r.tag_name == format!("v{}", version.trim_start_matches('v'))) + .ok_or(format!("Brave version {} not found", version))?; + + // Find the universal macOS DMG asset + let asset = release + .assets + .iter() + .find(|asset| { + asset.name.contains(".dmg") && asset.name.contains("universal") + }) + .ok_or(format!("No universal macOS DMG asset found for Brave version {}", version))?; + + Ok(asset.browser_download_url.clone()) + } + BrowserType::Zen => { + // For Zen, verify the asset exists + let releases = self.api_client.fetch_zen_releases().await?; + + let release = releases + .iter() + .find(|r| r.tag_name == version) + .ok_or(format!("Zen version {} not found", version))?; + + // Find the macOS universal DMG asset + let asset = release + .assets + .iter() + .find(|asset| asset.name == "zen.macos-universal.dmg") + .ok_or(format!("No macOS universal asset found for Zen version {}", version))?; + + Ok(asset.browser_download_url.clone()) + } + BrowserType::MullvadBrowser => { + // For Mullvad, verify the asset exists + let releases = self.api_client.fetch_mullvad_releases().await?; + + let release = releases + .iter() + .find(|r| r.tag_name == version) + .ok_or(format!("Mullvad version {} not found", version))?; + + // Find the macOS DMG asset + let asset = release + .assets + .iter() + .find(|asset| { + asset.name.contains(".dmg") && asset.name.contains("mac") + }) + .ok_or(format!("No macOS asset found for Mullvad version {}", version))?; + + Ok(asset.browser_download_url.clone()) + } + _ => { + // For other browsers, use the provided URL + Ok(download_info.url.clone()) + } + } + } + + pub async fn download_browser( + &self, + app_handle: &tauri::AppHandle, + browser_type: BrowserType, + version: &str, + download_info: &DownloadInfo, + dest_path: &Path, + ) -> Result> { + let file_path = dest_path.join(&download_info.filename); + + // Resolve the actual download URL + let download_url = self.resolve_download_url(browser_type.clone(), version, download_info).await?; + + // Emit initial progress + let progress = DownloadProgress { + browser: browser_type.as_str().to_string(), + version: version.to_string(), + downloaded_bytes: 0, + total_bytes: None, + percentage: 0.0, + speed_bytes_per_sec: 0.0, + eta_seconds: None, + stage: "downloading".to_string(), + }; + + let _ = app_handle.emit("download-progress", &progress); + + // Start download + let response = self + .client + .get(&download_url) + .header("User-Agent", "donutbrowser") + .send() + .await?; + + let total_size = response.content_length(); + let mut downloaded = 0u64; + let start_time = std::time::Instant::now(); + let mut last_update = start_time; + + let mut file = File::create(&file_path)?; + let mut stream = response.bytes_stream(); + + use futures_util::StreamExt; + while let Some(chunk) = stream.next().await { + let chunk = chunk?; + io::copy(&mut chunk.as_ref(), &mut file)?; + downloaded += chunk.len() as u64; + + let now = std::time::Instant::now(); + // Update progress every 100ms to avoid too many events + if now.duration_since(last_update).as_millis() >= 100 { + let elapsed = start_time.elapsed().as_secs_f64(); + let speed = if elapsed > 0.0 { downloaded as f64 / elapsed } else { 0.0 }; + let percentage = if let Some(total) = total_size { + (downloaded as f64 / total as f64) * 100.0 + } else { + 0.0 + }; + let eta = if speed > 0.0 && total_size.is_some() { + Some((total_size.unwrap() - downloaded) as f64 / speed) + } else { + None + }; + + let progress = DownloadProgress { + browser: browser_type.as_str().to_string(), + version: version.to_string(), + downloaded_bytes: downloaded, + total_bytes: total_size, + percentage, + speed_bytes_per_sec: speed, + eta_seconds: eta, + stage: "downloading".to_string(), + }; + + let _ = app_handle.emit("download-progress", &progress); + last_update = now; + } + } + + Ok(file_path) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio; + + #[tokio::test] + async fn test_resolve_brave_download_url() { + let downloader = Downloader::new(); + + // Test with a known Brave version + let download_info = DownloadInfo { + url: "placeholder".to_string(), + filename: "brave-test.dmg".to_string(), + is_archive: true, + }; + + let result = downloader.resolve_download_url( + BrowserType::Brave, + "v1.81.9", + &download_info + ).await; + + match result { + Ok(url) => { + assert!(url.contains("github.com/brave/brave-browser")); + assert!(url.contains(".dmg")); + assert!(url.contains("universal")); + println!("Brave download URL resolved: {}", url); + } + Err(e) => { + println!("Brave URL resolution failed (expected if version doesn't exist): {}", e); + // This might fail if the version doesn't exist, which is okay for testing + } + } + } + + #[tokio::test] + async fn test_resolve_zen_download_url() { + let downloader = Downloader::new(); + + let download_info = DownloadInfo { + url: "placeholder".to_string(), + filename: "zen-test.dmg".to_string(), + is_archive: true, + }; + + let result = downloader.resolve_download_url( + BrowserType::Zen, + "1.11b", + &download_info + ).await; + + match result { + Ok(url) => { + assert!(url.contains("github.com/zen-browser/desktop")); + assert!(url.contains("zen.macos-universal.dmg")); + println!("Zen download URL resolved: {}", url); + } + Err(e) => { + println!("Zen URL resolution failed (expected if version doesn't exist): {}", e); + } + } + } + + #[tokio::test] + async fn test_resolve_mullvad_download_url() { + let downloader = Downloader::new(); + + let download_info = DownloadInfo { + url: "placeholder".to_string(), + filename: "mullvad-test.dmg".to_string(), + is_archive: true, + }; + + let result = downloader.resolve_download_url( + BrowserType::MullvadBrowser, + "14.5a6", + &download_info + ).await; + + match result { + Ok(url) => { + assert!(url.contains("github.com/mullvad/mullvad-browser")); + assert!(url.contains(".dmg")); + println!("Mullvad download URL resolved: {}", url); + } + Err(e) => { + println!("Mullvad URL resolution failed (expected if version doesn't exist): {}", e); + } + } + } + + #[tokio::test] + async fn test_resolve_firefox_download_url() { + let downloader = Downloader::new(); + + let download_info = DownloadInfo { + url: "https://download.mozilla.org/?product=firefox-139.0&os=osx&lang=en-US".to_string(), + filename: "firefox-test.dmg".to_string(), + is_archive: true, + }; + + let result = downloader.resolve_download_url( + BrowserType::Firefox, + "139.0", + &download_info + ).await; + + match result { + Ok(url) => { + assert_eq!(url, download_info.url); + println!("Firefox download URL (passthrough): {}", url); + } + Err(e) => { + panic!("Firefox URL resolution should not fail: {}", e); + } + } + } + + #[tokio::test] + async fn test_resolve_chromium_download_url() { + let downloader = Downloader::new(); + + let download_info = DownloadInfo { + url: "https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac/1465660/chrome-mac.zip".to_string(), + filename: "chromium-test.zip".to_string(), + is_archive: true, + }; + + let result = downloader.resolve_download_url( + BrowserType::Chromium, + "1465660", + &download_info + ).await; + + match result { + Ok(url) => { + assert_eq!(url, download_info.url); + println!("Chromium download URL (passthrough): {}", url); + } + Err(e) => { + panic!("Chromium URL resolution should not fail: {}", e); + } + } + } + + #[tokio::test] + async fn test_resolve_tor_download_url() { + let downloader = Downloader::new(); + + let download_info = DownloadInfo { + url: "https://archive.torproject.org/tor-package-archive/torbrowser/14.0.4/tor-browser-macos-14.0.4.dmg".to_string(), + filename: "tor-test.dmg".to_string(), + is_archive: true, + }; + + let result = downloader.resolve_download_url( + BrowserType::TorBrowser, + "14.0.4", + &download_info + ).await; + + match result { + Ok(url) => { + assert_eq!(url, download_info.url); + println!("TOR download URL (passthrough): {}", url); + } + Err(e) => { + panic!("TOR URL resolution should not fail: {}", e); + } + } + } +} \ No newline at end of file diff --git a/src-tauri/src/downloaded_browsers.rs b/src-tauri/src/downloaded_browsers.rs new file mode 100644 index 0000000..21fb855 --- /dev/null +++ b/src-tauri/src/downloaded_browsers.rs @@ -0,0 +1,258 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; +use directories::BaseDirs; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DownloadedBrowserInfo { + pub browser: String, + pub version: String, + pub download_date: u64, + pub file_path: PathBuf, + pub verified: bool, + pub actual_version: Option, // For browsers like Chromium where we track the actual version +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct DownloadedBrowsersRegistry { + pub browsers: HashMap>, // browser -> version -> info +} + +impl DownloadedBrowsersRegistry { + pub fn new() -> Self { + Self::default() + } + + pub fn load() -> Result> { + let registry_path = Self::get_registry_path()?; + + if !registry_path.exists() { + return Ok(Self::new()); + } + + let content = fs::read_to_string(®istry_path)?; + let registry: DownloadedBrowsersRegistry = serde_json::from_str(&content)?; + Ok(registry) + } + + pub fn save(&self) -> Result<(), Box> { + let registry_path = Self::get_registry_path()?; + + // Ensure parent directory exists + if let Some(parent) = registry_path.parent() { + fs::create_dir_all(parent)?; + } + + let content = serde_json::to_string_pretty(self)?; + fs::write(®istry_path, content)?; + Ok(()) + } + + fn get_registry_path() -> Result> { + let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?; + let mut path = base_dirs.data_local_dir().to_path_buf(); + path.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" }); + path.push("data"); + path.push("downloaded_browsers.json"); + Ok(path) + } + + pub fn add_browser(&mut self, info: DownloadedBrowserInfo) { + self.browsers + .entry(info.browser.clone()) + .or_insert_with(HashMap::new) + .insert(info.version.clone(), info); + } + + pub fn remove_browser(&mut self, browser: &str, version: &str) -> Option { + self.browsers + .get_mut(browser)? + .remove(version) + } + + pub fn is_browser_downloaded(&self, browser: &str, version: &str) -> bool { + self.browsers + .get(browser) + .and_then(|versions| versions.get(version)) + .map(|info| info.verified) + .unwrap_or(false) + } + + pub fn get_downloaded_versions(&self, browser: &str) -> Vec { + self.browsers + .get(browser) + .map(|versions| { + versions + .iter() + .filter(|(_, info)| info.verified) + .map(|(version, _)| version.clone()) + .collect() + }) + .unwrap_or_default() + } + + pub fn mark_download_started(&mut self, browser: &str, version: &str, file_path: PathBuf) { + let info = DownloadedBrowserInfo { + browser: browser.to_string(), + version: version.to_string(), + download_date: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(), + file_path, + verified: false, + actual_version: None, + }; + self.add_browser(info); + } + + pub fn mark_download_completed_with_actual_version(&mut self, browser: &str, version: &str, actual_version: Option) -> Result<(), String> { + if let Some(info) = self.browsers + .get_mut(browser) + .and_then(|versions| versions.get_mut(version)) + { + info.verified = true; + info.actual_version = actual_version; + Ok(()) + } else { + Err(format!("Browser {}:{} not found in registry", browser, version)) + } + } + + pub fn cleanup_failed_download(&mut self, browser: &str, version: &str) -> Result<(), Box> { + if let Some(info) = self.remove_browser(browser, version) { + // Clean up any files that might have been left behind + if info.file_path.exists() { + if info.file_path.is_dir() { + fs::remove_dir_all(&info.file_path)?; + } else { + fs::remove_file(&info.file_path)?; + } + } + + // Also clean up the browser directory if it exists + let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?; + let mut browser_dir = base_dirs.data_local_dir().to_path_buf(); + browser_dir.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" }); + browser_dir.push("binaries"); + browser_dir.push(browser); + browser_dir.push(version); + + if browser_dir.exists() { + fs::remove_dir_all(&browser_dir)?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + + #[test] + fn test_registry_creation() { + let registry = DownloadedBrowsersRegistry::new(); + assert!(registry.browsers.is_empty()); + } + + #[test] + fn test_add_and_get_browser() { + let mut registry = DownloadedBrowsersRegistry::new(); + let info = DownloadedBrowserInfo { + browser: "firefox".to_string(), + version: "139.0".to_string(), + download_date: 1234567890, + file_path: PathBuf::from("/test/path"), + verified: true, + actual_version: None, + }; + + registry.add_browser(info.clone()); + + assert!(registry.is_browser_downloaded("firefox", "139.0")); + assert!(!registry.is_browser_downloaded("firefox", "140.0")); + assert!(!registry.is_browser_downloaded("chrome", "139.0")); + } + + #[test] + fn test_get_downloaded_versions() { + let mut registry = DownloadedBrowsersRegistry::new(); + + let info1 = DownloadedBrowserInfo { + browser: "firefox".to_string(), + version: "139.0".to_string(), + download_date: 1234567890, + file_path: PathBuf::from("/test/path1"), + verified: true, + actual_version: None, + }; + + let info2 = DownloadedBrowserInfo { + browser: "firefox".to_string(), + version: "140.0".to_string(), + download_date: 1234567891, + file_path: PathBuf::from("/test/path2"), + verified: false, // Not verified, should not be included + actual_version: None, + }; + + let info3 = DownloadedBrowserInfo { + browser: "firefox".to_string(), + version: "141.0".to_string(), + download_date: 1234567892, + file_path: PathBuf::from("/test/path3"), + verified: true, + actual_version: None, + }; + + registry.add_browser(info1); + registry.add_browser(info2); + registry.add_browser(info3); + + let versions = registry.get_downloaded_versions("firefox"); + assert_eq!(versions.len(), 2); + assert!(versions.contains(&"139.0".to_string())); + assert!(versions.contains(&"141.0".to_string())); + assert!(!versions.contains(&"140.0".to_string())); + } + + #[test] + fn test_mark_download_lifecycle() { + let mut registry = DownloadedBrowsersRegistry::new(); + + // Mark download started + registry.mark_download_started("firefox", "139.0", PathBuf::from("/test/path")); + + // Should not be considered downloaded yet + assert!(!registry.is_browser_downloaded("firefox", "139.0")); + + // Mark as completed + registry.mark_download_completed_with_actual_version("firefox", "139.0", Some("139.0".to_string())).unwrap(); + + // Now should be considered downloaded + assert!(registry.is_browser_downloaded("firefox", "139.0")); + } + + #[test] + fn test_remove_browser() { + let mut registry = DownloadedBrowsersRegistry::new(); + let info = DownloadedBrowserInfo { + browser: "firefox".to_string(), + version: "139.0".to_string(), + download_date: 1234567890, + file_path: PathBuf::from("/test/path"), + verified: true, + actual_version: None, + }; + + registry.add_browser(info); + assert!(registry.is_browser_downloaded("firefox", "139.0")); + + let removed = registry.remove_browser("firefox", "139.0"); + assert!(removed.is_some()); + assert!(!registry.is_browser_downloaded("firefox", "139.0")); + } +} \ No newline at end of file diff --git a/src-tauri/src/extraction.rs b/src-tauri/src/extraction.rs new file mode 100644 index 0000000..181eccc --- /dev/null +++ b/src-tauri/src/extraction.rs @@ -0,0 +1,374 @@ +use std::fs::{self, create_dir_all}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use tauri::Emitter; + +use crate::download::DownloadProgress; +use crate::browser::BrowserType; + +pub struct Extractor; + +impl Extractor { + pub fn new() -> Self { + Self + } + + pub async fn extract_browser( + &self, + app_handle: &tauri::AppHandle, + browser_type: BrowserType, + version: &str, + archive_path: &Path, + dest_dir: &Path, + ) -> Result> { + // Emit extraction start progress + let progress = DownloadProgress { + browser: browser_type.as_str().to_string(), + version: version.to_string(), + downloaded_bytes: 0, + total_bytes: None, + percentage: 0.0, + speed_bytes_per_sec: 0.0, + eta_seconds: None, + stage: "extracting".to_string(), + }; + let _ = app_handle.emit("download-progress", &progress); + + let extension = archive_path + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or(""); + + match extension { + "dmg" => self.extract_dmg(archive_path, dest_dir).await, + "zip" => self.extract_zip(archive_path, dest_dir).await, + _ => Err(format!("Unsupported archive format: {}", extension).into()), + } + } + + async fn extract_dmg( + &self, + dmg_path: &Path, + dest_dir: &Path, + ) -> Result> { + // Create a temporary mount point + let mount_point = std::env::temp_dir().join(format!( + "donut_mount_{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + )); + create_dir_all(&mount_point)?; + + // Mount the DMG + let output = Command::new("hdiutil") + .args([ + "attach", + "-nobrowse", + "-mountpoint", + mount_point.to_str().unwrap(), + dmg_path.to_str().unwrap(), + ]) + .output()?; + + if !output.status.success() { + return Err( + format!( + "Failed to mount DMG: {}", + String::from_utf8_lossy(&output.stderr) + ) + .into(), + ); + } + + // Find the .app directory in the mount point + let app_entry = fs::read_dir(&mount_point)? + .filter_map(Result::ok) + .find(|entry| entry.path().extension().map_or(false, |ext| ext == "app")) + .ok_or("No .app found in DMG")?; + + // Copy the .app to the destination + let app_path = dest_dir.join(app_entry.file_name()); + + let output = Command::new("cp") + .args([ + "-R", + app_entry.path().to_str().unwrap(), + app_path.to_str().unwrap(), + ]) + .output()?; + + if !output.status.success() { + return Err( + format!( + "Failed to copy app: {}", + String::from_utf8_lossy(&output.stderr) + ) + .into(), + ); + } + + // Remove quarantine attributes + let _ = Command::new("xattr") + .args(["-dr", "com.apple.quarantine", app_path.to_str().unwrap()]) + .output(); + + let _ = Command::new("xattr") + .args(["-cr", app_path.to_str().unwrap()]) + .output(); + + // Try to unmount the DMG with retries + let mut retry_count = 0; + let max_retries = 3; + let mut unmounted = false; + + while retry_count < max_retries && !unmounted { + // Wait a bit before trying to unmount + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + let output = Command::new("hdiutil") + .args(["detach", mount_point.to_str().unwrap()]) + .output()?; + + if output.status.success() { + unmounted = true; + } else if retry_count == max_retries - 1 { + // Force unmount on last retry + let _ = Command::new("hdiutil") + .args(["detach", "-force", mount_point.to_str().unwrap()]) + .output(); + unmounted = true; // Consider it unmounted even if force fails + } + retry_count += 1; + } + + // Clean up mount point directory + let _ = fs::remove_dir_all(&mount_point); + + Ok(app_path) + } + + async fn extract_zip( + &self, + zip_path: &Path, + dest_dir: &Path, + ) -> Result> { + // Use unzip command to extract + let output = Command::new("unzip") + .args([ + "-q", // quiet + zip_path.to_str().unwrap(), + "-d", + dest_dir.to_str().unwrap(), + ]) + .output()?; + + if !output.status.success() { + return Err( + format!( + "Failed to extract zip: {}", + String::from_utf8_lossy(&output.stderr) + ) + .into(), + ); + } + + // Find the extracted .app directory or Chromium.app specifically + let mut app_path: Option = None; + + // First, try to find any .app file in the destination directory + if let Ok(entries) = fs::read_dir(dest_dir) { + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if path.extension().map_or(false, |ext| ext == "app") { + app_path = Some(path); + break; + } + // For Chromium, check subdirectories (chrome-mac folder) + if path.is_dir() { + if let Ok(sub_entries) = fs::read_dir(&path) { + for sub_entry in sub_entries { + if let Ok(sub_entry) = sub_entry { + let sub_path = sub_entry.path(); + if sub_path.extension().map_or(false, |ext| ext == "app") { + // Move the app to the root destination directory + let target_path = dest_dir.join(sub_path.file_name().unwrap()); + fs::rename(&sub_path, &target_path)?; + app_path = Some(target_path); + + // Clean up the now-empty subdirectory + let _ = fs::remove_dir_all(&path); + break; + } + } + } + if app_path.is_some() { + break; + } + } + } + } + } + } + + let app_path = app_path.ok_or("No .app found after extraction")?; + + // Remove quarantine attributes + let _ = Command::new("xattr") + .args(["-dr", "com.apple.quarantine", app_path.to_str().unwrap()]) + .output(); + + let _ = Command::new("xattr") + .args(["-cr", app_path.to_str().unwrap()]) + .output(); + + Ok(app_path) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use tempfile::TempDir; + + #[test] + fn test_extractor_creation() { + let _extractor = Extractor::new(); + // Just verify we can create an extractor instance + assert!(true); + } + + #[test] + fn test_unsupported_archive_format() { + let _extractor = Extractor::new(); + let temp_dir = TempDir::new().unwrap(); + let fake_archive = temp_dir.path().join("test.rar"); + File::create(&fake_archive).unwrap(); + + // Create a mock app handle (this won't work in real tests without Tauri runtime) + // For now, we'll just test the logic without the actual extraction + + // Test that unsupported formats return an error + let extension = fake_archive + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or(""); + + assert_eq!(extension, "rar"); + // We know this would fail with "Unsupported archive format: rar" + } + + #[test] + fn test_dmg_path_validation() { + let temp_dir = TempDir::new().unwrap(); + let dmg_path = temp_dir.path().join("test.dmg"); + + // Test that we can identify DMG files correctly + let extension = dmg_path + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or(""); + + assert_eq!(extension, "dmg"); + } + + #[test] + fn test_zip_path_validation() { + let temp_dir = TempDir::new().unwrap(); + let zip_path = temp_dir.path().join("test.zip"); + + // Test that we can identify ZIP files correctly + let extension = zip_path + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or(""); + + assert_eq!(extension, "zip"); + } + + #[test] + fn test_mount_point_generation() { + // Test that mount point generation creates unique paths + let mount_point1 = std::env::temp_dir().join(format!( + "donut_mount_{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + )); + + std::thread::sleep(std::time::Duration::from_millis(10)); + + let mount_point2 = std::env::temp_dir().join(format!( + "donut_mount_{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + )); + + // They should be different (or at least have the potential to be) + assert!(mount_point1.to_string_lossy().contains("donut_mount_")); + assert!(mount_point2.to_string_lossy().contains("donut_mount_")); + } + + #[test] + fn test_app_path_detection() { + let temp_dir = TempDir::new().unwrap(); + + // Create a fake .app directory + let app_dir = temp_dir.path().join("TestApp.app"); + std::fs::create_dir_all(&app_dir).unwrap(); + + // Test finding .app directories + let entries: Vec<_> = fs::read_dir(temp_dir.path()) + .unwrap() + .filter_map(Result::ok) + .filter(|entry| entry.path().extension().map_or(false, |ext| ext == "app")) + .collect(); + + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].file_name(), "TestApp.app"); + } + + #[test] + fn test_nested_app_detection() { + let temp_dir = TempDir::new().unwrap(); + + // Create a nested structure like Chromium + let chrome_dir = temp_dir.path().join("chrome-mac"); + std::fs::create_dir_all(&chrome_dir).unwrap(); + + let app_dir = chrome_dir.join("Chromium.app"); + std::fs::create_dir_all(&app_dir).unwrap(); + + // Test finding nested .app directories + let mut found_app = false; + + if let Ok(entries) = fs::read_dir(temp_dir.path()) { + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if path.is_dir() { + if let Ok(sub_entries) = fs::read_dir(&path) { + for sub_entry in sub_entries { + if let Ok(sub_entry) = sub_entry { + let sub_path = sub_entry.path(); + if sub_path.extension().map_or(false, |ext| ext == "app") { + found_app = true; + break; + } + } + } + } + } + } + } + } + + assert!(found_app); + } +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..469a9d7 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,225 @@ +// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ +use std::sync::Mutex; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::{Manager, Emitter}; +use tauri_plugin_deep_link::DeepLinkExt; + +// Store pending URLs that need to be handled when the window is ready +static PENDING_URLS: Mutex> = Mutex::new(Vec::new()); + +mod api_client; +mod auto_updater; +mod browser; +mod browser_runner; +mod browser_version_service; +mod default_browser; +mod download; +mod downloaded_browsers; +mod extraction; +mod proxy_manager; +mod settings_manager; +mod version_updater; + +extern crate lazy_static; + +use browser_runner::{ + check_browser_status, create_browser_profile, create_browser_profile_new, delete_profile, + download_browser, fetch_browser_versions, fetch_browser_versions_cached_first, + fetch_browser_versions_detailed, fetch_browser_versions_with_count, fetch_browser_versions_with_count_cached_first, + get_cached_browser_versions_detailed, get_downloaded_browser_versions, get_saved_mullvad_releases, get_supported_browsers, is_browser_downloaded, check_browser_exists, + kill_browser_profile, launch_browser_profile, list_browser_profiles, rename_profile, should_update_browser_cache, update_profile_proxy, + update_profile_version, +}; + +use settings_manager::{ + disable_default_browser_prompt, get_app_settings, save_app_settings, + should_show_settings_on_startup, get_table_sorting_settings, save_table_sorting_settings, +}; + +use default_browser::{is_default_browser, open_url_with_profile, set_as_default_browser, smart_open_url}; + +use version_updater::{trigger_manual_version_update, get_version_update_status, get_version_updater, check_version_update_needed, force_version_update_check}; + +use auto_updater::{ + check_for_browser_updates, start_browser_update, complete_browser_update, + is_browser_disabled_for_update, dismiss_update_notification, + complete_browser_update_with_auto_update, + mark_auto_update_download, remove_auto_update_download, is_auto_update_download, +}; + +#[tauri::command] +fn greet() -> String { + let now = SystemTime::now(); + let epoch_ms = now.duration_since(UNIX_EPOCH).unwrap().as_millis(); + format!("Hello world from Rust! Current epoch: {}", epoch_ms) +} + +#[tauri::command] +async fn handle_url_open(app: tauri::AppHandle, url: String) -> Result<(), String> { + println!("handle_url_open called with URL: {}", url); + + // Check if the main window exists and is ready + if let Some(window) = app.get_webview_window("main") { + if window.is_visible().unwrap_or(false) { + // Window is visible, emit event directly + println!("Main window is visible, emitting show-profile-selector event"); + app.emit("show-profile-selector", url.clone()) + .map_err(|e| format!("Failed to emit URL open event: {}", e))?; + let _ = window.show(); + let _ = window.set_focus(); + } else { + // Window not visible yet - add to pending URLs + println!("Main window not visible, adding URL to pending list"); + let mut pending = PENDING_URLS.lock().unwrap(); + pending.push(url); + } + } else { + // Window doesn't exist yet - add to pending URLs + println!("Main window doesn't exist, adding URL to pending list"); + let mut pending = PENDING_URLS.lock().unwrap(); + pending.push(url); + } + + Ok(()) +} + +#[tauri::command] +async fn check_and_handle_startup_url(app_handle: tauri::AppHandle) -> Result { + let pending_urls = { + let mut pending = PENDING_URLS.lock().unwrap(); + let urls = pending.clone(); + pending.clear(); // Clear after getting them + urls + }; + + if !pending_urls.is_empty() { + println!("Handling {} pending URLs from frontend request", pending_urls.len()); + + for url in pending_urls { + println!("Emitting show-profile-selector event for URL: {}", url); + if let Err(e) = app_handle.emit("show-profile-selector", url.clone()) { + eprintln!("Failed to emit URL event: {}", e); + return Err(format!("Failed to emit URL event: {}", e)); + } + } + + return Ok(true); + } + + Ok(false) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_deep_link::init()) + .setup(|app| { + // Set up deep link handler + let handle = app.handle().clone(); + + #[cfg(any(windows, target_os = "linux"))] + { + // For Windows and Linux, register all deep links at runtime for development + app.deep_link().register_all()?; + } + + // Handle deep links - this works for both scenarios: + // 1. App is running and URL is opened + // 2. App is not running and URL causes app to launch + app.deep_link().on_open_url({ + let handle = handle.clone(); + move |event| { + let urls = event.urls(); + for url in urls { + let url_string = url.to_string(); + println!("Deep link received: {}", url_string); + + // Clone the handle for each async task + let handle_clone = handle.clone(); + + // Handle the URL asynchronously + tauri::async_runtime::spawn(async move { + if let Err(e) = handle_url_open(handle_clone, url_string.clone()).await { + eprintln!("Failed to handle deep link URL: {}", e); + } + }); + } + } + }); + + // Initialize and start background version updater + let app_handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + let version_updater = get_version_updater(); + let mut updater_guard = version_updater.lock().await; + + // Set the app handle + updater_guard.set_app_handle(app_handle).await; + + // Start the background updates + updater_guard.start_background_updates().await; + }); + + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + greet, + get_supported_browsers, + download_browser, + delete_profile, + is_browser_downloaded, + check_browser_exists, + create_browser_profile_new, + create_browser_profile, // Keep for backward compatibility + list_browser_profiles, + launch_browser_profile, + fetch_browser_versions, + fetch_browser_versions_detailed, + fetch_browser_versions_with_count, + fetch_browser_versions_cached_first, + fetch_browser_versions_with_count_cached_first, + get_cached_browser_versions_detailed, + should_update_browser_cache, + get_downloaded_browser_versions, + get_saved_mullvad_releases, + update_profile_proxy, + update_profile_version, + check_browser_status, + kill_browser_profile, + rename_profile, + // Settings commands + get_app_settings, + save_app_settings, + should_show_settings_on_startup, + disable_default_browser_prompt, + get_table_sorting_settings, + save_table_sorting_settings, + // Default browser commands + is_default_browser, + open_url_with_profile, + set_as_default_browser, + smart_open_url, + handle_url_open, + check_and_handle_startup_url, + // Version update commands + trigger_manual_version_update, + get_version_update_status, + check_version_update_needed, + force_version_update_check, + // Auto-update commands + check_for_browser_updates, + start_browser_update, + complete_browser_update, + is_browser_disabled_for_update, + dismiss_update_notification, + complete_browser_update_with_auto_update, + mark_auto_update_download, + remove_auto_update_download, + is_auto_update_download, + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..29d4720 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + donutbrowser::run() +} diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs new file mode 100644 index 0000000..6420f3f --- /dev/null +++ b/src-tauri/src/proxy_manager.rs @@ -0,0 +1,189 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::Mutex; +use tauri_plugin_shell::ShellExt; + +use crate::browser::ProxySettings; + +// Store active proxy information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProxyInfo { + pub id: String, + pub local_url: String, + pub upstream_url: String, + pub local_port: u16, +} + +// Global proxy manager to track active proxies +pub struct ProxyManager { + active_proxies: Mutex>, // Maps browser process ID to proxy info + // Store proxy info by profile name for persistence across browser restarts + profile_proxies: Mutex>, // Maps profile name to (upstream_url, port) +} + +impl ProxyManager { + pub fn new() -> Self { + Self { + active_proxies: Mutex::new(HashMap::new()), + profile_proxies: Mutex::new(HashMap::new()), + } + } + + // Start a proxy for a given upstream URL and associate it with a browser process ID + pub async fn start_proxy( + &self, + app_handle: tauri::AppHandle, + upstream_url: &str, + browser_pid: u32, + profile_name: Option<&str>, + ) -> Result { + // Check if we already have a proxy for this browser + { + let proxies = self.active_proxies.lock().unwrap(); + if let Some(proxy) = proxies.get(&browser_pid) { + return Ok(ProxySettings { + enabled: true, + proxy_type: "http".to_string(), + host: "localhost".to_string(), + port: proxy.local_port, + }); + } + } + + // Check if we have a preferred port for this profile + let preferred_port = if let Some(name) = profile_name { + let profile_proxies = self.profile_proxies.lock().unwrap(); + profile_proxies.get(name).map(|(_, port)| *port) + } else { + None + }; + + // Start a new proxy using the nodecar binary + let mut nodecar = app_handle + .shell() + .sidecar("nodecar") + .unwrap() + .arg("proxy") + .arg("start") + .arg("-u") + .arg(upstream_url); + + // If we have a preferred port, use it + if let Some(port) = preferred_port { + nodecar = nodecar.arg("-p").arg(port.to_string()); + } + + let output = nodecar.output().await.unwrap(); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("Proxy start failed: {}", stderr)); + } + + let json_string = String::from_utf8(output.stdout) + .map_err(|e| format!("Failed to parse proxy output: {}", e))?; + + // Parse the JSON output + let json: Value = + serde_json::from_str(&json_string).map_err(|e| format!("Failed to parse JSON: {}", e))?; + + // Extract proxy information + let id = json["id"].as_str().ok_or("Missing proxy ID")?; + let local_port = json["localPort"].as_u64().ok_or("Missing local port")? as u16; + let local_url = json["localUrl"] + .as_str() + .ok_or("Missing local URL")? + .to_string(); + let upstream_url_str = json["upstreamUrl"] + .as_str() + .ok_or("Missing upstream URL")? + .to_string(); + + let proxy_info = ProxyInfo { + id: id.to_string(), + local_url, + upstream_url: upstream_url_str.clone(), + local_port, + }; + + // Store the proxy info + { + let mut proxies = self.active_proxies.lock().unwrap(); + proxies.insert(browser_pid, proxy_info.clone()); + } + + // Store the profile proxy info for persistence + if let Some(name) = profile_name { + let mut profile_proxies = self.profile_proxies.lock().unwrap(); + profile_proxies.insert(name.to_string(), (upstream_url_str, local_port)); + } + + // Return proxy settings for the browser + Ok(ProxySettings { + enabled: true, + proxy_type: "http".to_string(), + host: "localhost".to_string(), + port: proxy_info.local_port, + }) + } + + // Stop the proxy associated with a browser process ID + pub async fn stop_proxy( + &self, + app_handle: tauri::AppHandle, + browser_pid: u32, + ) -> Result<(), String> { + let proxy_id = { + let mut proxies = self.active_proxies.lock().unwrap(); + match proxies.remove(&browser_pid) { + Some(proxy) => proxy.id, + None => return Ok(()), // No proxy to stop + } + }; + + // Stop the proxy using the nodecar binary + let nodecar = app_handle + .shell() + .sidecar("nodecar") + .map_err(|e| format!("Failed to create sidecar: {}", e))? + .arg("proxy") + .arg("stop") + .arg("--id") + .arg(proxy_id); + + let output = nodecar.output().await.unwrap(); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + eprintln!("Proxy stop error: {}", stderr); + // We still return Ok since we've already removed the proxy from our tracking + } + + Ok(()) + } + + // Get proxy settings for a browser process ID + pub fn get_proxy_settings(&self, browser_pid: u32) -> Option { + let proxies = self.active_proxies.lock().unwrap(); + proxies.get(&browser_pid).map(|proxy| ProxySettings { + enabled: true, + proxy_type: "http".to_string(), + host: "localhost".to_string(), + port: proxy.local_port, + }) + } + + + + // Get stored proxy info for a profile + pub fn get_profile_proxy_info(&self, profile_name: &str) -> Option<(String, u16)> { + let profile_proxies = self.profile_proxies.lock().unwrap(); + profile_proxies.get(profile_name).cloned() + } +} + +// Create a singleton instance of the proxy manager +lazy_static::lazy_static! { + pub static ref PROXY_MANAGER: ProxyManager = ProxyManager::new(); +} diff --git a/src-tauri/src/settings_manager.rs b/src-tauri/src/settings_manager.rs new file mode 100644 index 0000000..4b6e7f9 --- /dev/null +++ b/src-tauri/src/settings_manager.rs @@ -0,0 +1,214 @@ +use directories::BaseDirs; +use serde::{Deserialize, Serialize}; +use std::fs::{self, create_dir_all}; +use std::path::PathBuf; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TableSortingSettings { + pub column: String, // Column to sort by: "name", "browser", "status" + pub direction: String, // "asc" or "desc" +} + +impl Default for TableSortingSettings { + fn default() -> Self { + Self { + column: "name".to_string(), + direction: "asc".to_string(), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AppSettings { + #[serde(default)] + pub set_as_default_browser: bool, + #[serde(default = "default_show_settings_on_startup")] + pub show_settings_on_startup: bool, + #[serde(default = "default_theme")] + pub theme: String, // "light", "dark", or "system" + #[serde(default = "default_auto_updates_enabled")] + pub auto_updates_enabled: bool, +} + +fn default_show_settings_on_startup() -> bool { + true +} + +fn default_theme() -> String { + "system".to_string() +} + +fn default_auto_updates_enabled() -> bool { + true +} + +impl Default for AppSettings { + fn default() -> Self { + Self { + set_as_default_browser: false, + show_settings_on_startup: default_show_settings_on_startup(), + theme: default_theme(), + auto_updates_enabled: default_auto_updates_enabled(), + } + } +} + +pub struct SettingsManager { + base_dirs: BaseDirs, +} + +impl SettingsManager { + pub fn new() -> Self { + Self { + base_dirs: BaseDirs::new().expect("Failed to get base directories"), + } + } + + pub fn get_settings_dir(&self) -> PathBuf { + let mut path = self.base_dirs.data_local_dir().to_path_buf(); + path.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" }); + path.push("settings"); + path + } + + pub fn get_settings_file(&self) -> PathBuf { + self.get_settings_dir().join("app_settings.json") + } + + pub fn get_table_sorting_file(&self) -> PathBuf { + self.get_settings_dir().join("table_sorting.json") + } + + pub fn load_settings(&self) -> Result> { + let settings_file = self.get_settings_file(); + + if !settings_file.exists() { + // Return default settings if file doesn't exist + return Ok(AppSettings::default()); + } + + let content = fs::read_to_string(&settings_file)?; + + // Parse the settings file - serde will use default values for missing fields + match serde_json::from_str::(&content) { + Ok(settings) => { + // Save the settings back to ensure any missing fields are written with defaults + if let Err(e) = self.save_settings(&settings) { + eprintln!("Warning: Failed to update settings file with defaults: {}", e); + } + Ok(settings) + } + Err(e) => { + eprintln!("Warning: Failed to parse settings file, using defaults: {}", e); + let default_settings = AppSettings::default(); + + // Try to save default settings to fix the corrupted file + if let Err(save_error) = self.save_settings(&default_settings) { + eprintln!("Warning: Failed to save default settings: {}", save_error); + } + + Ok(default_settings) + } + } + } + + pub fn save_settings(&self, settings: &AppSettings) -> Result<(), Box> { + let settings_dir = self.get_settings_dir(); + create_dir_all(&settings_dir)?; + + let settings_file = self.get_settings_file(); + let json = serde_json::to_string_pretty(settings)?; + fs::write(settings_file, json)?; + + Ok(()) + } + + pub fn load_table_sorting(&self) -> Result> { + let sorting_file = self.get_table_sorting_file(); + + if !sorting_file.exists() { + // Return default sorting if file doesn't exist + return Ok(TableSortingSettings::default()); + } + + let content = fs::read_to_string(sorting_file)?; + let sorting: TableSortingSettings = serde_json::from_str(&content)?; + Ok(sorting) + } + + pub fn save_table_sorting(&self, sorting: &TableSortingSettings) -> Result<(), Box> { + let settings_dir = self.get_settings_dir(); + create_dir_all(&settings_dir)?; + + let sorting_file = self.get_table_sorting_file(); + let json = serde_json::to_string_pretty(sorting)?; + fs::write(sorting_file, json)?; + + Ok(()) + } + + pub fn should_show_settings_on_startup(&self) -> Result> { + let settings = self.load_settings()?; + + // Show prompt if: + // 1. User wants to see the prompt + // 2. Donut Browser is not set as default + // 3. User hasn't explicitly disabled the default browser setting + Ok(settings.show_settings_on_startup && !settings.set_as_default_browser) + } + + pub fn disable_default_browser_prompt(&self) -> Result<(), Box> { + let mut settings = self.load_settings()?; + settings.show_settings_on_startup = false; + self.save_settings(&settings)?; + Ok(()) + } +} + +#[tauri::command] +pub async fn get_app_settings() -> Result { + let manager = SettingsManager::new(); + manager + .load_settings() + .map_err(|e| format!("Failed to load settings: {}", e)) +} + +#[tauri::command] +pub async fn save_app_settings(settings: AppSettings) -> Result<(), String> { + let manager = SettingsManager::new(); + manager + .save_settings(&settings) + .map_err(|e| format!("Failed to save settings: {}", e)) +} + +#[tauri::command] +pub async fn should_show_settings_on_startup() -> Result { + let manager = SettingsManager::new(); + manager + .should_show_settings_on_startup() + .map_err(|e| format!("Failed to check prompt setting: {}", e)) +} + +#[tauri::command] +pub async fn disable_default_browser_prompt() -> Result<(), String> { + let manager = SettingsManager::new(); + manager + .disable_default_browser_prompt() + .map_err(|e| format!("Failed to disable prompt: {}", e)) +} + +#[tauri::command] +pub async fn get_table_sorting_settings() -> Result { + let manager = SettingsManager::new(); + manager + .load_table_sorting() + .map_err(|e| format!("Failed to load table sorting settings: {}", e)) +} + +#[tauri::command] +pub async fn save_table_sorting_settings(sorting: TableSortingSettings) -> Result<(), String> { + let manager = SettingsManager::new(); + manager + .save_table_sorting(&sorting) + .map_err(|e| format!("Failed to save table sorting settings: {}", e)) +} diff --git a/src-tauri/src/version_updater.rs b/src-tauri/src/version_updater.rs new file mode 100644 index 0000000..d9aa9e3 --- /dev/null +++ b/src-tauri/src/version_updater.rs @@ -0,0 +1,535 @@ +use crate::browser_version_service::BrowserVersionService; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tauri::Emitter; +use tokio::sync::Mutex; +use tokio::time::{interval, Interval}; +use std::path::PathBuf; +use std::fs; +use directories::BaseDirs; +use std::sync::OnceLock; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct VersionUpdateProgress { + pub current_browser: String, + pub total_browsers: usize, + pub completed_browsers: usize, + pub new_versions_found: usize, + pub browser_new_versions: usize, // New versions found for current browser + pub status: String, // "updating", "completed", "error" +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BackgroundUpdateResult { + pub browser: String, + pub new_versions_count: usize, + pub total_versions_count: usize, + pub updated_successfully: bool, + pub error: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct BackgroundUpdateState { + last_update_time: u64, + update_interval_hours: u64, +} + +impl Default for BackgroundUpdateState { + fn default() -> Self { + Self { + last_update_time: 0, + update_interval_hours: 3, + } + } +} + +pub struct VersionUpdater { + version_service: BrowserVersionService, + app_handle: Arc>>, + update_interval: Interval, +} + +impl VersionUpdater { + pub fn new() -> Self { + let mut update_interval = interval(Duration::from_secs(5 * 60)); // Check every 5 minutes + update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + Self { + version_service: BrowserVersionService::new(), + app_handle: Arc::new(Mutex::new(None)), + update_interval, + } + } + + fn get_cache_dir() -> Result> { + let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?; + let app_name = if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }; + let cache_dir = base_dirs.cache_dir().join(app_name).join("version_cache"); + fs::create_dir_all(&cache_dir)?; + Ok(cache_dir) + } + + fn get_background_update_state_file() -> Result> { + let cache_dir = Self::get_cache_dir()?; + Ok(cache_dir.join("background_update_state.json")) + } + + fn load_background_update_state() -> BackgroundUpdateState { + let state_file = match Self::get_background_update_state_file() { + Ok(file) => file, + Err(_) => return BackgroundUpdateState::default(), + }; + + if !state_file.exists() { + return BackgroundUpdateState::default(); + } + + let content = match fs::read_to_string(&state_file) { + Ok(content) => content, + Err(_) => return BackgroundUpdateState::default(), + }; + + serde_json::from_str(&content).unwrap_or_default() + } + + fn save_background_update_state(state: &BackgroundUpdateState) -> Result<(), Box> { + let state_file = Self::get_background_update_state_file()?; + let content = serde_json::to_string_pretty(state)?; + fs::write(&state_file, content)?; + Ok(()) + } + + fn get_current_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() + } + + fn should_run_background_update() -> bool { + let state = Self::load_background_update_state(); + let current_time = Self::get_current_timestamp(); + let elapsed_secs = current_time.saturating_sub(state.last_update_time); + let update_interval_secs = state.update_interval_hours * 60 * 60; + + // Run update if: + // 1. Never updated before (last_update_time == 0) + // 2. More than 3 hours have passed since last update + let should_update = state.last_update_time == 0 || elapsed_secs >= update_interval_secs; + + if should_update { + println!( + "Background update needed: last_update={}, elapsed={}h, required={}h", + state.last_update_time, + elapsed_secs / 3600, + state.update_interval_hours + ); + } else { + println!( + "Background update not needed: last_update={}, elapsed={}h, required={}h", + state.last_update_time, + elapsed_secs / 3600, + state.update_interval_hours + ); + } + + should_update + } + + pub async fn set_app_handle(&self, app_handle: tauri::AppHandle) { + let mut handle = self.app_handle.lock().await; + *handle = Some(app_handle); + } + + pub async fn check_and_run_startup_update(&self) -> Result<(), Box> { + // Only run if an update is actually needed + if !Self::should_run_background_update() { + println!("No startup version update needed"); + return Ok(()); + } + + let app_handle = { + let handle_guard = self.app_handle.lock().await; + handle_guard.clone() + }; + + if let Some(handle) = app_handle { + println!("Running startup version update..."); + + match self.update_all_browser_versions(&handle).await { + Ok(_) => { + // Update the persistent state after successful update + let state = BackgroundUpdateState { + last_update_time: Self::get_current_timestamp(), + update_interval_hours: 3, + }; + + if let Err(e) = Self::save_background_update_state(&state) { + eprintln!("Failed to save background update state: {}", e); + } else { + println!("Startup version update completed successfully"); + } + } + Err(e) => { + eprintln!("Startup version update failed: {}", e); + return Err(e); + } + } + } else { + return Err("App handle not available for startup update".into()); + } + + Ok(()) + } + + pub async fn start_background_updates(&mut self) { + println!("Starting background version update service (checking every 5 minutes for 3-hour intervals)"); + + // Run initial startup check + if let Err(e) = self.check_and_run_startup_update().await { + eprintln!("Startup version update failed: {}", e); + } + + loop { + self.update_interval.tick().await; + + // Check if we should run an update based on persistent state + if !Self::should_run_background_update() { + continue; + } + + // Check if we have an app handle + let app_handle = { + let handle_guard = self.app_handle.lock().await; + handle_guard.clone() + }; + + if let Some(handle) = app_handle { + println!("Starting background version update..."); + + match self.update_all_browser_versions(&handle).await { + Ok(_) => { + // Update the persistent state after successful update + let state = BackgroundUpdateState { + last_update_time: Self::get_current_timestamp(), + update_interval_hours: 3, + }; + + if let Err(e) = Self::save_background_update_state(&state) { + eprintln!("Failed to save background update state: {}", e); + } else { + println!("Background version update completed successfully"); + } + } + Err(e) => { + eprintln!("Background version update failed: {}", e); + + // Emit error event + let progress = VersionUpdateProgress { + current_browser: "".to_string(), + total_browsers: 0, + completed_browsers: 0, + new_versions_found: 0, + browser_new_versions: 0, + status: "error".to_string(), + }; + let _ = handle.emit("version-update-progress", &progress); + } + } + } else { + println!("App handle not available, skipping background update"); + } + } + } + + async fn update_all_browser_versions(&self, app_handle: &tauri::AppHandle) -> Result, Box> { + println!("Starting background version update for all browsers"); + + let browsers = vec![ + "firefox", + "firefox-developer", + "mullvad-browser", + "zen", + "brave", + "chromium", + "tor-browser", + ]; + + let total_browsers = browsers.len(); + let mut results = Vec::new(); + let mut total_new_versions = 0; + + // Emit start event + let progress = VersionUpdateProgress { + current_browser: "".to_string(), + total_browsers, + completed_browsers: 0, + new_versions_found: 0, + browser_new_versions: 0, + status: "updating".to_string(), + }; + let _ = app_handle.emit("version-update-progress", &progress); + + for (index, browser) in browsers.iter().enumerate() { + // Check if individual browser cache is expired before updating + if !self.version_service.should_update_cache(browser) { + println!("Skipping {} - cache is still fresh", browser); + + let browser_result = BackgroundUpdateResult { + browser: browser.to_string(), + new_versions_count: 0, + total_versions_count: 0, + updated_successfully: true, + error: None, + }; + results.push(browser_result); + continue; + } + + println!("Updating versions for browser: {}", browser); + + // Emit progress for current browser + let progress = VersionUpdateProgress { + current_browser: browser.to_string(), + total_browsers, + completed_browsers: index, + new_versions_found: total_new_versions, + browser_new_versions: 0, + status: "updating".to_string(), + }; + let _ = app_handle.emit("version-update-progress", &progress); + + let result = self.update_browser_versions(browser).await; + + match result { + Ok(new_count) => { + total_new_versions += new_count; + let browser_result = BackgroundUpdateResult { + browser: browser.to_string(), + new_versions_count: new_count, + total_versions_count: 0, // We'll update this if needed + updated_successfully: true, + error: None, + }; + results.push(browser_result); + + println!("Found {} new versions for {}", new_count, browser); + } + Err(e) => { + eprintln!("Failed to update versions for {}: {}", browser, e); + let browser_result = BackgroundUpdateResult { + browser: browser.to_string(), + new_versions_count: 0, + total_versions_count: 0, + updated_successfully: false, + error: Some(e.to_string()), + }; + results.push(browser_result); + } + } + + // Small delay between browsers to avoid overwhelming APIs + tokio::time::sleep(Duration::from_millis(500)).await; + } + + // Emit completion event + let progress = VersionUpdateProgress { + current_browser: "".to_string(), + total_browsers, + completed_browsers: total_browsers, + new_versions_found: total_new_versions, + browser_new_versions: 0, + status: "completed".to_string(), + }; + let _ = app_handle.emit("version-update-progress", &progress); + + println!("Background version update completed. Found {} new versions total", total_new_versions); + + Ok(results) + } + + async fn update_browser_versions(&self, browser: &str) -> Result> { + self.version_service.update_browser_versions_incrementally(browser).await + } + + pub async fn trigger_manual_update(&self, app_handle: &tauri::AppHandle) -> Result, Box> { + let results = self.update_all_browser_versions(app_handle).await?; + + // Update the persistent state after successful manual update + let state = BackgroundUpdateState { + last_update_time: Self::get_current_timestamp(), + update_interval_hours: 3, + }; + + if let Err(e) = Self::save_background_update_state(&state) { + eprintln!("Failed to save background update state after manual update: {}", e); + } + + Ok(results) + } + + pub async fn get_last_update_time(&self) -> Option { + let state = Self::load_background_update_state(); + if state.last_update_time == 0 { + None + } else { + Some(state.last_update_time) + } + } + + pub async fn get_time_until_next_update(&self) -> u64 { + let state = Self::load_background_update_state(); + let current_time = Self::get_current_timestamp(); + + if state.last_update_time == 0 { + 0 // No previous update, should update now + } else { + let elapsed = current_time.saturating_sub(state.last_update_time); + let update_interval_secs = state.update_interval_hours * 60 * 60; + + if elapsed >= update_interval_secs { + 0 // Update overdue + } else { + update_interval_secs - elapsed + } + } + } +} + +// Global instance +static VERSION_UPDATER: OnceLock>> = OnceLock::new(); + +pub fn get_version_updater() -> Arc> { + VERSION_UPDATER.get_or_init(|| { + Arc::new(Mutex::new(VersionUpdater::new())) + }).clone() +} + +#[tauri::command] +pub async fn trigger_manual_version_update(app_handle: tauri::AppHandle) -> Result, String> { + let updater = get_version_updater(); + let updater_guard = updater.lock().await; + + updater_guard.trigger_manual_update(&app_handle) + .await + .map_err(|e| format!("Failed to trigger manual update: {}", e)) +} + +#[tauri::command] +pub async fn get_version_update_status() -> Result<(Option, u64), String> { + let updater = get_version_updater(); + let updater_guard = updater.lock().await; + + let last_update = updater_guard.get_last_update_time().await; + let time_until_next = updater_guard.get_time_until_next_update().await; + + Ok((last_update, time_until_next)) +} + +#[tauri::command] +pub async fn check_version_update_needed() -> Result { + Ok(VersionUpdater::should_run_background_update()) +} + +#[tauri::command] +pub async fn force_version_update_check(_app_handle: tauri::AppHandle) -> Result { + let updater = get_version_updater(); + let updater_guard = updater.lock().await; + + match updater_guard.check_and_run_startup_update().await { + Ok(_) => Ok(true), + Err(e) => Err(format!("Failed to run version update check: {}", e)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Helper function to create a unique test state file + fn get_test_state_file(test_name: &str) -> PathBuf { + let cache_dir = VersionUpdater::get_cache_dir().unwrap(); + cache_dir.join(format!("test_{}_state.json", test_name)) + } + + fn save_test_state(test_name: &str, state: &BackgroundUpdateState) -> Result<(), Box> { + let state_file = get_test_state_file(test_name); + let content = serde_json::to_string_pretty(state)?; + fs::write(&state_file, content)?; + Ok(()) + } + + fn load_test_state(test_name: &str) -> BackgroundUpdateState { + let state_file = get_test_state_file(test_name); + + if !state_file.exists() { + return BackgroundUpdateState::default(); + } + + let content = match fs::read_to_string(&state_file) { + Ok(content) => content, + Err(_) => return BackgroundUpdateState::default(), + }; + + serde_json::from_str(&content).unwrap_or_default() + } + + #[test] + fn test_background_update_state_persistence() { + let test_name = "persistence"; + + // Create a test state + let test_state = BackgroundUpdateState { + last_update_time: 1609459200, // 2021-01-01 00:00:00 UTC + update_interval_hours: 3, + }; + + // Save the state + save_test_state(test_name, &test_state).unwrap(); + + // Load the state back + let loaded_state = load_test_state(test_name); + + // Verify the values match + assert_eq!(loaded_state.last_update_time, test_state.last_update_time); + assert_eq!(loaded_state.update_interval_hours, test_state.update_interval_hours); + + // Clean up + let _ = fs::remove_file(get_test_state_file(test_name)); + } + + #[test] + fn test_should_run_background_update_logic() { + // Note: This test uses the shared state file, so results may vary + // depending on previous test runs. This is expected behavior. + + // Test with recent update (should not update) + let recent_state = BackgroundUpdateState { + last_update_time: VersionUpdater::get_current_timestamp() - 60, // 1 minute ago + update_interval_hours: 3, + }; + VersionUpdater::save_background_update_state(&recent_state).unwrap(); + assert!(!VersionUpdater::should_run_background_update()); + + // Test with old update (should update) + let old_state = BackgroundUpdateState { + last_update_time: VersionUpdater::get_current_timestamp() - (4 * 60 * 60), // 4 hours ago + update_interval_hours: 3, + }; + VersionUpdater::save_background_update_state(&old_state).unwrap(); + assert!(VersionUpdater::should_run_background_update()); + } + + #[test] + fn test_cache_dir_creation() { + // This should not panic and should create the directory if it doesn't exist + let cache_dir = VersionUpdater::get_cache_dir().unwrap(); + assert!(cache_dir.exists()); + assert!(cache_dir.is_dir()); + } +} \ No newline at end of file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..a14d29b --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Donut Browser", + "version": "0.1.0", + "identifier": "com.donutbrowser", + "build": { + "beforeDevCommand": "pnpm dev", + "devUrl": "http://localhost:3000", + "beforeBuildCommand": "pnpm build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "Donut Browser", + "width": 900, + "height": 600, + "resizable": false, + "fullscreen": false + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "externalBin": ["binaries/nodecar"], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico", + "icons/icon.png" + ], + "macOS": { + "frameworks": [], + "minimumSystemVersion": "10.13", + "exceptionDomain": "", + "signingIdentity": "-", + "providerShortName": null, + "entitlements": "entitlements.plist", + "files": { + "Info.plist": "Info.plist" + } + } + }, + "plugins": { + "deep-link": { + "schemes": ["http", "https"], + "domains": [] + } + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..7a564a3 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,35 @@ +"use client"; +import { Geist, Geist_Mono } from "next/font/google"; +import "@/styles/globals.css"; +import { CustomThemeProvider } from "@/components/theme-provider"; +import { Toaster } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..ff40778 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,563 @@ +"use client"; + +import { ChangeVersionDialog } from "@/components/change-version-dialog"; +import { CreateProfileDialog } from "@/components/create-profile-dialog"; +import { ProfilesDataTable } from "@/components/profile-data-table"; +import { ProfileSelectorDialog } from "@/components/profile-selector-dialog"; +import { ProxySettingsDialog } from "@/components/proxy-settings-dialog"; +import { SettingsDialog } from "@/components/settings-dialog"; +import { useUpdateNotifications } from "@/components/update-notification"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import type { BrowserProfile, ProxySettings } from "@/types"; +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { GoGear, GoPlus } from "react-icons/go"; +import { showErrorToast } from "@/components/custom-toast"; + +type BrowserTypeString = + | "mullvad-browser" + | "firefox" + | "firefox-developer" + | "chromium" + | "brave" + | "zen" + | "tor-browser"; + +interface PendingUrl { + id: string; + url: string; +} + +export default function Home() { + const [profiles, setProfiles] = useState([]); + const [error, setError] = useState(null); + const [proxyDialogOpen, setProxyDialogOpen] = useState(false); + const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false); + const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); + const [settingsDialogOpen, setSettingsDialogOpen] = useState(false); + const [pendingUrls, setPendingUrls] = useState([]); + const [currentProfileForProxy, setCurrentProfileForProxy] = + useState(null); + const [currentProfileForVersionChange, setCurrentProfileForVersionChange] = + useState(null); + const [isClient, setIsClient] = useState(false); + + // Auto-update functionality - only initialize on client + const updateNotifications = useUpdateNotifications(); + const { checkForUpdates, isUpdating } = updateNotifications; + + // Ensure we're on the client side to prevent hydration mismatches + useEffect(() => { + setIsClient(true); + }, []); + + const loadProfiles = useCallback(async () => { + if (!isClient) return; // Only run on client side + + try { + const profileList = await invoke( + "list_browser_profiles" + ); + setProfiles(profileList); + + // Check for updates after loading profiles + await checkForUpdates(); + } catch (err: unknown) { + console.error("Failed to load profiles:", err); + setError(`Failed to load profiles: ${JSON.stringify(err)}`); + } + }, [checkForUpdates, isClient]); + + useEffect(() => { + if (!isClient) return; // Only run on client side + + void loadProfiles(); + + // Check for startup default browser prompt + void checkStartupPrompt(); + + // Listen for URL open events + void listenForUrlEvents(); + + // Check for startup URLs (when app was launched as default browser) + void checkStartupUrls(); + + // Set up periodic update checks (every 30 minutes) + const updateInterval = setInterval(() => { + void checkForUpdates(); + }, 30 * 60 * 1000); + + return () => { + clearInterval(updateInterval); + }; + }, [loadProfiles, checkForUpdates, isClient]); + + const checkStartupPrompt = async () => { + if (!isClient) return; // Only run on client side + + try { + const shouldShow = await invoke( + "should_show_settings_on_startup" + ); + if (shouldShow) { + setSettingsDialogOpen(true); + } + } catch (error) { + console.error("Failed to check startup prompt:", error); + } + }; + + const checkStartupUrls = async () => { + if (!isClient) return; // Only run on client side + + try { + const hasStartupUrl = await invoke( + "check_and_handle_startup_url" + ); + if (hasStartupUrl) { + console.log("Handled startup URL successfully"); + } + } catch (error) { + console.error("Failed to check startup URLs:", error); + } + }; + + const listenForUrlEvents = async () => { + if (!isClient) return; // Only run on client side + + try { + // Listen for URL open events from the deep link handler (when app is already running) + await listen("url-open-request", (event) => { + console.log("Received URL open request:", event.payload); + void handleUrlOpen(event.payload); + }); + + // Listen for show profile selector events + await listen("show-profile-selector", (event) => { + console.log("Received show profile selector request:", event.payload); + setPendingUrls((prev) => [ + ...prev, + { id: Date.now().toString(), url: event.payload }, + ]); + }); + + // Listen for show create profile dialog events + await listen("show-create-profile-dialog", (event) => { + console.log( + "Received show create profile dialog request:", + event.payload + ); + setError( + "No profiles available. Please create a profile first before opening URLs." + ); + setCreateProfileDialogOpen(true); + }); + } catch (error) { + console.error("Failed to setup URL listener:", error); + } + }; + + const handleUrlOpen = async (url: string) => { + if (!isClient) return; // Only run on client side + + try { + // Use smart profile selection + const result = await invoke("smart_open_url", { + url, + }); + console.log("Smart URL opening succeeded:", result); + // URL was handled successfully + } catch (error: any) { + console.log( + "Smart URL opening failed or requires profile selection:", + error + ); + + // Check if it's the special error cases + if (error === "show_selector") { + // Show profile selector + setPendingUrls((prev) => [...prev, { id: Date.now().toString(), url }]); + } else if (error === "no_profiles") { + // No profiles available, show error message + setError( + "No profiles available. Please create a profile first before opening URLs." + ); + } else { + // Some other error occurred + console.error("Failed to open URL:", error); + setError(`Failed to open URL: ${error}`); + } + } + }; + + const openProxyDialog = useCallback((profile: BrowserProfile | null) => { + setCurrentProfileForProxy(profile); + setProxyDialogOpen(true); + }, []); + + const openChangeVersionDialog = useCallback((profile: BrowserProfile) => { + setCurrentProfileForVersionChange(profile); + setChangeVersionDialogOpen(true); + }, []); + + const handleSaveProxy = useCallback( + async (proxySettings: ProxySettings) => { + setProxyDialogOpen(false); + setError(null); + + try { + if (currentProfileForProxy) { + await invoke("update_profile_proxy", { + profileName: currentProfileForProxy.name, + proxy: proxySettings, + }); + } + await loadProfiles(); + } catch (err: unknown) { + console.error("Failed to update proxy settings:", err); + setError(`Failed to update proxy settings: ${JSON.stringify(err)}`); + } + }, + [currentProfileForProxy, loadProfiles] + ); + + const handleCreateProfile = useCallback( + async (profileData: { + name: string; + browserStr: BrowserTypeString; + version: string; + proxy?: ProxySettings; + }) => { + setError(null); + + try { + const profile = await invoke( + "create_browser_profile_new", + { + name: profileData.name, + browserStr: profileData.browserStr, + version: profileData.version, + } + ); + + // Update proxy if provided + if (profileData.proxy) { + await invoke("update_profile_proxy", { + profileName: profile.name, + proxy: profileData.proxy, + }); + } + + await loadProfiles(); + } catch (error) { + setError(`Failed to create profile: ${error as any}`); + throw error; + } + }, + [loadProfiles] + ); + + const [runningProfiles, setRunningProfiles] = useState>( + new Set() + ); + + const runningProfilesRef = useRef>(new Set()); + + const checkBrowserStatus = useCallback( + async (profile: BrowserProfile) => { + if (!isClient) return; // Only run on client side + + try { + const isRunning = await invoke("check_browser_status", { + profile, + }); + + const currentRunning = runningProfilesRef.current.has(profile.name); + + if (isRunning !== currentRunning) { + setRunningProfiles((prev) => { + const next = new Set(prev); + if (isRunning) { + next.add(profile.name); + } else { + next.delete(profile.name); + } + runningProfilesRef.current = next; + return next; + }); + } + } catch (err) { + console.error("Failed to check browser status:", err); + } + }, + [isClient] + ); + + const launchProfile = useCallback( + async (profile: BrowserProfile) => { + if (!isClient) return; // Only run on client side + + setError(null); + + // Check if browser is disabled due to ongoing update + try { + const isDisabled = await invoke( + "is_browser_disabled_for_update", + { + browser: profile.browser, + } + ); + + if (isDisabled || isUpdating(profile.browser)) { + setError( + `${profile.browser} is currently being updated. Please wait for the update to complete.` + ); + return; + } + } catch (err) { + console.error("Failed to check browser update status:", err); + } + + try { + const updatedProfile = await invoke( + "launch_browser_profile", + { profile } + ); + await loadProfiles(); + await checkBrowserStatus(updatedProfile); + } catch (err: unknown) { + console.error("Failed to launch browser:", err); + setError(`Failed to launch browser: ${JSON.stringify(err)}`); + } + }, + [loadProfiles, checkBrowserStatus, isUpdating, isClient] + ); + + useEffect(() => { + if (profiles.length === 0 || !isClient) return; + + const interval = setInterval(() => { + profiles.forEach((profile) => { + void checkBrowserStatus(profile); + }); + }, 500); + + return () => { + clearInterval(interval); + }; + }, [profiles, checkBrowserStatus, isClient]); + + useEffect(() => { + runningProfilesRef.current = runningProfiles; + }, [runningProfiles]); + + useEffect(() => { + if (error) { + showErrorToast(error); + setError(null); + } + }, [error]); + + const handleDeleteProfile = useCallback( + async (profile: BrowserProfile) => { + setError(null); + try { + await invoke("delete_profile", { profileName: profile.name }); + await loadProfiles(); + } catch (err: unknown) { + console.error("Failed to delete profile:", err); + setError(`Failed to delete profile: ${JSON.stringify(err)}`); + } + }, + [loadProfiles] + ); + + const handleRenameProfile = useCallback( + async (oldName: string, newName: string) => { + setError(null); + try { + await invoke("rename_profile", { oldName, newName }); + await loadProfiles(); + } catch (err: unknown) { + console.error("Failed to rename profile:", err); + setError(`Failed to rename profile: ${JSON.stringify(err)}`); + throw err; + } + }, + [loadProfiles] + ); + + const handleKillProfile = useCallback( + async (profile: BrowserProfile) => { + setError(null); + try { + await invoke("kill_browser_profile", { profile }); + await loadProfiles(); + } catch (err: unknown) { + console.error("Failed to kill browser:", err); + setError(`Failed to kill browser: ${JSON.stringify(err)}`); + } + }, + [loadProfiles] + ); + + // Don't render anything until we're on the client side to prevent hydration issues + if (!isClient) { + return ( +
+
+ + +
+ Profiles +
+ + + + + Settings + + + + + + Create a new profile + +
+
+
+ +
Loading...
+
+
+
+
+ ); + } + + return ( +
+
+ + +
+ Profiles +
+ + + + + Settings + + + + + + Create a new profile + +
+
+
+ + + +
+
+ + { + setProxyDialogOpen(false); + }} + onSave={(proxy: ProxySettings) => void handleSaveProxy(proxy)} + initialSettings={currentProfileForProxy?.proxy} + browserType={currentProfileForProxy?.browser} + /> + + { + setCreateProfileDialogOpen(false); + }} + onCreateProfile={handleCreateProfile} + /> + + { + setSettingsDialogOpen(false); + }} + /> + + { + setChangeVersionDialogOpen(false); + }} + profile={currentProfileForVersionChange} + onVersionChanged={() => void loadProfiles()} + /> + + {pendingUrls.map((pendingUrl) => ( + { + setPendingUrls((prev) => + prev.filter((u) => u.id !== pendingUrl.id) + ); + }} + url={pendingUrl.url} + runningProfiles={runningProfiles} + /> + ))} +
+ ); +} diff --git a/src/components/change-version-dialog.tsx b/src/components/change-version-dialog.tsx new file mode 100644 index 0000000..d23d8a8 --- /dev/null +++ b/src/components/change-version-dialog.tsx @@ -0,0 +1,196 @@ +"use client"; + +import { LoadingButton } from "@/components/loading-button"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { VersionSelector } from "@/components/version-selector"; +import { useBrowserDownload } from "@/hooks/use-browser-download"; +import type { BrowserProfile } from "@/types"; +import { invoke } from "@tauri-apps/api/core"; +import { LuTriangleAlert } from "react-icons/lu"; +import { useEffect, useState } from "react"; + +interface ChangeVersionDialogProps { + isOpen: boolean; + onClose: () => void; + profile: BrowserProfile | null; + onVersionChanged: () => void; +} + +export function ChangeVersionDialog({ + isOpen, + onClose, + profile, + onVersionChanged, +}: ChangeVersionDialogProps) { + const [selectedVersion, setSelectedVersion] = useState(null); + const [isUpdating, setIsUpdating] = useState(false); + const [showDowngradeWarning, setShowDowngradeWarning] = useState(false); + const [acknowledgeDowngrade, setAcknowledgeDowngrade] = useState(false); + + const { + availableVersions, + downloadedVersions, + isDownloading, + loadVersions, + loadDownloadedVersions, + downloadBrowser, + isVersionDownloaded, + } = useBrowserDownload(); + + useEffect(() => { + if (isOpen && profile) { + setSelectedVersion(profile.version); + setAcknowledgeDowngrade(false); + void loadVersions(profile.browser); + void loadDownloadedVersions(profile.browser); + } + }, [isOpen, profile, loadVersions, loadDownloadedVersions]); + + useEffect(() => { + if (profile && selectedVersion) { + // Check if this is a downgrade + const currentVersionIndex = availableVersions.findIndex( + (v) => v.tag_name === profile.version + ); + const selectedVersionIndex = availableVersions.findIndex( + (v) => v.tag_name === selectedVersion + ); + + // If selected version has a higher index, it's older (downgrade) + const isDowngrade = + currentVersionIndex !== -1 && + selectedVersionIndex !== -1 && + selectedVersionIndex > currentVersionIndex; + setShowDowngradeWarning(isDowngrade); + + if (!isDowngrade) { + setAcknowledgeDowngrade(false); + } + } + }, [selectedVersion, profile, availableVersions]); + + const handleDownload = async () => { + if (!profile || !selectedVersion) return; + await downloadBrowser(profile.browser, selectedVersion); + }; + + const handleVersionChange = async () => { + if (!profile || !selectedVersion) return; + + setIsUpdating(true); + try { + await invoke("update_profile_version", { + profileName: profile.name, + version: selectedVersion, + }); + onVersionChanged(); + onClose(); + } catch (error) { + console.error("Failed to update profile version:", error); + } finally { + setIsUpdating(false); + } + }; + + const canUpdate = + profile && + selectedVersion && + selectedVersion !== profile.version && + selectedVersion && + isVersionDownloaded(selectedVersion) && + (!showDowngradeWarning || acknowledgeDowngrade); + + if (!profile) return null; + + return ( + + + + Change Browser Version + + +
+
+ +
{profile.name}
+
+ +
+ +
+ {profile.version} +
+
+ + {/* Version Selection */} +
+ + { + void handleDownload(); + }} + placeholder="Select version..." + /> +
+ + {/* Downgrade Warning */} + {showDowngradeWarning && ( + + + + Downgrade Warning + + + You are about to downgrade from version {profile.version} to{" "} + {selectedVersion}. This may lead to compatibility issues, data + loss, or unexpected behavior. +
+ { + setAcknowledgeDowngrade(checked as boolean); + }} + /> + +
+
+
+ )} +
+ + + + { + void handleVersionChange(); + }} + disabled={!canUpdate} + > + {isUpdating ? "Updating..." : "Update Version"} + + +
+
+ ); +} diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx new file mode 100644 index 0000000..f8927cc --- /dev/null +++ b/src/components/create-profile-dialog.tsx @@ -0,0 +1,401 @@ +"use client"; + +import { LoadingButton } from "@/components/loading-button"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { VersionSelector } from "@/components/version-selector"; +import { useBrowserDownload } from "@/hooks/use-browser-download"; +import type { BrowserProfile, ProxySettings } from "@/types"; +import { invoke } from "@tauri-apps/api/core"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; + +type BrowserTypeString = + | "mullvad-browser" + | "firefox" + | "firefox-developer" + | "chromium" + | "brave" + | "zen" + | "tor-browser"; + +interface CreateProfileDialogProps { + isOpen: boolean; + onClose: () => void; + onCreateProfile: (profileData: { + name: string; + browserStr: BrowserTypeString; + version: string; + proxy?: ProxySettings; + }) => Promise; +} + +export function CreateProfileDialog({ + isOpen, + onClose, + onCreateProfile, +}: CreateProfileDialogProps) { + const [profileName, setProfileName] = useState(""); + const [selectedBrowser, setSelectedBrowser] = + useState("mullvad-browser"); + const [selectedVersion, setSelectedVersion] = useState(null); + const [supportedBrowsers, setSupportedBrowsers] = useState< + BrowserTypeString[] + >([]); + const [isCreating, setIsCreating] = useState(false); + const [existingProfiles, setExistingProfiles] = useState( + [] + ); + + // Proxy settings + const [proxyEnabled, setProxyEnabled] = useState(false); + const [proxyType, setProxyType] = useState("http"); + const [proxyHost, setProxyHost] = useState(""); + const [proxyPort, setProxyPort] = useState(8080); + + const { + availableVersions, + downloadedVersions, + isDownloading, + loadVersions, + loadDownloadedVersions, + downloadBrowser, + isVersionDownloaded, + } = useBrowserDownload(); + + useEffect(() => { + if (isOpen) { + void loadSupportedBrowsers(); + void loadExistingProfiles(); + } + }, [isOpen]); + + useEffect(() => { + if (isOpen && selectedBrowser) { + // Reset selected version when browser changes + setSelectedVersion(null); + void loadVersions(selectedBrowser); + void loadDownloadedVersions(selectedBrowser); + } + }, [isOpen, selectedBrowser, loadVersions, loadDownloadedVersions]); + + // Set default version when versions are loaded and no version is selected + useEffect(() => { + if (availableVersions.length > 0 && selectedBrowser) { + // Always reset version when browser changes or versions are loaded + // Find the latest stable version (not alpha/beta) + const stableVersions = availableVersions.filter((v) => !v.is_alpha); + + if (stableVersions.length > 0) { + // Select the first stable version (they're already sorted newest first) + setSelectedVersion(stableVersions[0].tag_name); + } else if (availableVersions.length > 0) { + // If no stable version found, select the first available version + setSelectedVersion(availableVersions[0].tag_name); + } + } + }, [availableVersions, selectedBrowser]); + + const loadSupportedBrowsers = async () => { + try { + const browsers = await invoke( + "get_supported_browsers" + ); + setSupportedBrowsers(browsers); + if (browsers.includes("mullvad-browser")) { + setSelectedBrowser("mullvad-browser"); + } else if (browsers.length > 0) { + setSelectedBrowser(browsers[0]); + } + } catch (error) { + console.error("Failed to load supported browsers:", error); + } + }; + + const loadExistingProfiles = async () => { + try { + const profiles = await invoke("list_browser_profiles"); + setExistingProfiles(profiles); + } catch (error) { + console.error("Failed to load existing profiles:", error); + } + }; + + const handleDownload = async () => { + if (!selectedBrowser || !selectedVersion) return; + await downloadBrowser(selectedBrowser, selectedVersion); + }; + + const validateProfileName = (name: string): string | null => { + const trimmedName = name.trim(); + + if (!trimmedName) { + return "Profile name cannot be empty"; + } + + // Check for duplicate names (case insensitive) + const isDuplicate = existingProfiles.some( + (profile) => profile.name.toLowerCase() === trimmedName.toLowerCase() + ); + + if (isDuplicate) { + return "A profile with this name already exists"; + } + + return null; + }; + + // Helper to determine if proxy should be disabled for the selected browser + const isProxyDisabled = selectedBrowser === "tor-browser"; + + // Update proxy enabled state when browser changes to tor-browser + useEffect(() => { + if (selectedBrowser === "tor-browser" && proxyEnabled) { + setProxyEnabled(false); + } + }, [selectedBrowser, proxyEnabled]); + + const handleCreate = async () => { + if (!profileName.trim() || !selectedBrowser || !selectedVersion) return; + + // Validate profile name + const nameError = validateProfileName(profileName); + if (nameError) { + toast.error(nameError); + return; + } + + setIsCreating(true); + try { + const proxy = + proxyEnabled && !isProxyDisabled + ? { + enabled: true, + proxy_type: proxyType, + host: proxyHost, + port: proxyPort, + } + : undefined; + + await onCreateProfile({ + name: profileName.trim(), + browserStr: selectedBrowser, + version: selectedVersion, + proxy, + }); + + // Reset form + setProfileName(""); + setSelectedVersion(null); + setProxyEnabled(false); + setProxyHost(""); + setProxyPort(8080); + onClose(); + } catch (error) { + console.error("Failed to create profile:", error); + } finally { + setIsCreating(false); + } + }; + + const nameError = profileName.trim() + ? validateProfileName(profileName) + : null; + const canCreate = + profileName.trim() && + selectedBrowser && + selectedVersion && + isVersionDownloaded(selectedVersion) && + (!proxyEnabled || isProxyDisabled || (proxyHost && proxyPort)) && + !nameError; + + return ( + + + + Create New Profile + + +
+ {/* Profile Name */} +
+ + { + setProfileName(e.target.value); + }} + placeholder="Enter profile name" + className={nameError ? "border-red-500" : ""} + /> + {nameError &&

{nameError}

} +
+ + {/* Browser Selection */} +
+ + +
+ + {/* Version Selection */} +
+ + { + void handleDownload(); + }} + placeholder="Select version..." + /> +
+ + {/* Proxy Settings */} +
+
+ {isProxyDisabled ? ( + + +
+ + +
+
+ +

+ Tor Browser has its own built-in proxy system and + doesn't support additional proxy configuration +

+
+
+ ) : ( + <> + { + setProxyEnabled(checked as boolean); + }} + /> + + + )} +
+ + {proxyEnabled && !isProxyDisabled && ( + <> +
+ + +
+ +
+ + { + setProxyHost(e.target.value); + }} + placeholder="e.g. 127.0.0.1" + /> +
+ +
+ + { + setProxyPort(Number.parseInt(e.target.value, 10) || 0); + }} + placeholder="e.g. 8080" + min="1" + max="65535" + /> +
+ + )} +
+
+ + + + void handleCreate()} + disabled={!canCreate} + > + Create Profile + + +
+
+ ); +} diff --git a/src/components/custom-toast.tsx b/src/components/custom-toast.tsx new file mode 100644 index 0000000..3ce0adf --- /dev/null +++ b/src/components/custom-toast.tsx @@ -0,0 +1,417 @@ +/** + * Unified Toast System + * + * This module provides a comprehensive toast system that solves styling issues + * and provides a single, flexible toast component for all use cases. + * + * Features: + * - Proper background styling (no transparency issues) + * - Loading states with spinners + * - Progress bars for downloads/updates + * - Success/error states + * - Customizable icons and content + * + * Usage Examples: + * + * Simple loading toast: + * ``` + * import { showToast } from "./custom-toast"; + * showToast({ + * type: "loading", + * title: "Loading...", + * description: "Please wait..." + * }); + * ``` + * + * Download progress toast: + * ``` + * showToast({ + * type: "download", + * title: "Downloading Firefox 123.0", + * progress: { percentage: 45, speed: "2.5", eta: "30s" } + * }); + * ``` + * + * Version update progress: + * ``` + * showToast({ + * type: "version-update", + * title: "Updating browser versions", + * progress: { current: 3, total: 5, found: 12 } + * }); + * ``` + */ + +import React from "react"; +import { toast as sonnerToast } from "sonner"; +import { + LuCheckCheck, + LuTriangleAlert, + LuDownload, + LuRefreshCw, +} from "react-icons/lu"; + +interface BaseToastProps { + id?: string; + title: string; + description?: string; + duration?: number; +} + +interface LoadingToastProps extends BaseToastProps { + type: "loading"; +} + +interface SuccessToastProps extends BaseToastProps { + type: "success"; +} + +interface ErrorToastProps extends BaseToastProps { + type: "error"; +} + +interface DownloadToastProps extends BaseToastProps { + type: "download"; + stage?: "downloading" | "extracting" | "verifying" | "completed"; + progress?: { + percentage: number; + speed?: string; + eta?: string; + }; +} + +interface VersionUpdateToastProps extends BaseToastProps { + type: "version-update"; + progress?: { + current: number; + total: number; + found: number; + }; +} + +interface FetchingToastProps extends BaseToastProps { + type: "fetching"; + browserName?: string; +} + +type ToastProps = + | LoadingToastProps + | SuccessToastProps + | ErrorToastProps + | DownloadToastProps + | VersionUpdateToastProps + | FetchingToastProps; + +function getToastIcon(type: ToastProps["type"], stage?: string) { + switch (type) { + case "success": + return ; + case "error": + return ; + case "download": + if (stage === "completed") { + return ( + + ); + } + return ; + case "version-update": + return ( + + ); + case "fetching": + return ( + + ); + case "loading": + default: + return ( +
+ ); + } +} + +export function UnifiedToast(props: ToastProps) { + const { title, description, type } = props; + const stage = "stage" in props ? props.stage : undefined; + const progress = "progress" in props ? props.progress : undefined; + + return ( +
+
{getToastIcon(type, stage)}
+
+

+ {title} +

+ + {/* Download progress */} + {type === "download" && + progress && + "percentage" in progress && + stage === "downloading" && ( +
+
+

+ {progress.percentage.toFixed(1)}% + {progress.speed && ` • ${progress.speed} MB/s`} + {progress.eta && ` • ${progress.eta} remaining`} +

+
+
+
+
+
+ )} + + {/* Version update progress */} + {type === "version-update" && progress && "found" in progress && ( +
+

+ {progress.found} new versions found so far +

+
+
+
+
+ + {progress.current}/{progress.total} + +
+
+ )} + + {/* Description */} + {description && ( +

+ {description} +

+ )} + + {/* Stage-specific descriptions for downloads */} + {type === "download" && !description && ( + <> + {stage === "extracting" && ( +

+ Extracting browser files... +

+ )} + {stage === "verifying" && ( +

+ Verifying installation... +

+ )} + + )} +
+
+ ); +} + +// Unified toast function +export function showToast(props: ToastProps & { id?: string }) { + const toastId = props.id ?? `toast-${props.type}-${Date.now()}`; + + // Improved duration logic - make toasts disappear more quickly + let duration: number; + if (props.duration !== undefined) { + duration = props.duration; + } else { + switch (props.type) { + case "loading": + case "fetching": + duration = 10000; // 10 seconds instead of infinite + break; + case "download": + // Only keep infinite for active downloading, others get shorter durations + if ("stage" in props && props.stage === "downloading") { + duration = Number.POSITIVE_INFINITY; + } else if ("stage" in props && props.stage === "completed") { + duration = 3000; // Shorter duration for completed downloads + } else { + duration = 8000; // 8 seconds for extracting/verifying + } + break; + case "version-update": + duration = 15000; // 15 seconds instead of infinite + break; + case "success": + duration = 3000; // Shorter success duration + break; + case "error": + duration = 5000; // Reasonable error duration + break; + default: + duration = 4000; + } + } + + if (props.type === "success") { + sonnerToast.success(, { + id: toastId, + duration, + style: { + background: "transparent", + border: "none", + boxShadow: "none", + padding: 0, + }, + }); + } else if (props.type === "error") { + sonnerToast.error(, { + id: toastId, + duration, + style: { + background: "transparent", + border: "none", + boxShadow: "none", + padding: 0, + }, + }); + } else { + sonnerToast.custom((id) => , { + id: toastId, + duration, + style: { + background: "transparent", + border: "none", + boxShadow: "none", + padding: 0, + }, + }); + } + + return toastId; +} + +// Convenience functions for common use cases +export function showLoadingToast( + title: string, + options?: { + id?: string; + description?: string; + duration?: number; + } +) { + return showToast({ + type: "loading", + title, + ...options, + }); +} + +export function showDownloadToast( + browserName: string, + version: string, + stage: "downloading" | "extracting" | "verifying" | "completed", + progress?: { percentage: number; speed?: string; eta?: string }, + options?: { suppressCompletionToast?: boolean } +) { + const title = + stage === "completed" + ? `${browserName} ${version} downloaded successfully!` + : stage === "downloading" + ? `Downloading ${browserName} ${version}` + : stage === "extracting" + ? `Extracting ${browserName} ${version}` + : `Verifying ${browserName} ${version}`; + + // Don't show completion toast if suppressed (for auto-update scenarios) + if (stage === "completed" && options?.suppressCompletionToast) { + dismissToast(`download-${browserName.toLowerCase()}-${version}`); + return; + } + + return showToast({ + type: "download", + title, + stage, + progress, + id: `download-${browserName.toLowerCase()}-${version}`, + }); +} + +export function showVersionUpdateToast( + title: string, + options?: { + id?: string; + description?: string; + progress?: { + current: number; + total: number; + found: number; + }; + duration?: number; + } +) { + return showToast({ + type: "version-update", + title, + ...options, + }); +} + +export function showFetchingToast( + browserName: string, + options?: { + id?: string; + description?: string; + duration?: number; + } +) { + return showToast({ + type: "fetching", + title: `Checking for new ${browserName} versions...`, + description: + options?.description ?? "Fetching latest release information...", + browserName, + ...options, + }); +} + +export function showSuccessToast( + title: string, + options?: { + id?: string; + description?: string; + duration?: number; + } +) { + return showToast({ + type: "success", + title, + ...options, + }); +} + +export function showErrorToast( + title: string, + options?: { + id?: string; + description?: string; + duration?: number; + } +) { + return showToast({ + type: "error", + title, + ...options, + }); +} + +// Generic helper for dismissing toasts +export function dismissToast(id: string) { + sonnerToast.dismiss(id); +} + +// Dismiss all toasts +export function dismissAllToasts() { + sonnerToast.dismiss(); +} diff --git a/src/components/loading-button.tsx b/src/components/loading-button.tsx new file mode 100644 index 0000000..2cbf71d --- /dev/null +++ b/src/components/loading-button.tsx @@ -0,0 +1,17 @@ +import { LuLoaderCircle } from "react-icons/lu"; +import { type ButtonProps, Button as UIButton } from "./ui/button"; +type Props = ButtonProps & { + isLoading: boolean; + "aria-label"?: string; +}; +export const LoadingButton = ({ isLoading, ...props }: Props) => { + return ( + + {isLoading ? ( + + ) : ( + props.children + )} + + ); +}; diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx new file mode 100644 index 0000000..c742287 --- /dev/null +++ b/src/components/profile-data-table.tsx @@ -0,0 +1,509 @@ +"use client"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import type { BrowserProfile } from "@/types"; +import { + type ColumnDef, + type SortingState, + flexRender, + getCoreRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { LuChevronDown, LuChevronUp } from "react-icons/lu"; +import { IoEllipsisHorizontal } from "react-icons/io5"; +import * as React from "react"; +import { CiCircleCheck } from "react-icons/ci"; +import { useTableSorting } from "@/hooks/use-table-sorting"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; +import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils"; + +interface ProfilesDataTableProps { + data: BrowserProfile[]; + onLaunchProfile: (profile: BrowserProfile) => void | Promise; + onKillProfile: (profile: BrowserProfile) => void | Promise; + onProxySettings: (profile: BrowserProfile) => void; + onDeleteProfile: (profile: BrowserProfile) => void | Promise; + onRenameProfile: (oldName: string, newName: string) => Promise; + onChangeVersion: (profile: BrowserProfile) => void; + runningProfiles: Set; + isUpdating?: (browser: string) => boolean; +} + +export function ProfilesDataTable({ + data, + onLaunchProfile, + onKillProfile, + onProxySettings, + onDeleteProfile, + onRenameProfile, + onChangeVersion, + runningProfiles, + isUpdating = () => false, +}: ProfilesDataTableProps) { + const { getTableSorting, updateSorting, isLoaded } = useTableSorting(); + const [sorting, setSorting] = React.useState([]); + const [profileToRename, setProfileToRename] = + React.useState(null); + const [newProfileName, setNewProfileName] = React.useState(""); + const [renameError, setRenameError] = React.useState(null); + const [isClient, setIsClient] = React.useState(false); + + // Ensure we're on the client side to prevent hydration mismatches + React.useEffect(() => { + setIsClient(true); + }, []); + + // Update local sorting state when settings are loaded + React.useEffect(() => { + if (isLoaded && isClient) { + setSorting(getTableSorting()); + } + }, [isLoaded, getTableSorting, isClient]); + + // Handle sorting changes + const handleSortingChange = React.useCallback( + (updater: React.SetStateAction) => { + if (!isClient) return; + const newSorting = + typeof updater === "function" ? updater(sorting) : updater; + setSorting(newSorting); + updateSorting(newSorting); + }, + [sorting, updateSorting, isClient] + ); + + const handleRename = async () => { + if (!profileToRename || !newProfileName.trim()) return; + + try { + await onRenameProfile(profileToRename.name, newProfileName.trim()); + setProfileToRename(null); + setNewProfileName(""); + setRenameError(null); + } catch (err) { + setRenameError(err as string); + } + }; + + const columns: ColumnDef[] = React.useMemo( + () => [ + { + id: "actions", + cell: ({ row }) => { + const profile = row.original; + const isRunning = isClient && runningProfiles.has(profile.name); + const isBrowserUpdating = isClient && isUpdating(profile.browser); + + // Check if any TOR browser profile is running + const isTorBrowser = profile.browser === "tor-browser"; + const anyTorRunning = + isClient && + data.some( + (p) => p.browser === "tor-browser" && runningProfiles.has(p.name) + ); + const shouldDisableTorStart = + isTorBrowser && !isRunning && anyTorRunning; + + const isDisabled = shouldDisableTorStart || isBrowserUpdating; + + return ( +
+ + + + + + {!isClient + ? "Loading..." + : isRunning + ? "Click to forcefully stop the browser" + : isBrowserUpdating + ? `${profile.browser} is being updated. Please wait for the update to complete.` + : shouldDisableTorStart + ? "Only one TOR browser instance can run at a time. Stop the running TOR browser first." + : "Click to launch the browser"} + + +
+ ); + }, + }, + { + accessorKey: "name", + header: ({ column }) => { + const isSorted = column.getIsSorted(); + return ( + + ); + }, + enableSorting: true, + sortingFn: "alphanumeric", + }, + { + accessorKey: "browser", + header: ({ column }) => { + const isSorted = column.getIsSorted(); + return ( + + ); + }, + cell: ({ row }) => { + const browser: string = row.getValue("browser"); + const IconComponent = getBrowserIcon(browser); + return ( +
+ {IconComponent && } + {getBrowserDisplayName(browser)} +
+ ); + }, + enableSorting: true, + sortingFn: (rowA, rowB, columnId) => { + const browserA = getBrowserDisplayName(rowA.getValue(columnId)); + const browserB = getBrowserDisplayName(rowB.getValue(columnId)); + return browserA.localeCompare(browserB); + }, + }, + { + accessorKey: "version", + header: "Version", + }, + { + id: "status", + header: ({ column }) => { + const isSorted = column.getIsSorted(); + return ( + + ); + }, + cell: ({ row }) => { + const profile = row.original; + const isRunning = isClient && runningProfiles.has(profile.name); + return ( +
+ + {isClient ? (isRunning ? "Running" : "Stopped") : "Loading..."} + + {isClient && isRunning && profile.process_id && ( + + PID: {profile.process_id} + + )} +
+ ); + }, + enableSorting: true, + sortingFn: (rowA, rowB) => { + // If not on client, sort by name only to ensure consistency + if (!isClient) { + return rowA.original.name.localeCompare(rowB.original.name); + } + + const isRunningA = runningProfiles.has(rowA.original.name); + const isRunningB = runningProfiles.has(rowB.original.name); + + // Running profiles come first, then stopped ones + // Secondary sort by profile name + if (isRunningA === isRunningB) { + return rowA.original.name.localeCompare(rowB.original.name); + } + return isRunningA ? -1 : 1; + }, + }, + { + id: "proxy", + header: "Proxy", + cell: ({ row }) => { + const profile = row.original; + const hasProxy = profile.proxy?.enabled; + return ( + + +
+ {hasProxy && ( + + )} + + {hasProxy ? profile.proxy?.proxy_type : "Disabled"} + +
+
+ + {hasProxy + ? `${profile.proxy?.proxy_type.toUpperCase()} proxy enabled (${ + profile.proxy?.host + }:${profile.proxy?.port})` + : "No proxy configured"} + +
+ ); + }, + }, + // Update the settings column to use the confirmation dialog + { + id: "settings", + cell: ({ row }) => { + const profile = row.original; + const isRunning = isClient && runningProfiles.has(profile.name); + const isBrowserUpdating = isClient && isUpdating(profile.browser); + return ( +
+ + + + + + Actions + + { + onProxySettings(profile); + }} + disabled={!isClient || isBrowserUpdating} + > + Configure proxy + + { + onChangeVersion(profile); + }} + disabled={!isClient || isRunning || isBrowserUpdating} + > + Change version + + { + setProfileToRename(profile); + setNewProfileName(profile.name); + }} + disabled={!isClient || isRunning || isBrowserUpdating} + > + Rename profile + + void onDeleteProfile(profile)} + className="text-red-600" + disabled={!isClient || isRunning || isBrowserUpdating} + > + Delete profile + + + +
+ ); + }, + }, + ], + [isClient, runningProfiles, isUpdating, data] + ); + + const table = useReactTable({ + data, + columns, + state: { + sorting, + }, + onSortingChange: handleSortingChange, + getSortedRowModel: getSortedRowModel(), + getCoreRowModel: getCoreRowModel(), + }); + + return ( + <> +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No profiles found. + + + )} + +
+
+ + { + if (!open) { + setProfileToRename(null); + setNewProfileName(""); + setRenameError(null); + } + }} + > + + + Rename Profile + +
+
+ + { + setNewProfileName(e.target.value); + }} + className="col-span-3" + /> +
+ {renameError && ( +

{renameError}

+ )} +
+ + + + +
+
+ + ); +} diff --git a/src/components/profile-selector-dialog.tsx b/src/components/profile-selector-dialog.tsx new file mode 100644 index 0000000..e14782a --- /dev/null +++ b/src/components/profile-selector-dialog.tsx @@ -0,0 +1,366 @@ +"use client"; + +import { LoadingButton } from "@/components/loading-button"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import type { BrowserProfile } from "@/types"; +import { invoke } from "@tauri-apps/api/core"; +import { LuCopy } from "react-icons/lu"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils"; + +interface ProfileSelectorDialogProps { + isOpen: boolean; + onClose: () => void; + url?: string; + runningProfiles?: Set; +} + +export function ProfileSelectorDialog({ + isOpen, + onClose, + url, + runningProfiles = new Set(), +}: ProfileSelectorDialogProps) { + const [profiles, setProfiles] = useState([]); + const [selectedProfile, setSelectedProfile] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isLaunching, setIsLaunching] = useState(false); + + useEffect(() => { + if (isOpen) { + void loadProfiles(); + } + }, [isOpen]); + + const loadProfiles = async () => { + setIsLoading(true); + try { + const profileList = await invoke( + "list_browser_profiles" + ); + + // Sort profiles by name + profileList.sort((a, b) => a.name.localeCompare(b.name)); + + // Don't filter any profiles, show all of them + setProfiles(profileList); + + // Auto-select first available profile for link opening + if (profileList.length > 0) { + // Find the first profile that can be used for opening links + const availableProfile = profileList.find((profile) => { + return canUseProfileForLinks(profile, profileList, runningProfiles); + }); + + if (availableProfile) { + setSelectedProfile(availableProfile.name); + } else { + // If no suitable profile found, still select the first one to show UI + setSelectedProfile(profileList[0].name); + } + } + } catch (error) { + console.error("Failed to load profiles:", error); + } finally { + setIsLoading(false); + } + }; + + // Helper function to determine if a profile can be used for opening links + const canUseProfileForLinks = ( + profile: BrowserProfile, + allProfiles: BrowserProfile[], + runningProfiles: Set + ): boolean => { + const isRunning = runningProfiles.has(profile.name); + + // For TOR browser: Check if any TOR browser is running + if (profile.browser === "tor-browser") { + const runningTorProfiles = allProfiles.filter( + (p) => p.browser === "tor-browser" && runningProfiles.has(p.name) + ); + + // If no TOR browser is running, allow any TOR profile + if (runningTorProfiles.length === 0) { + return true; + } + + // If TOR browser(s) are running, only allow the running one(s) + return isRunning; + } + + // For Mullvad browser: never allow if running + if (profile.browser === "mullvad-browser" && isRunning) { + return false; + } + + // For other browsers: always allow + return true; + }; + + // Helper function to get tooltip content for profiles + const getProfileTooltipContent = (profile: BrowserProfile): string => { + const isRunning = runningProfiles.has(profile.name); + + if (profile.browser === "tor-browser") { + const runningTorProfiles = profiles.filter( + (p) => p.browser === "tor-browser" && runningProfiles.has(p.name) + ); + + // If another TOR profile is running, this one is not available + return "Only 1 instance can run at a time"; + } + + if (profile.browser === "mullvad-browser") { + if (isRunning) { + return "Only launching the browser is supported, opening them in a running browser is not yet available"; + } + return "Only launching the browser is supported, opening them in a running browser is not yet available"; + } + + if (isRunning) { + return "URL will open in a new tab in the existing browser window"; + } + + return ""; + }; + + const handleOpenUrl = async () => { + if (!selectedProfile || !url) return; + + setIsLaunching(true); + try { + await invoke("open_url_with_profile", { + profileName: selectedProfile, + url, + }); + onClose(); + } catch (error) { + console.error("Failed to open URL with profile:", error); + } finally { + setIsLaunching(false); + } + }; + + const handleCancel = () => { + setSelectedProfile(null); + onClose(); + }; + + const handleCopyUrl = async () => { + if (!url) return; + + try { + await navigator.clipboard.writeText(url); + toast.success("URL copied to clipboard!"); + } catch (error) { + console.error("Failed to copy URL:", error); + toast.error("Failed to copy URL to clipboard"); + } + }; + + const selectedProfileData = profiles.find((p) => p.name === selectedProfile); + const isSelectedProfileRunning = selectedProfile + ? runningProfiles.has(selectedProfile) + : false; + + // Check if the selected profile can be used for opening links + const canOpenWithSelectedProfile = () => { + if (!selectedProfileData) return false; + return canUseProfileForLinks( + selectedProfileData, + profiles, + runningProfiles + ); + }; + + // Get tooltip content for disabled profiles + const getTooltipContent = () => { + if (!selectedProfileData) return ""; + return getProfileTooltipContent(selectedProfileData); + }; + + return ( + + + + Choose Profile + + +
+ {url && ( +
+
+ + +
+
+ {url} +
+
+ )} + +
+ + {isLoading ? ( +
+ Loading profiles... +
+ ) : profiles.length === 0 ? ( +
+
+ No profiles available. Please create a profile first. +
+
+ Close this dialog and create a profile from the main window to + get started. +
+
+ ) : ( + <> + + + )} +
+
+ + + + + +
+ void handleOpenUrl()} + disabled={ + !selectedProfile || + profiles.length === 0 || + !canOpenWithSelectedProfile() + } + > + Open + +
+
+ {getTooltipContent() && ( + {getTooltipContent()} + )} +
+
+
+
+ ); +} diff --git a/src/components/proxy-settings-dialog.tsx b/src/components/proxy-settings-dialog.tsx new file mode 100644 index 0000000..0aa542e --- /dev/null +++ b/src/components/proxy-settings-dialog.tsx @@ -0,0 +1,240 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { useEffect, useState } from "react"; + +interface ProxySettings { + enabled: boolean; + proxy_type: string; + host: string; + port: number; +} + +interface ProxySettingsDialogProps { + isOpen: boolean; + onClose: () => void; + onSave: (proxySettings: ProxySettings) => void; + initialSettings?: ProxySettings; + browserType?: string; +} + +export function ProxySettingsDialog({ + isOpen, + onClose, + onSave, + initialSettings, + browserType, +}: ProxySettingsDialogProps) { + const [settings, setSettings] = useState({ + enabled: initialSettings?.enabled ?? false, + proxy_type: initialSettings?.proxy_type ?? "http", + host: initialSettings?.host ?? "", + port: initialSettings?.port ?? 8080, + }); + + const [initialSettingsState, setInitialSettingsState] = + useState({ + enabled: false, + proxy_type: "http", + host: "", + port: 8080, + }); + + useEffect(() => { + if (isOpen && initialSettings) { + const newSettings = { + enabled: initialSettings.enabled, + proxy_type: initialSettings.proxy_type, + host: initialSettings.host, + port: initialSettings.port, + }; + setSettings(newSettings); + setInitialSettingsState(newSettings); + } else if (isOpen) { + const defaultSettings = { + enabled: false, + proxy_type: "http", + host: "", + port: 80, + }; + setSettings(defaultSettings); + setInitialSettingsState(defaultSettings); + } + }, [isOpen, initialSettings]); + + const handleSubmit = () => { + onSave(settings); + }; + + // Check if settings have changed + const hasChanged = () => { + return ( + settings.enabled !== initialSettingsState.enabled || + settings.proxy_type !== initialSettingsState.proxy_type || + settings.host !== initialSettingsState.host || + settings.port !== initialSettingsState.port + ); + }; + + // Helper to determine if proxy should be disabled for the selected browser + const isProxyDisabled = browserType === "tor-browser"; + + // Update proxy enabled state when browser is tor-browser + useEffect(() => { + if (browserType === "tor-browser" && settings.enabled) { + setSettings((prev) => ({ ...prev, enabled: false })); + } + }, [browserType, settings.enabled]); + + return ( + { + if (!open) { + onClose(); + } + }} + > + + + Proxy Settings + + +
+
+ {isProxyDisabled ? ( + + +
+ + +
+
+ +

+ Tor Browser has its own built-in proxy system and + doesn't support additional proxy configuration +

+
+
+ ) : ( + <> + { + setSettings({ ...settings, enabled: checked as boolean }); + }} + /> + + + )} +
+ + {settings.enabled && !isProxyDisabled && ( + <> +
+ + +
+ +
+ + { + setSettings({ ...settings, host: e.target.value }); + }} + placeholder="e.g. 127.0.0.1" + /> +
+ +
+ + { + setSettings({ + ...settings, + port: Number.parseInt(e.target.value, 10) || 0, + }); + }} + placeholder="e.g. 8080" + min="1" + max="65535" + /> +
+ + )} +
+ + + + + +
+
+ ); +} diff --git a/src/components/settings-dialog.tsx b/src/components/settings-dialog.tsx new file mode 100644 index 0000000..f6ad183 --- /dev/null +++ b/src/components/settings-dialog.tsx @@ -0,0 +1,266 @@ +"use client"; + +import { LoadingButton } from "@/components/loading-button"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { invoke } from "@tauri-apps/api/core"; +import { useTheme } from "next-themes"; +import { useEffect, useState } from "react"; + +interface AppSettings { + set_as_default_browser: boolean; + show_settings_on_startup: boolean; + theme: string; + auto_updates_enabled: boolean; +} + +interface SettingsDialogProps { + isOpen: boolean; + onClose: () => void; +} + +export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { + const [settings, setSettings] = useState({ + set_as_default_browser: false, + show_settings_on_startup: true, + theme: "system", + auto_updates_enabled: true, + }); + const [originalSettings, setOriginalSettings] = useState({ + set_as_default_browser: false, + show_settings_on_startup: true, + theme: "system", + auto_updates_enabled: true, + }); + const [isDefaultBrowser, setIsDefaultBrowser] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [isSettingDefault, setIsSettingDefault] = useState(false); + + const { setTheme } = useTheme(); + + useEffect(() => { + if (isOpen) { + void loadSettings(); + void checkDefaultBrowserStatus(); + + // Set up interval to check default browser status + const intervalId = setInterval(() => { + void checkDefaultBrowserStatus(); + }, 500); // Check every 2 seconds + + // Cleanup interval on component unmount or dialog close + return () => { + clearInterval(intervalId); + }; + } + }, [isOpen]); + + const loadSettings = async () => { + setIsLoading(true); + try { + const appSettings = await invoke("get_app_settings"); + setSettings(appSettings); + setOriginalSettings(appSettings); + } catch (error) { + console.error("Failed to load settings:", error); + } finally { + setIsLoading(false); + } + }; + + const checkDefaultBrowserStatus = async () => { + try { + const isDefault = await invoke("is_default_browser"); + setIsDefaultBrowser(isDefault); + } catch (error) { + console.error("Failed to check default browser status:", error); + } + }; + + const handleSetDefaultBrowser = async () => { + setIsSettingDefault(true); + try { + await invoke("set_as_default_browser"); + await checkDefaultBrowserStatus(); + } catch (error) { + console.error("Failed to set as default browser:", error); + } finally { + setIsSettingDefault(false); + } + }; + + const handleSave = async () => { + setIsSaving(true); + try { + await invoke("save_app_settings", { settings }); + // Apply theme change immediately + setTheme(settings.theme); + setOriginalSettings(settings); + onClose(); + } catch (error) { + console.error("Failed to save settings:", error); + } finally { + setIsSaving(false); + } + }; + + const updateSetting = (key: keyof AppSettings, value: boolean | string) => { + setSettings((prev) => ({ ...prev, [key]: value })); + }; + + // Check if settings have changed (excluding default browser setting) + const hasChanges = + settings.show_settings_on_startup !== + originalSettings.show_settings_on_startup || + settings.theme !== originalSettings.theme || + settings.auto_updates_enabled !== originalSettings.auto_updates_enabled; + + return ( + + + + Settings + + +
+ {/* Appearance Section */} +
+ + +
+ + +
+ +

+ Choose your preferred theme or follow your system settings. +

+
+ + {/* Default Browser Section */} +
+
+ + + {isDefaultBrowser ? "Active" : "Inactive"} + +
+ + { + void handleSetDefaultBrowser(); + }} + disabled={isDefaultBrowser} + variant={isDefaultBrowser ? "outline" : "default"} + className="w-full" + > + {isDefaultBrowser + ? "Already Default Browser" + : "Set as Default Browser"} + + +

+ When set as default, Donut Browser will handle web links and allow + you to choose which profile to use. +

+
+ + {/* Auto-Update Section */} +
+ + +
+ { + updateSetting("auto_updates_enabled", checked as boolean); + }} + /> + +
+ +

+ When enabled, Donut Browser will check for browser updates and + notify you when updates are available for your profiles. +

+
+ + {/* Startup Behavior Section */} +
+ + +
+ { + updateSetting("show_settings_on_startup", checked as boolean); + }} + /> + +
+ +

+ When enabled, the settings dialog will be shown when the app + starts. +

+
+
+ + + + { + void handleSave(); + }} + disabled={isLoading || !hasChanges} + > + Save Settings + + +
+
+ ); +} diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx new file mode 100644 index 0000000..59821e5 --- /dev/null +++ b/src/components/theme-provider.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { invoke } from "@tauri-apps/api/core"; +import { ThemeProvider } from "next-themes"; +import { useEffect, useState } from "react"; + +interface AppSettings { + show_settings_on_startup: boolean; + theme: string; +} + +interface CustomThemeProviderProps { + children: React.ReactNode; +} + +// Helper function to detect system dark mode preference +function getSystemTheme(): string { + if (typeof window !== "undefined") { + const isDarkMode = window.matchMedia( + "(prefers-color-scheme: dark)" + ).matches; + return isDarkMode ? "dark" : "light"; + } + return "light"; +} + +export function CustomThemeProvider({ children }: CustomThemeProviderProps) { + const [isLoading, setIsLoading] = useState(true); + const [defaultTheme, setDefaultTheme] = useState("system"); + + useEffect(() => { + const loadTheme = async () => { + try { + const settings = await invoke("get_app_settings"); + setDefaultTheme(settings.theme); + } catch (error) { + console.error("Failed to load theme settings:", error); + // For first-time users, detect system preference and apply it + const systemTheme = getSystemTheme(); + console.log( + "First-time user detected, applying system theme:", + systemTheme + ); + + // Save the detected theme as the default + try { + await invoke("save_app_settings", { + settings: { + show_settings_on_startup: true, + theme: "system", + auto_updates_enabled: true, + }, + }); + } catch (saveError) { + console.error("Failed to save initial theme settings:", saveError); + } + + setDefaultTheme("system"); + } finally { + setIsLoading(false); + } + }; + + void loadTheme(); + }, []); + + if (isLoading) { + // Detect system theme to show appropriate loading screen + const systemTheme = getSystemTheme(); + const loadingBgColor = systemTheme === "dark" ? "bg-gray-900" : "bg-white"; + const spinnerColor = + systemTheme === "dark" ? "border-white" : "border-gray-900"; + + return ( +
+
+
+ ); + } + + return ( + + {children} + + ); +} diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..d4700bd --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import { type VariantProps, cva } from "class-variance-authority"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { Alert, AlertTitle, AlertDescription }; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..dac19eb --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import { Slot } from "@radix-ui/react-slot"; +import { type VariantProps, cva } from "class-variance-authority"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span"; + + return ( + + ); +} + +export { Badge, badgeVariants }; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..1d0f7c5 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,61 @@ +import { Slot } from "@radix-ui/react-slot"; +import { type VariantProps, cva } from "class-variance-authority"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export type ButtonProps = React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + }; + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: ButtonProps) { + const Comp = asChild ? Slot : "button"; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..13631ed --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +}; diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..0a0105a --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client"; + +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { LuCheck } from "react-icons/lu"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ); +} + +export { Checkbox }; diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx new file mode 100644 index 0000000..5ea9004 --- /dev/null +++ b/src/components/ui/combobox.tsx @@ -0,0 +1,94 @@ +"use client"; + +import * as React from "react"; +import { LuCheck, LuChevronsUpDown } from "react-icons/lu"; + +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; + +const frameworks = [ + { + value: "next.js", + label: "Next.js", + }, + { + value: "sveltekit", + label: "SvelteKit", + }, + { + value: "nuxt.js", + label: "Nuxt.js", + }, + { + value: "remix", + label: "Remix", + }, + { + value: "astro", + label: "Astro", + }, +]; + +export function ComboboxDemo() { + const [open, setOpen] = React.useState(false); + const [value, setValue] = React.useState(""); + + return ( + + + + + + + + + No framework found. + + {frameworks.map((framework) => ( + { + setValue(currentValue === value ? "" : currentValue); + setOpen(false); + }} + > + + {framework.label} + + ))} + + + + + + ); +} diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..b4f9c26 --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { Command as CommandPrimitive } from "cmdk"; +import type * as React from "react"; +import { LuSearch } from "react-icons/lu"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { cn } from "@/lib/utils"; + +function Command({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandDialog({ + title = "Command Palette", + description = "Search for a command to run...", + children, + ...props +}: React.ComponentProps & { + title?: string; + description?: string; +}) { + return ( + + + {title} + {description} + + + + {children} + + + + ); +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ + +
+ ); +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandEmpty({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ); +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..ee2d460 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,135 @@ +"use client"; + +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import type * as React from "react"; +import { RxCross2 } from "react-icons/rx"; + +import { cn } from "@/lib/utils"; + +function Dialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ); +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..b446c7e --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +"use client"; + +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { LuCheck, LuChevronRight, LuCircle } from "react-icons/lu"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: "default" | "destructive"; +}) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ); +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..cb0e803 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ); +} + +export { Input }; diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..a3661df --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +"use client"; + +import * as LabelPrimitive from "@radix-ui/react-label"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Label }; diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..d68b87a --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,48 @@ +"use client"; + +import * as PopoverPrimitive from "@radix-ui/react-popover"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Popover({ + ...props +}: React.ComponentProps) { + return ; +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return ; +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx new file mode 100644 index 0000000..d00d550 --- /dev/null +++ b/src/components/ui/progress.tsx @@ -0,0 +1,31 @@ +"use client"; + +import * as ProgressPrimitive from "@radix-ui/react-progress"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Progress({ + className, + value, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { Progress }; diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..554e349 --- /dev/null +++ b/src/components/ui/scroll-area.tsx @@ -0,0 +1,58 @@ +"use client"; + +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ); +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { ScrollArea, ScrollBar }; diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..5601a89 --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,185 @@ +"use client"; + +import * as SelectPrimitive from "@radix-ui/react-select"; +import { LuCheck, LuChevronDown, LuChevronUp } from "react-icons/lu"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Select({ + ...props +}: React.ComponentProps) { + return ; +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return ; +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return ; +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default"; +}) { + return ( + + {children} + + + + + ); +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ); +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}; diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..f96a98d --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { useTheme } from "next-themes"; +import { Toaster as Sonner, type ToasterProps } from "sonner"; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx new file mode 100644 index 0000000..430838f --- /dev/null +++ b/src/components/ui/table.tsx @@ -0,0 +1,116 @@ +"use client"; + +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Table({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ); +} + +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ); +} + +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + + ); +} + +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", + className, + )} + {...props} + /> + ); +} + +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ); +} + +function TableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( +
[role=checkbox]]:translate-y-[2px]", + className, + )} + {...props} + /> + ); +} + +function TableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + [role=checkbox]]:translate-y-[2px]", + className, + )} + {...props} + /> + ); +} + +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { + return ( +
+ ); +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..35a9ba6 --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -0,0 +1,61 @@ +"use client"; + +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ); +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/src/components/update-notification.tsx b/src/components/update-notification.tsx new file mode 100644 index 0000000..102c391 --- /dev/null +++ b/src/components/update-notification.tsx @@ -0,0 +1,305 @@ +"use client"; + +/* eslint-disable @typescript-eslint/no-misused-promises */ + +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { invoke } from "@tauri-apps/api/core"; +import { toast } from "sonner"; +import { useCallback, useEffect, useState } from "react"; +import { FaDownload, FaTimes } from "react-icons/fa"; +import { showToast } from "@/components/custom-toast"; +import { getBrowserDisplayName } from "@/lib/browser-utils"; + +interface UpdateNotification { + id: string; + browser: string; + current_version: string; + new_version: string; + affected_profiles: string[]; + is_stable_update: boolean; + timestamp: number; +} + +interface UpdateNotificationProps { + notification: UpdateNotification; + onUpdate: (browser: string, newVersion: string) => Promise; + onDismiss: (notificationId: string) => Promise; + isUpdating?: boolean; +} + +function UpdateNotificationComponent({ + notification, + onUpdate, + onDismiss, + isUpdating = false, +}: UpdateNotificationProps) { + const browserDisplayName = getBrowserDisplayName(notification.browser); + + const profileText = + notification.affected_profiles.length === 1 + ? `profile "${notification.affected_profiles[0]}"` + : `${notification.affected_profiles.length} profiles`; + + const handleUpdateClick = async () => { + // Dismiss the notification immediately to close the modal + await onDismiss(notification.id); + // Then start the update process + await onUpdate(notification.browser, notification.new_version); + }; + + return ( +
+
+
+
+ + {browserDisplayName} Update Available + + + {notification.is_stable_update ? "Stable" : "Beta"} + +
+
+ Update {profileText} from {notification.current_version} to{" "} + {notification.new_version} +
+
+ +
+ +
+ + +
+ + {notification.affected_profiles.length > 1 && ( +
+ Affected profiles: {notification.affected_profiles.join(", ")} +
+ )} +
+ ); +} + +export function useUpdateNotifications() { + const [notifications, setNotifications] = useState([]); + const [updatingBrowsers, setUpdatingBrowsers] = useState>( + new Set() + ); + const [isClient, setIsClient] = useState(false); + + // Ensure we're on the client side to prevent hydration mismatches + useEffect(() => { + setIsClient(true); + }, []); + + const checkForUpdates = useCallback(async () => { + if (!isClient) return; // Only run on client side + + try { + const updates = await invoke( + "check_for_browser_updates" + ); + setNotifications(updates); + + // Show toasts for new notifications - we'll define handleUpdate and handleDismiss separately + // to avoid circular dependencies + } catch (error) { + console.error("Failed to check for updates:", error); + } + }, [isClient]); + + const handleUpdate = useCallback( + async (browser: string, newVersion: string) => { + try { + setUpdatingBrowsers((prev) => new Set(prev).add(browser)); + const browserDisplayName = getBrowserDisplayName(browser); + + // Dismiss all notifications for this browser first + const browserNotifications = notifications.filter( + (n) => n.browser === browser + ); + for (const notification of browserNotifications) { + toast.dismiss(notification.id); + await invoke("dismiss_update_notification", { + notificationId: notification.id, + }); + } + + try { + // Check if browser already exists before downloading + const isDownloaded = await invoke("check_browser_exists", { + browserStr: browser, + version: newVersion, + }); + + if (isDownloaded) { + // Browser already exists, skip download and go straight to profile update + console.log( + `${browserDisplayName} ${newVersion} already exists, skipping download` + ); + } else { + // Mark download as auto-update in the backend for toast suppression + await invoke("mark_auto_update_download", { + browser, + version: newVersion, + }); + + // Download the browser (progress will be handled by use-browser-download hook) + await invoke("download_browser", { + browserStr: browser, + version: newVersion, + }); + } + + // Complete the update with auto-update of profile versions + const updatedProfiles = await invoke( + "complete_browser_update_with_auto_update", + { + browser, + newVersion, + } + ); + + // Show success message based on whether profiles were updated + if (updatedProfiles.length > 0) { + const profileText = + updatedProfiles.length === 1 + ? `Profile "${updatedProfiles[0]}" has been updated` + : `${updatedProfiles.length} profiles have been updated`; + + showToast({ + type: "success", + title: `${browserDisplayName} update completed`, + description: `${profileText} to version ${newVersion}. Running profiles were not updated and can be updated manually.`, + duration: 5000, + }); + } else { + showToast({ + type: "success", + title: `${browserDisplayName} update ready`, + description: + "All affected profiles are currently running. Stop them and manually update their versions to use the new version.", + duration: 5000, + }); + } + } catch (downloadError) { + console.error("Failed to download browser:", downloadError); + + // Clean up auto-update tracking on error + try { + await invoke("remove_auto_update_download", { + browser, + version: newVersion, + }); + } catch (e) { + console.error("Failed to clean up auto-update tracking:", e); + } + + showToast({ + type: "error", + title: `Failed to download ${browserDisplayName} ${newVersion}`, + description: String(downloadError), + duration: 6000, + }); + throw downloadError; + } + + // Refresh notifications to clear any remaining ones + await checkForUpdates(); + } catch (error) { + console.error("Failed to start update:", error); + const browserDisplayName = getBrowserDisplayName(browser); + showToast({ + type: "error", + title: `Failed to update ${browserDisplayName}`, + description: String(error), + duration: 6000, + }); + } finally { + setUpdatingBrowsers((prev) => { + const next = new Set(prev); + next.delete(browser); + return next; + }); + } + }, + [notifications, checkForUpdates] + ); + + const handleDismiss = useCallback( + async (notificationId: string) => { + if (!isClient) return; // Only run on client side + + try { + toast.dismiss(notificationId); + await invoke("dismiss_update_notification", { notificationId }); + await checkForUpdates(); + } catch (error) { + console.error("Failed to dismiss notification:", error); + } + }, + [checkForUpdates, isClient] + ); + + // Separate effect to show toasts when notifications change + useEffect(() => { + if (!isClient) return; + + notifications.forEach((notification) => { + const isUpdating = updatingBrowsers.has(notification.browser); + + toast.custom( + () => ( + + ), + { + id: notification.id, + duration: Infinity, // Persistent until user action + position: "top-right", + // Remove transparent styling to fix background issue + style: undefined, + } + ); + }); + }, [notifications, updatingBrowsers, handleUpdate, handleDismiss, isClient]); + + return { + notifications, + checkForUpdates, + isUpdating: (browser: string) => updatingBrowsers.has(browser), + }; +} diff --git a/src/components/version-selector.tsx b/src/components/version-selector.tsx new file mode 100644 index 0000000..f3ac915 --- /dev/null +++ b/src/components/version-selector.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { LoadingButton } from "@/components/loading-button"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; +import { LuDownload } from "react-icons/lu"; +import { useState } from "react"; +import { LuCheck, LuChevronsUpDown } from "react-icons/lu"; +import { ScrollArea } from "./ui/scroll-area"; + +interface GithubRelease { + tag_name: string; + assets: Array<{ + name: string; + browser_download_url: string; + hash?: string; + }>; + published_at: string; + is_alpha: boolean; +} + +interface VersionSelectorProps { + selectedVersion: string | null; + onVersionSelect: (version: string | null) => void; + availableVersions: GithubRelease[]; + downloadedVersions: string[]; + isDownloading: boolean; + onDownload: () => void; + placeholder?: string; + showDownloadButton?: boolean; +} + +export function VersionSelector({ + selectedVersion, + onVersionSelect, + availableVersions, + downloadedVersions, + isDownloading, + onDownload, + placeholder = "Select version...", + showDownloadButton = true, +}: VersionSelectorProps) { + const [versionPopoverOpen, setVersionPopoverOpen] = useState(false); + + const isVersionDownloaded = selectedVersion + ? downloadedVersions.includes(selectedVersion) + : false; + + return ( +
+ + + + + + + + No versions found. + + [data-radix-scroll-area-viewport]]:max-h-[200px]" + } + > + + {availableVersions.map((version) => { + const isDownloaded = downloadedVersions.includes( + version.tag_name + ); + return ( + { + onVersionSelect( + currentValue === selectedVersion + ? null + : currentValue + ); + setVersionPopoverOpen(false); + }} + > + +
+ {version.tag_name} + {version.is_alpha && ( + + Alpha + + )} + {isDownloaded && ( + + Downloaded + + )} +
+
+ ); + })} +
+
+
+
+
+
+ + {/* Download Button */} + {showDownloadButton && selectedVersion && !isVersionDownloaded && ( + { + onDownload(); + }} + variant="outline" + className="w-full" + > + + {isDownloading ? "Downloading..." : "Download Browser"} + + )} +
+ ); +} diff --git a/src/components/version-update-settings.tsx b/src/components/version-update-settings.tsx new file mode 100644 index 0000000..08d47c7 --- /dev/null +++ b/src/components/version-update-settings.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { LoadingButton } from "@/components/loading-button"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { useVersionUpdater } from "@/hooks/use-version-updater"; +import { + LuRefreshCw, + LuClock, + LuCheckCheck, + LuCircleAlert, +} from "react-icons/lu"; + +export function VersionUpdateSettings() { + const { + isUpdating, + lastUpdateTime, + timeUntilNextUpdate, + updateProgress, + triggerManualUpdate, + formatTimeUntilUpdate, + formatLastUpdateTime, + } = useVersionUpdater(); + + return ( + + + + + Background Version Updates + + + Browser versions are automatically checked every 3 hours in the + background. New versions are cached and ready when you need them. + + + + {/* Current Status */} +
+
+
+ + Last Update +
+
+ {formatLastUpdateTime(lastUpdateTime)} +
+
+ +
+
+ + Next Update +
+
+ {timeUntilNextUpdate <= 0 + ? "Now" + : `In ${formatTimeUntilUpdate(timeUntilNextUpdate)}`} +
+
+
+ + {/* Progress indicator */} + {isUpdating && updateProgress && ( + + + Updating Browser Versions + + {updateProgress.current_browser ? ( + <> + Checking {updateProgress.current_browser} ( + {updateProgress.completed_browsers}/ + {updateProgress.total_browsers}) +
+ {updateProgress.new_versions_found} new versions found so far + + ) : ( + "Starting version update..." + )} +
+
+ )} + + {/* Manual update button */} +
+
+
Manual Update
+
+ Check for new browser versions now +
+
+ { + void triggerManualUpdate(); + }} + variant="outline" + size="sm" + disabled={isUpdating} + > + + {isUpdating ? "Updating..." : "Check Now"} + +
+ + {/* Information about background updates */} + + + How it works + + • Version information is checked automatically every 3 hours +
+ • New versions are added to the cache without removing old ones +
+ • When creating profiles or changing versions, you'll see how + many new versions were found +
• This keeps the app responsive while ensuring you have the + latest information +
+
+
+
+ ); +} diff --git a/src/hooks/use-browser-download.ts b/src/hooks/use-browser-download.ts new file mode 100644 index 0000000..8d9f2c5 --- /dev/null +++ b/src/hooks/use-browser-download.ts @@ -0,0 +1,372 @@ +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import { useCallback, useEffect, useState } from "react"; +import { toast } from "sonner"; +import { + showDownloadToast, + showFetchingToast, + showSuccessToast, + showErrorToast, + dismissToast, +} from "../components/custom-toast"; +import { getBrowserDisplayName } from "@/lib/browser-utils"; + +interface GithubRelease { + tag_name: string; + assets: Array<{ + name: string; + browser_download_url: string; + hash?: string; + }>; + published_at: string; + is_alpha: boolean; +} + +interface BrowserVersionInfo { + version: string; + is_prerelease: boolean; + date: string; +} + +interface DownloadProgress { + browser: string; + version: string; + downloaded_bytes: number; + total_bytes?: number; + percentage: number; + speed_bytes_per_sec: number; + eta_seconds?: number; + stage: string; +} + +interface BrowserVersionsResult { + versions: string[]; + new_versions_count?: number; + total_versions_count: number; +} + +interface VersionUpdateProgress { + current_browser: string; + total_browsers: number; + completed_browsers: number; + new_versions_found: number; + browser_new_versions: number; + status: string; +} + +const isAlphaVersion = (version: string): boolean => { + // Check for common alpha/beta/dev indicators + const lowerVersion = version.toLowerCase(); + return ( + lowerVersion.includes("a") || + lowerVersion.includes("b") || + lowerVersion.includes("alpha") || + lowerVersion.includes("beta") || + lowerVersion.includes("dev") || + lowerVersion.includes("rc") || + lowerVersion.includes("pre") || + // Check for patterns like "139.0b1" or "140.0a1" + /\d+\.\d+[ab]\d+/.test(lowerVersion) + ); +}; + +export function useBrowserDownload() { + const [availableVersions, setAvailableVersions] = useState( + [] + ); + const [downloadedVersions, setDownloadedVersions] = useState([]); + const [isDownloading, setIsDownloading] = useState(false); + const [downloadProgress, setDownloadProgress] = + useState(null); + const [isUpdatingVersions, setIsUpdatingVersions] = useState(false); + + // Listen for download progress events + useEffect(() => { + const unlisten = listen("download-progress", (event) => { + const progress = event.payload; + setDownloadProgress(progress); + + const browserName = getBrowserDisplayName(progress.browser); + + // Check if this is an auto-update download to suppress completion toast + const checkAutoUpdate = async () => { + let isAutoUpdate = false; + try { + isAutoUpdate = await invoke("is_auto_update_download", { + browser: progress.browser, + version: progress.version, + }); + } catch (error) { + console.error("Failed to check auto-update status:", error); + } + + // Show toast with progress + if (progress.stage === "downloading") { + const speedMBps = ( + progress.speed_bytes_per_sec / + (1024 * 1024) + ).toFixed(1); + const etaText = progress.eta_seconds + ? formatTime(progress.eta_seconds) + : "calculating..."; + + showDownloadToast(browserName, progress.version, "downloading", { + percentage: progress.percentage, + speed: speedMBps, + eta: etaText, + }); + } else if (progress.stage === "extracting") { + showDownloadToast(browserName, progress.version, "extracting"); + } else if (progress.stage === "verifying") { + showDownloadToast(browserName, progress.version, "verifying"); + } else if (progress.stage === "completed") { + // Suppress completion toast for auto-updates + showDownloadToast( + browserName, + progress.version, + "completed", + undefined, + { + suppressCompletionToast: isAutoUpdate, + } + ); + setDownloadProgress(null); + } + }; + + void checkAutoUpdate(); + }); + + return () => { + void unlisten.then((fn) => { + fn(); + }); + }; + }, []); + + // Listen for version update progress events + useEffect(() => { + const unlisten = listen( + "version-update-progress", + (event) => { + const progress = event.payload; + + if (progress.status === "updating") { + setIsUpdatingVersions(true); + if (progress.current_browser) { + const browserName = getBrowserDisplayName(progress.current_browser); + showFetchingToast(browserName, { + id: `version-update-${progress.current_browser}`, + description: "Fetching latest release information...", + }); + } + } else if (progress.status === "completed") { + setIsUpdatingVersions(false); + if (progress.new_versions_found > 0) { + showSuccessToast( + `Found ${progress.new_versions_found} new browser versions!`, + { + duration: 3000, + } + ); + } + // Dismiss any update toasts + toast.dismiss(); + } else if (progress.status === "error") { + setIsUpdatingVersions(false); + showErrorToast("Failed to check for new versions", { + duration: 4000, + }); + toast.dismiss(); + } + } + ); + + return () => { + void unlisten.then((fn) => { + fn(); + }); + }; + }, []); + + const formatTime = (seconds: number): string => { + if (seconds < 60) { + return `${Math.round(seconds)}s`; + } else if (seconds < 3600) { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.round(seconds % 60); + return `${minutes}m ${remainingSeconds}s`; + } else { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + return `${hours}h ${minutes}m`; + } + }; + + const formatBytes = (bytes: number): string => { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${Number.parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${ + sizes[i] + }`; + }; + + const loadVersions = useCallback(async (browserStr: string) => { + const browserName = getBrowserDisplayName(browserStr); + + // Show fetching toast + const toastId = showFetchingToast(browserName, { + id: `fetch-${browserStr}`, + }); + + try { + const versionInfos = await invoke( + "fetch_browser_versions_cached_first", + { browserStr } + ); + + // Convert BrowserVersionInfo to GithubRelease format for compatibility + const githubReleases: GithubRelease[] = versionInfos.map( + (versionInfo) => ({ + tag_name: versionInfo.version, + assets: [], + published_at: versionInfo.date, + is_alpha: versionInfo.is_prerelease, + }) + ); + + setAvailableVersions(githubReleases); + dismissToast(toastId); + return githubReleases; + } catch (error) { + console.error("Failed to load versions:", error); + dismissToast(toastId); + showErrorToast(`Failed to fetch ${browserName} versions`, { + description: + error instanceof Error ? error.message : "Unknown error occurred", + duration: 4000, + }); + throw error; + } + }, []); + + const loadVersionsWithNewCount = useCallback(async (browserStr: string) => { + const browserName = getBrowserDisplayName(browserStr); + + try { + // Get versions with new count info and cached detailed info + const result = await invoke( + "fetch_browser_versions_with_count_cached_first", + { browserStr } + ); + + // Get detailed version info for compatibility + const versionInfos = await invoke( + "fetch_browser_versions_cached_first", + { browserStr } + ); + + // Convert BrowserVersionInfo to GithubRelease format for compatibility + const githubReleases: GithubRelease[] = versionInfos.map( + (versionInfo) => ({ + tag_name: versionInfo.version, + assets: [], + published_at: versionInfo.date, + is_alpha: versionInfo.is_prerelease, + }) + ); + + setAvailableVersions(githubReleases); + + // Show notification about new versions if any were found + if (result.new_versions_count && result.new_versions_count > 0) { + showSuccessToast( + `Found ${result.new_versions_count} new ${browserName} versions!`, + { + duration: 3000, + description: `Total available: ${result.total_versions_count} versions`, + } + ); + } + + return githubReleases; + } catch (error) { + console.error("Failed to load versions:", error); + showErrorToast(`Failed to fetch ${browserName} versions`, { + description: + error instanceof Error ? error.message : "Unknown error occurred", + duration: 4000, + }); + throw error; + } + }, []); + + const loadDownloadedVersions = useCallback(async (browserStr: string) => { + try { + const downloadedVersions = await invoke( + "get_downloaded_browser_versions", + { browserStr } + ); + setDownloadedVersions(downloadedVersions); + return downloadedVersions; + } catch (error) { + console.error("Failed to load downloaded versions:", error); + throw error; + } + }, []); + + const downloadBrowser = useCallback( + async ( + browserStr: string, + version: string, + suppressNotifications: boolean = false + ) => { + const browserName = getBrowserDisplayName(browserStr); + setIsDownloading(true); + + try { + await invoke("download_browser", { browserStr, version }); + await loadDownloadedVersions(browserStr); + } catch (error) { + console.error("Failed to download browser:", error); + + if (!suppressNotifications) { + // Dismiss any existing download toast and show error + dismissToast(`download-${browserStr}-${version}`); + showErrorToast(`Failed to download ${browserName} ${version}`, { + description: + error instanceof Error ? error.message : "Unknown error occurred", + }); + } + throw error; + } finally { + setIsDownloading(false); + } + }, + [loadDownloadedVersions] + ); + + const isVersionDownloaded = useCallback( + (version: string) => { + return downloadedVersions.includes(version); + }, + [downloadedVersions] + ); + + return { + availableVersions, + downloadedVersions, + isDownloading, + downloadProgress, + isUpdatingVersions, + loadVersions, + loadVersionsWithNewCount, + loadDownloadedVersions, + downloadBrowser, + isVersionDownloaded, + formatBytes, + formatTime, + }; +} diff --git a/src/hooks/use-table-sorting.ts b/src/hooks/use-table-sorting.ts new file mode 100644 index 0000000..7dfeeb8 --- /dev/null +++ b/src/hooks/use-table-sorting.ts @@ -0,0 +1,79 @@ +import { invoke } from "@tauri-apps/api/core"; +import { useCallback, useEffect, useState } from "react"; +import type { TableSortingSettings } from "@/types"; +import type { SortingState } from "@tanstack/react-table"; + +export function useTableSorting() { + const [sortingSettings, setSortingSettings] = useState({ + column: "name", + direction: "asc", + }); + const [isLoaded, setIsLoaded] = useState(false); + + // Load sorting settings on mount + useEffect(() => { + const loadSettings = async () => { + try { + const settings = await invoke( + "get_table_sorting_settings" + ); + setSortingSettings(settings); + } catch (error) { + console.error("Failed to load table sorting settings:", error); + // Keep default settings if loading fails + } finally { + setIsLoaded(true); + } + }; + + void loadSettings(); + }, []); + + // Save sorting settings to disk + const saveSortingSettings = useCallback( + async (settings: TableSortingSettings) => { + try { + await invoke("save_table_sorting_settings", { sorting: settings }); + setSortingSettings(settings); + } catch (error) { + console.error("Failed to save table sorting settings:", error); + } + }, + [] + ); + + // Convert our settings to tanstack table sorting format + const getTableSorting = useCallback((): SortingState => { + if (!isLoaded) return []; + + return [ + { + id: sortingSettings.column, + desc: sortingSettings.direction === "desc", + }, + ]; + }, [sortingSettings, isLoaded]); + + // Update sorting when table state changes + const updateSorting = useCallback( + (sorting: SortingState) => { + if (!isLoaded) return; + + if (sorting.length > 0) { + const newSettings: TableSortingSettings = { + column: sorting[0].id, + direction: sorting[0].desc ? "desc" : "asc", + }; + void saveSortingSettings(newSettings); + } + }, + [saveSortingSettings, isLoaded] + ); + + return { + sortingSettings, + isLoaded, + getTableSorting, + updateSorting, + }; +} diff --git a/src/hooks/use-version-updater.ts b/src/hooks/use-version-updater.ts new file mode 100644 index 0000000..e1edbe7 --- /dev/null +++ b/src/hooks/use-version-updater.ts @@ -0,0 +1,268 @@ +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import { useCallback, useEffect, useState } from "react"; +import { toast } from "sonner"; +import { + showVersionUpdateToast, + showLoadingToast, + dismissToast, +} from "../components/custom-toast"; +import { getBrowserDisplayName } from "@/lib/browser-utils"; + +interface VersionUpdateProgress { + current_browser: string; + total_browsers: number; + completed_browsers: number; + new_versions_found: number; + browser_new_versions: number; + status: string; // "updating", "completed", "error" +} + +interface BackgroundUpdateResult { + browser: string; + new_versions_count: number; + total_versions_count: number; + updated_successfully: boolean; + error?: string; +} + +interface BrowserVersionsResult { + versions: string[]; + new_versions_count?: number; + total_versions_count: number; +} + +export function useVersionUpdater() { + const [isUpdating, setIsUpdating] = useState(false); + const [lastUpdateTime, setLastUpdateTime] = useState(null); + const [timeUntilNextUpdate, setTimeUntilNextUpdate] = useState(0); + const [updateProgress, setUpdateProgress] = + useState(null); + + // Listen for version update progress events + useEffect(() => { + const unlisten = listen( + "version-update-progress", + (event) => { + const progress = event.payload; + setUpdateProgress(progress); + + if (progress.status === "updating") { + setIsUpdating(true); + + if (progress.current_browser) { + const browserName = getBrowserDisplayName(progress.current_browser); + showVersionUpdateToast( + `Downloading release information for ${browserName}`, + { + id: "version-update-progress", + progress: { + current: progress.completed_browsers + 1, + total: progress.total_browsers, + found: progress.new_versions_found, + }, + } + ); + } else { + showLoadingToast("Starting version update check...", { + id: "version-update-progress", + description: "Initializing browser version check...", + }); + } + } else if (progress.status === "completed") { + setIsUpdating(false); + setUpdateProgress(null); + + if (progress.new_versions_found > 0) { + toast.success( + `Found ${progress.new_versions_found} new browser versions!`, + { + id: "version-update-progress", + duration: 4000, + description: + "Version information has been updated in the background", + } + ); + } else { + toast.success("No new browser versions found", { + id: "version-update-progress", + duration: 3000, + description: "All browser versions are up to date", + }); + } + + // Refresh status + void loadUpdateStatus(); + } else if (progress.status === "error") { + setIsUpdating(false); + setUpdateProgress(null); + + toast.error("Failed to update browser versions", { + id: "version-update-progress", + duration: 4000, + description: "Check your internet connection and try again", + }); + } + } + ); + + return () => { + void unlisten.then((fn) => { + fn(); + }); + }; + }, []); + + // Load update status on mount and periodically + useEffect(() => { + void loadUpdateStatus(); + + // Update status every minute + const interval = setInterval(() => { + void loadUpdateStatus(); + }, 60000); + + return () => { + clearInterval(interval); + }; + }, []); + + const loadUpdateStatus = useCallback(async () => { + try { + const [lastUpdate, timeUntilNext] = await invoke<[number | null, number]>( + "get_version_update_status" + ); + setLastUpdateTime(lastUpdate); + setTimeUntilNextUpdate(timeUntilNext); + } catch (error) { + console.error("Failed to load version update status:", error); + } + }, []); + + const triggerManualUpdate = useCallback(async () => { + try { + setIsUpdating(true); + const results = await invoke( + "trigger_manual_version_update" + ); + + const totalNewVersions = results.reduce( + (sum, result) => sum + result.new_versions_count, + 0 + ); + const successfulUpdates = results.filter( + (r) => r.updated_successfully + ).length; + const failedUpdates = results.filter( + (r) => !r.updated_successfully + ).length; + + if (failedUpdates > 0) { + toast.warning(`Update completed with some errors`, { + description: `${totalNewVersions} new versions found, ${failedUpdates} browsers failed to update`, + duration: 5000, + }); + } else if (totalNewVersions > 0) { + toast.success(`Found ${totalNewVersions} new browser versions!`, { + description: `Updated ${successfulUpdates} browsers successfully`, + duration: 4000, + }); + } else { + toast.success("No new browser versions found", { + description: "All browser versions are up to date", + duration: 3000, + }); + } + + await loadUpdateStatus(); + return results; + } catch (error) { + console.error("Failed to trigger manual update:", error); + toast.error("Failed to update browser versions", { + description: + error instanceof Error ? error.message : "Unknown error occurred", + duration: 4000, + }); + throw error; + } finally { + setIsUpdating(false); + } + }, [loadUpdateStatus]); + + const fetchBrowserVersionsWithNewCount = useCallback( + async (browserStr: string) => { + try { + const result = await invoke( + "fetch_browser_versions_with_count", + { browserStr } + ); + + // Show notification about new versions if any were found + if (result.new_versions_count && result.new_versions_count > 0) { + const browserName = getBrowserDisplayName(browserStr); + toast.success( + `Found ${result.new_versions_count} new ${browserName} versions!`, + { + duration: 3000, + description: `Total available: ${result.total_versions_count} versions`, + } + ); + } + + return result; + } catch (error) { + console.error("Failed to fetch browser versions with count:", error); + throw error; + } + }, + [] + ); + + const formatTimeUntilUpdate = useCallback((seconds: number): string => { + if (seconds <= 0) return "Update overdue"; + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + if (hours > 0) { + return `${hours}h ${minutes}m`; + } else if (minutes > 0) { + return `${minutes}m`; + } else { + return "< 1m"; + } + }, []); + + const formatLastUpdateTime = useCallback( + (timestamp: number | null): string => { + if (!timestamp) return "Never"; + + const date = new Date(timestamp * 1000); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); + + if (diffHours > 0) { + return `${diffHours}h ${diffMinutes}m ago`; + } else if (diffMinutes > 0) { + return `${diffMinutes}m ago`; + } else { + return "Just now"; + } + }, + [] + ); + + return { + isUpdating, + lastUpdateTime, + timeUntilNextUpdate, + updateProgress, + triggerManualUpdate, + fetchBrowserVersionsWithNewCount, + formatTimeUntilUpdate, + formatLastUpdateTime, + loadUpdateStatus, + }; +} diff --git a/src/lib/browser-utils.ts b/src/lib/browser-utils.ts new file mode 100644 index 0000000..ac3ca2b --- /dev/null +++ b/src/lib/browser-utils.ts @@ -0,0 +1,58 @@ +/** + * Browser utility functions + * Centralized helpers for browser name mapping, icons, etc. + */ + +import { SiMullvad, SiBrave, SiTorbrowser } from "react-icons/si"; +import { FaChrome, FaFirefox } from "react-icons/fa"; + +/** + * Map internal browser names to display names + */ +export function getBrowserDisplayName(browserType: string): string { + const browserNames: Record = { + firefox: "Firefox", + "firefox-developer": "Firefox Developer Edition", + "mullvad-browser": "Mullvad Browser", + zen: "Zen Browser", + brave: "Brave", + chromium: "Chromium", + "tor-browser": "Tor Browser", + }; + + return browserNames[browserType] || browserType; +} + +/** + * Get the appropriate icon component for a browser type + */ +export function getBrowserIcon(browserType: string) { + switch (browserType) { + case "mullvad-browser": + return SiMullvad; + case "chromium": + return FaChrome; + case "brave": + return SiBrave; + case "firefox": + case "firefox-developer": + return FaFirefox; + case "zen": + return FaFirefox; + case "tor-browser": + return SiTorbrowser; + default: + return null; + } +} + +/** + * Format browser name by capitalizing words and joining with spaces + * (fallback method for simple transformations) + */ +export function formatBrowserName(browserType: string): string { + return browserType + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..365058c --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/styles/globals.css b/src/styles/globals.css new file mode 100644 index 0000000..76ca069 --- /dev/null +++ b/src/styles/globals.css @@ -0,0 +1,125 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme { + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.21 0.006 285.885); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.705 0.015 286.067); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.21 0.006 285.885); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.705 0.015 286.067); +} + +.dark { + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.92 0.004 286.32); + --primary-foreground: oklch(0.21 0.006 285.885); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.552 0.016 285.938); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.552 0.016 285.938); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..0d618ce --- /dev/null +++ b/src/types.ts @@ -0,0 +1,21 @@ +export interface ProxySettings { + enabled: boolean; + proxy_type: string; // "http", "https", "socks4", or "socks5" + host: string; + port: number; +} + +export interface TableSortingSettings { + column: string; // "name", "browser", "status" + direction: string; // "asc" or "desc" +} + +export interface BrowserProfile { + name: string; + browser: string; + version: string; + profile_path: string; + proxy?: ProxySettings; + process_id?: number; + last_launch?: number; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..19e161f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "target": "ES2021", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "strictNullChecks": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "next-env.d.ts", + "dist/types/**/*.ts" + ], + "exclude": ["node_modules"] +}